You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
45070 lines
1.6 MiB
45070 lines
1.6 MiB
// CodeMirror, copyright (c) by Marijn Haverbeke and others |
|
// Distributed under an MIT license: http://codemirror.net/LICENSE |
|
|
|
// This is CodeMirror (http://codemirror.net), a code editor |
|
// implemented in JavaScript on top of the browser's DOM. |
|
// |
|
// You can find some technical background for some of the code below |
|
// at http://marijnhaverbeke.nl/blog/#cm-internals . |
|
|
|
(function(mod) { |
|
this.CodeMirror = mod(); |
|
})(function() { |
|
"use strict"; |
|
|
|
// BROWSER SNIFFING |
|
|
|
// Kludges for bugs and behavior differences that can't be feature |
|
// detected are enabled based on userAgent etc sniffing. |
|
|
|
var gecko = /gecko\/\d/i.test(navigator.userAgent); |
|
var ie_upto10 = /MSIE \d/.test(navigator.userAgent); |
|
var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); |
|
var ie = ie_upto10 || ie_11up; |
|
var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); |
|
var webkit = /WebKit\//.test(navigator.userAgent); |
|
var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); |
|
var chrome = /Chrome\//.test(navigator.userAgent); |
|
var presto = /Opera\//.test(navigator.userAgent); |
|
var safari = /Apple Computer/.test(navigator.vendor); |
|
var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); |
|
var phantom = /PhantomJS/.test(navigator.userAgent); |
|
|
|
var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); |
|
// This is woefully incomplete. Suggestions for alternative methods welcome. |
|
var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); |
|
var mac = ios || /Mac/.test(navigator.platform); |
|
var windows = /win/i.test(navigator.platform); |
|
|
|
var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); |
|
if (presto_version) presto_version = Number(presto_version[1]); |
|
if (presto_version && presto_version >= 15) { presto = false; webkit = true; } |
|
// Some browsers use the wrong event properties to signal cmd/ctrl on OS X |
|
var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); |
|
var captureRightClick = gecko || (ie && ie_version >= 9); |
|
|
|
// Optimize some code when these features are not used. |
|
var sawReadOnlySpans = false, sawCollapsedSpans = false; |
|
|
|
// EDITOR CONSTRUCTOR |
|
|
|
// A CodeMirror instance represents an editor. This is the object |
|
// that user code is usually dealing with. |
|
|
|
function CodeMirror(place, options) { |
|
if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); |
|
|
|
this.options = options = options ? copyObj(options) : {}; |
|
// Determine effective options based on given values and defaults. |
|
copyObj(defaults, options, false); |
|
setGuttersForLineNumbers(options); |
|
|
|
var doc = options.value; |
|
if (typeof doc == "string") doc = new Doc(doc, options.mode); |
|
this.doc = doc; |
|
|
|
var input = new CodeMirror.inputStyles[options.inputStyle](this); |
|
var display = this.display = new Display(place, doc, input); |
|
display.wrapper.CodeMirror = this; |
|
updateGutters(this); |
|
themeChanged(this); |
|
if (options.lineWrapping) |
|
this.display.wrapper.className += " CodeMirror-wrap"; |
|
if (options.autofocus && !mobile) display.input.focus(); |
|
initScrollbars(this); |
|
|
|
this.state = { |
|
keyMaps: [], // stores maps added by addKeyMap |
|
overlays: [], // highlighting overlays, as added by addOverlay |
|
modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info |
|
overwrite: false, |
|
delayingBlurEvent: false, |
|
focused: false, |
|
suppressEdits: false, // used to disable editing during key handlers when in readOnly mode |
|
pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll |
|
draggingText: false, |
|
highlight: new Delayed(), // stores highlight worker timeout |
|
keySeq: null, // Unfinished key sequence |
|
specialChars: null |
|
}; |
|
|
|
var cm = this; |
|
|
|
// Override magic textarea content restore that IE sometimes does |
|
// on our hidden textarea on reload |
|
if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20); |
|
|
|
registerEventHandlers(this); |
|
ensureGlobalHandlers(); |
|
|
|
startOperation(this); |
|
this.curOp.forceUpdate = true; |
|
attachDoc(this, doc); |
|
|
|
if ((options.autofocus && !mobile) || cm.hasFocus()) |
|
setTimeout(bind(onFocus, this), 20); |
|
else |
|
onBlur(this); |
|
|
|
for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) |
|
optionHandlers[opt](this, options[opt], Init); |
|
maybeUpdateLineNumberWidth(this); |
|
if (options.finishInit) options.finishInit(this); |
|
for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); |
|
endOperation(this); |
|
// Suppress optimizelegibility in Webkit, since it breaks text |
|
// measuring on line wrapping boundaries. |
|
if (webkit && options.lineWrapping && |
|
getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") |
|
display.lineDiv.style.textRendering = "auto"; |
|
} |
|
|
|
// DISPLAY CONSTRUCTOR |
|
|
|
// The display handles the DOM integration, both for input reading |
|
// and content drawing. It holds references to DOM nodes and |
|
// display-related state. |
|
|
|
function Display(place, doc, input) { |
|
var d = this; |
|
this.input = input; |
|
|
|
// Covers bottom-right square when both scrollbars are present. |
|
d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); |
|
d.scrollbarFiller.setAttribute("cm-not-content", "true"); |
|
// Covers bottom of gutter when coverGutterNextToScrollbar is on |
|
// and h scrollbar is present. |
|
d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); |
|
d.gutterFiller.setAttribute("cm-not-content", "true"); |
|
// Will contain the actual code, positioned to cover the viewport. |
|
d.lineDiv = elt("div", null, "CodeMirror-code"); |
|
// Elements are added to these to represent selection and cursors. |
|
d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); |
|
d.cursorDiv = elt("div", null, "CodeMirror-cursors"); |
|
// A visibility: hidden element used to find the size of things. |
|
d.measure = elt("div", null, "CodeMirror-measure"); |
|
// When lines outside of the viewport are measured, they are drawn in this. |
|
d.lineMeasure = elt("div", null, "CodeMirror-measure"); |
|
// Wraps everything that needs to exist inside the vertically-padded coordinate system |
|
d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], |
|
null, "position: relative; outline: none"); |
|
// Moved around its parent to cover visible view. |
|
d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); |
|
// Set to the height of the document, allowing scrolling. |
|
d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); |
|
d.sizerWidth = null; |
|
// Behavior of elts with overflow: auto and padding is |
|
// inconsistent across browsers. This is used to ensure the |
|
// scrollable area is big enough. |
|
d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); |
|
// Will contain the gutters, if any. |
|
d.gutters = elt("div", null, "CodeMirror-gutters"); |
|
d.lineGutter = null; |
|
// Actual scrollable element. |
|
d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); |
|
d.scroller.setAttribute("tabIndex", "-1"); |
|
// The element in which the editor lives. |
|
d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); |
|
|
|
// Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) |
|
if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } |
|
if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; |
|
|
|
if (place) { |
|
if (place.appendChild) place.appendChild(d.wrapper); |
|
else place(d.wrapper); |
|
} |
|
|
|
// Current rendered range (may be bigger than the view window). |
|
d.viewFrom = d.viewTo = doc.first; |
|
d.reportedViewFrom = d.reportedViewTo = doc.first; |
|
// Information about the rendered lines. |
|
d.view = []; |
|
d.renderedView = null; |
|
// Holds info about a single rendered line when it was rendered |
|
// for measurement, while not in view. |
|
d.externalMeasured = null; |
|
// Empty space (in pixels) above the view |
|
d.viewOffset = 0; |
|
d.lastWrapHeight = d.lastWrapWidth = 0; |
|
d.updateLineNumbers = null; |
|
|
|
d.nativeBarWidth = d.barHeight = d.barWidth = 0; |
|
d.scrollbarsClipped = false; |
|
|
|
// Used to only resize the line number gutter when necessary (when |
|
// the amount of lines crosses a boundary that makes its width change) |
|
d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; |
|
// Set to true when a non-horizontal-scrolling line widget is |
|
// added. As an optimization, line widget aligning is skipped when |
|
// this is false. |
|
d.alignWidgets = false; |
|
|
|
d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; |
|
|
|
// Tracks the maximum line length so that the horizontal scrollbar |
|
// can be kept static when scrolling. |
|
d.maxLine = null; |
|
d.maxLineLength = 0; |
|
d.maxLineChanged = false; |
|
|
|
// Used for measuring wheel scrolling granularity |
|
d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; |
|
|
|
// True when shift is held down. |
|
d.shift = false; |
|
|
|
// Used to track whether anything happened since the context menu |
|
// was opened. |
|
d.selForContextMenu = null; |
|
|
|
d.activeTouch = null; |
|
|
|
input.init(d); |
|
} |
|
|
|
// STATE UPDATES |
|
|
|
// Used to get the editor into a consistent state again when options change. |
|
|
|
function loadMode(cm) { |
|
cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); |
|
resetModeState(cm); |
|
} |
|
|
|
function resetModeState(cm) { |
|
cm.doc.iter(function(line) { |
|
if (line.stateAfter) line.stateAfter = null; |
|
if (line.styles) line.styles = null; |
|
}); |
|
cm.doc.frontier = cm.doc.first; |
|
startWorker(cm, 100); |
|
cm.state.modeGen++; |
|
if (cm.curOp) regChange(cm); |
|
} |
|
|
|
function wrappingChanged(cm) { |
|
if (cm.options.lineWrapping) { |
|
addClass(cm.display.wrapper, "CodeMirror-wrap"); |
|
cm.display.sizer.style.minWidth = ""; |
|
cm.display.sizerWidth = null; |
|
} else { |
|
rmClass(cm.display.wrapper, "CodeMirror-wrap"); |
|
findMaxLine(cm); |
|
} |
|
estimateLineHeights(cm); |
|
regChange(cm); |
|
clearCaches(cm); |
|
setTimeout(function(){updateScrollbars(cm);}, 100); |
|
} |
|
|
|
// Returns a function that estimates the height of a line, to use as |
|
// first approximation until the line becomes visible (and is thus |
|
// properly measurable). |
|
function estimateHeight(cm) { |
|
var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; |
|
var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); |
|
return function(line) { |
|
if (lineIsHidden(cm.doc, line)) return 0; |
|
|
|
var widgetsHeight = 0; |
|
if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { |
|
if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; |
|
} |
|
|
|
if (wrapping) |
|
return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; |
|
else |
|
return widgetsHeight + th; |
|
}; |
|
} |
|
|
|
function estimateLineHeights(cm) { |
|
var doc = cm.doc, est = estimateHeight(cm); |
|
doc.iter(function(line) { |
|
var estHeight = est(line); |
|
if (estHeight != line.height) updateLineHeight(line, estHeight); |
|
}); |
|
} |
|
|
|
function themeChanged(cm) { |
|
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + |
|
cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); |
|
clearCaches(cm); |
|
} |
|
|
|
function guttersChanged(cm) { |
|
updateGutters(cm); |
|
regChange(cm); |
|
setTimeout(function(){alignHorizontally(cm);}, 20); |
|
} |
|
|
|
// Rebuild the gutter elements, ensure the margin to the left of the |
|
// code matches their width. |
|
function updateGutters(cm) { |
|
var gutters = cm.display.gutters, specs = cm.options.gutters; |
|
removeChildren(gutters); |
|
for (var i = 0; i < specs.length; ++i) { |
|
var gutterClass = specs[i]; |
|
var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); |
|
if (gutterClass == "CodeMirror-linenumbers") { |
|
cm.display.lineGutter = gElt; |
|
gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; |
|
} |
|
} |
|
gutters.style.display = i ? "" : "none"; |
|
updateGutterSpace(cm); |
|
} |
|
|
|
function updateGutterSpace(cm) { |
|
var width = cm.display.gutters.offsetWidth; |
|
cm.display.sizer.style.marginLeft = width + "px"; |
|
} |
|
|
|
// Compute the character length of a line, taking into account |
|
// collapsed ranges (see markText) that might hide parts, and join |
|
// other lines onto it. |
|
function lineLength(line) { |
|
if (line.height == 0) return 0; |
|
var len = line.text.length, merged, cur = line; |
|
while (merged = collapsedSpanAtStart(cur)) { |
|
var found = merged.find(0, true); |
|
cur = found.from.line; |
|
len += found.from.ch - found.to.ch; |
|
} |
|
cur = line; |
|
while (merged = collapsedSpanAtEnd(cur)) { |
|
var found = merged.find(0, true); |
|
len -= cur.text.length - found.from.ch; |
|
cur = found.to.line; |
|
len += cur.text.length - found.to.ch; |
|
} |
|
return len; |
|
} |
|
|
|
// Find the longest line in the document. |
|
function findMaxLine(cm) { |
|
var d = cm.display, doc = cm.doc; |
|
d.maxLine = getLine(doc, doc.first); |
|
d.maxLineLength = lineLength(d.maxLine); |
|
d.maxLineChanged = true; |
|
doc.iter(function(line) { |
|
var len = lineLength(line); |
|
if (len > d.maxLineLength) { |
|
d.maxLineLength = len; |
|
d.maxLine = line; |
|
} |
|
}); |
|
} |
|
|
|
// Make sure the gutters options contains the element |
|
// "CodeMirror-linenumbers" when the lineNumbers option is true. |
|
function setGuttersForLineNumbers(options) { |
|
var found = indexOf(options.gutters, "CodeMirror-linenumbers"); |
|
if (found == -1 && options.lineNumbers) { |
|
options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); |
|
} else if (found > -1 && !options.lineNumbers) { |
|
options.gutters = options.gutters.slice(0); |
|
options.gutters.splice(found, 1); |
|
} |
|
} |
|
|
|
// SCROLLBARS |
|
|
|
// Prepare DOM reads needed to update the scrollbars. Done in one |
|
// shot to minimize update/measure roundtrips. |
|
function measureForScrollbars(cm) { |
|
var d = cm.display, gutterW = d.gutters.offsetWidth; |
|
var docH = Math.round(cm.doc.height + paddingVert(cm.display)); |
|
return { |
|
clientHeight: d.scroller.clientHeight, |
|
viewHeight: d.wrapper.clientHeight, |
|
scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, |
|
viewWidth: d.wrapper.clientWidth, |
|
barLeft: cm.options.fixedGutter ? gutterW : 0, |
|
docHeight: docH, |
|
scrollHeight: docH + scrollGap(cm) + d.barHeight, |
|
nativeBarWidth: d.nativeBarWidth, |
|
gutterWidth: gutterW |
|
}; |
|
} |
|
|
|
function NativeScrollbars(place, scroll, cm) { |
|
this.cm = cm; |
|
var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); |
|
var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); |
|
place(vert); place(horiz); |
|
|
|
on(vert, "scroll", function() { |
|
if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); |
|
}); |
|
on(horiz, "scroll", function() { |
|
if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); |
|
}); |
|
|
|
this.checkedOverlay = false; |
|
// Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). |
|
if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; |
|
} |
|
|
|
NativeScrollbars.prototype = copyObj({ |
|
update: function(measure) { |
|
var needsH = measure.scrollWidth > measure.clientWidth + 1; |
|
var needsV = measure.scrollHeight > measure.clientHeight + 1; |
|
var sWidth = measure.nativeBarWidth; |
|
|
|
if (needsV) { |
|
this.vert.style.display = "block"; |
|
this.vert.style.bottom = needsH ? sWidth + "px" : "0"; |
|
var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); |
|
// A bug in IE8 can cause this value to be negative, so guard it. |
|
this.vert.firstChild.style.height = |
|
Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; |
|
} else { |
|
this.vert.style.display = ""; |
|
this.vert.firstChild.style.height = "0"; |
|
} |
|
|
|
if (needsH) { |
|
this.horiz.style.display = "block"; |
|
this.horiz.style.right = needsV ? sWidth + "px" : "0"; |
|
this.horiz.style.left = measure.barLeft + "px"; |
|
var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); |
|
this.horiz.firstChild.style.width = |
|
(measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; |
|
} else { |
|
this.horiz.style.display = ""; |
|
this.horiz.firstChild.style.width = "0"; |
|
} |
|
|
|
if (!this.checkedOverlay && measure.clientHeight > 0) { |
|
if (sWidth == 0) this.overlayHack(); |
|
this.checkedOverlay = true; |
|
} |
|
|
|
return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; |
|
}, |
|
setScrollLeft: function(pos) { |
|
if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; |
|
}, |
|
setScrollTop: function(pos) { |
|
if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; |
|
}, |
|
overlayHack: function() { |
|
var w = mac && !mac_geMountainLion ? "12px" : "18px"; |
|
this.horiz.style.minHeight = this.vert.style.minWidth = w; |
|
var self = this; |
|
var barMouseDown = function(e) { |
|
if (e_target(e) != self.vert && e_target(e) != self.horiz) |
|
operation(self.cm, onMouseDown)(e); |
|
}; |
|
on(this.vert, "mousedown", barMouseDown); |
|
on(this.horiz, "mousedown", barMouseDown); |
|
}, |
|
clear: function() { |
|
var parent = this.horiz.parentNode; |
|
parent.removeChild(this.horiz); |
|
parent.removeChild(this.vert); |
|
} |
|
}, NativeScrollbars.prototype); |
|
|
|
function NullScrollbars() {} |
|
|
|
NullScrollbars.prototype = copyObj({ |
|
update: function() { return {bottom: 0, right: 0}; }, |
|
setScrollLeft: function() {}, |
|
setScrollTop: function() {}, |
|
clear: function() {} |
|
}, NullScrollbars.prototype); |
|
|
|
CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; |
|
|
|
function initScrollbars(cm) { |
|
if (cm.display.scrollbars) { |
|
cm.display.scrollbars.clear(); |
|
if (cm.display.scrollbars.addClass) |
|
rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); |
|
} |
|
|
|
cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { |
|
cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); |
|
// Prevent clicks in the scrollbars from killing focus |
|
on(node, "mousedown", function() { |
|
if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0); |
|
}); |
|
node.setAttribute("cm-not-content", "true"); |
|
}, function(pos, axis) { |
|
if (axis == "horizontal") setScrollLeft(cm, pos); |
|
else setScrollTop(cm, pos); |
|
}, cm); |
|
if (cm.display.scrollbars.addClass) |
|
addClass(cm.display.wrapper, cm.display.scrollbars.addClass); |
|
} |
|
|
|
function updateScrollbars(cm, measure) { |
|
if (!measure) measure = measureForScrollbars(cm); |
|
var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; |
|
updateScrollbarsInner(cm, measure); |
|
for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { |
|
if (startWidth != cm.display.barWidth && cm.options.lineWrapping) |
|
updateHeightsInViewport(cm); |
|
updateScrollbarsInner(cm, measureForScrollbars(cm)); |
|
startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; |
|
} |
|
} |
|
|
|
// Re-synchronize the fake scrollbars with the actual size of the |
|
// content. |
|
function updateScrollbarsInner(cm, measure) { |
|
var d = cm.display; |
|
var sizes = d.scrollbars.update(measure); |
|
|
|
d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; |
|
d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; |
|
|
|
if (sizes.right && sizes.bottom) { |
|
d.scrollbarFiller.style.display = "block"; |
|
d.scrollbarFiller.style.height = sizes.bottom + "px"; |
|
d.scrollbarFiller.style.width = sizes.right + "px"; |
|
} else d.scrollbarFiller.style.display = ""; |
|
if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { |
|
d.gutterFiller.style.display = "block"; |
|
d.gutterFiller.style.height = sizes.bottom + "px"; |
|
d.gutterFiller.style.width = measure.gutterWidth + "px"; |
|
} else d.gutterFiller.style.display = ""; |
|
} |
|
|
|
// Compute the lines that are visible in a given viewport (defaults |
|
// the the current scroll position). viewport may contain top, |
|
// height, and ensure (see op.scrollToPos) properties. |
|
function visibleLines(display, doc, viewport) { |
|
var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; |
|
top = Math.floor(top - paddingTop(display)); |
|
var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; |
|
|
|
var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); |
|
// Ensure is a {from: {line, ch}, to: {line, ch}} object, and |
|
// forces those lines into the viewport (if possible). |
|
if (viewport && viewport.ensure) { |
|
var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; |
|
if (ensureFrom < from) { |
|
from = ensureFrom; |
|
to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); |
|
} else if (Math.min(ensureTo, doc.lastLine()) >= to) { |
|
from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); |
|
to = ensureTo; |
|
} |
|
} |
|
return {from: from, to: Math.max(to, from + 1)}; |
|
} |
|
|
|
// LINE NUMBERS |
|
|
|
// Re-align line numbers and gutter marks to compensate for |
|
// horizontal scrolling. |
|
function alignHorizontally(cm) { |
|
var display = cm.display, view = display.view; |
|
if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; |
|
var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; |
|
var gutterW = display.gutters.offsetWidth, left = comp + "px"; |
|
for (var i = 0; i < view.length; i++) if (!view[i].hidden) { |
|
if (cm.options.fixedGutter && view[i].gutter) |
|
view[i].gutter.style.left = left; |
|
var align = view[i].alignable; |
|
if (align) for (var j = 0; j < align.length; j++) |
|
align[j].style.left = left; |
|
} |
|
if (cm.options.fixedGutter) |
|
display.gutters.style.left = (comp + gutterW) + "px"; |
|
} |
|
|
|
// Used to ensure that the line number gutter is still the right |
|
// size for the current document size. Returns true when an update |
|
// is needed. |
|
function maybeUpdateLineNumberWidth(cm) { |
|
if (!cm.options.lineNumbers) return false; |
|
var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; |
|
if (last.length != display.lineNumChars) { |
|
var test = display.measure.appendChild(elt("div", [elt("div", last)], |
|
"CodeMirror-linenumber CodeMirror-gutter-elt")); |
|
var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; |
|
display.lineGutter.style.width = ""; |
|
display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; |
|
display.lineNumWidth = display.lineNumInnerWidth + padding; |
|
display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; |
|
display.lineGutter.style.width = display.lineNumWidth + "px"; |
|
updateGutterSpace(cm); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
function lineNumberFor(options, i) { |
|
return String(options.lineNumberFormatter(i + options.firstLineNumber)); |
|
} |
|
|
|
// Computes display.scroller.scrollLeft + display.gutters.offsetWidth, |
|
// but using getBoundingClientRect to get a sub-pixel-accurate |
|
// result. |
|
function compensateForHScroll(display) { |
|
return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; |
|
} |
|
|
|
// DISPLAY DRAWING |
|
|
|
function DisplayUpdate(cm, viewport, force) { |
|
var display = cm.display; |
|
|
|
this.viewport = viewport; |
|
// Store some values that we'll need later (but don't want to force a relayout for) |
|
this.visible = visibleLines(display, cm.doc, viewport); |
|
this.editorIsHidden = !display.wrapper.offsetWidth; |
|
this.wrapperHeight = display.wrapper.clientHeight; |
|
this.wrapperWidth = display.wrapper.clientWidth; |
|
this.oldDisplayWidth = displayWidth(cm); |
|
this.force = force; |
|
this.dims = getDimensions(cm); |
|
this.events = []; |
|
} |
|
|
|
DisplayUpdate.prototype.signal = function(emitter, type) { |
|
if (hasHandler(emitter, type)) |
|
this.events.push(arguments); |
|
}; |
|
DisplayUpdate.prototype.finish = function() { |
|
for (var i = 0; i < this.events.length; i++) |
|
signal.apply(null, this.events[i]); |
|
}; |
|
|
|
function maybeClipScrollbars(cm) { |
|
var display = cm.display; |
|
if (!display.scrollbarsClipped && display.scroller.offsetWidth) { |
|
display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; |
|
display.heightForcer.style.height = scrollGap(cm) + "px"; |
|
display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; |
|
display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; |
|
display.scrollbarsClipped = true; |
|
} |
|
} |
|
|
|
// Does the actual updating of the line display. Bails out |
|
// (returning false) when there is nothing to be done and forced is |
|
// false. |
|
function updateDisplayIfNeeded(cm, update) { |
|
var display = cm.display, doc = cm.doc; |
|
|
|
if (update.editorIsHidden) { |
|
resetView(cm); |
|
return false; |
|
} |
|
|
|
// Bail out if the visible area is already rendered and nothing changed. |
|
if (!update.force && |
|
update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && |
|
(display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && |
|
display.renderedView == display.view && countDirtyView(cm) == 0) |
|
return false; |
|
|
|
if (maybeUpdateLineNumberWidth(cm)) { |
|
resetView(cm); |
|
update.dims = getDimensions(cm); |
|
} |
|
|
|
// Compute a suitable new viewport (from & to) |
|
var end = doc.first + doc.size; |
|
var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); |
|
var to = Math.min(end, update.visible.to + cm.options.viewportMargin); |
|
if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); |
|
if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); |
|
if (sawCollapsedSpans) { |
|
from = visualLineNo(cm.doc, from); |
|
to = visualLineEndNo(cm.doc, to); |
|
} |
|
|
|
var different = from != display.viewFrom || to != display.viewTo || |
|
display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; |
|
adjustView(cm, from, to); |
|
|
|
display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); |
|
// Position the mover div to align with the current scroll position |
|
cm.display.mover.style.top = display.viewOffset + "px"; |
|
|
|
var toUpdate = countDirtyView(cm); |
|
if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && |
|
(display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) |
|
return false; |
|
|
|
// For big changes, we hide the enclosing element during the |
|
// update, since that speeds up the operations on most browsers. |
|
var focused = activeElt(); |
|
if (toUpdate > 4) display.lineDiv.style.display = "none"; |
|
patchDisplay(cm, display.updateLineNumbers, update.dims); |
|
if (toUpdate > 4) display.lineDiv.style.display = ""; |
|
display.renderedView = display.view; |
|
// There might have been a widget with a focused element that got |
|
// hidden or updated, if so re-focus it. |
|
if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); |
|
|
|
// Prevent selection and cursors from interfering with the scroll |
|
// width and height. |
|
removeChildren(display.cursorDiv); |
|
removeChildren(display.selectionDiv); |
|
display.gutters.style.height = 0; |
|
|
|
if (different) { |
|
display.lastWrapHeight = update.wrapperHeight; |
|
display.lastWrapWidth = update.wrapperWidth; |
|
startWorker(cm, 400); |
|
} |
|
|
|
display.updateLineNumbers = null; |
|
|
|
return true; |
|
} |
|
|
|
function postUpdateDisplay(cm, update) { |
|
var viewport = update.viewport; |
|
for (var first = true;; first = false) { |
|
if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { |
|
// Clip forced viewport to actual scrollable area. |
|
if (viewport && viewport.top != null) |
|
viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; |
|
// Updated line heights might result in the drawn area not |
|
// actually covering the viewport. Keep looping until it does. |
|
update.visible = visibleLines(cm.display, cm.doc, viewport); |
|
if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) |
|
break; |
|
} |
|
if (!updateDisplayIfNeeded(cm, update)) break; |
|
updateHeightsInViewport(cm); |
|
var barMeasure = measureForScrollbars(cm); |
|
updateSelection(cm); |
|
setDocumentHeight(cm, barMeasure); |
|
updateScrollbars(cm, barMeasure); |
|
} |
|
|
|
update.signal(cm, "update", cm); |
|
if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { |
|
update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); |
|
cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; |
|
} |
|
} |
|
|
|
function updateDisplaySimple(cm, viewport) { |
|
var update = new DisplayUpdate(cm, viewport); |
|
if (updateDisplayIfNeeded(cm, update)) { |
|
updateHeightsInViewport(cm); |
|
postUpdateDisplay(cm, update); |
|
var barMeasure = measureForScrollbars(cm); |
|
updateSelection(cm); |
|
setDocumentHeight(cm, barMeasure); |
|
updateScrollbars(cm, barMeasure); |
|
update.finish(); |
|
} |
|
} |
|
|
|
function setDocumentHeight(cm, measure) { |
|
cm.display.sizer.style.minHeight = measure.docHeight + "px"; |
|
var total = measure.docHeight + cm.display.barHeight; |
|
cm.display.heightForcer.style.top = total + "px"; |
|
cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px"; |
|
} |
|
|
|
// Read the actual heights of the rendered lines, and update their |
|
// stored heights to match. |
|
function updateHeightsInViewport(cm) { |
|
var display = cm.display; |
|
var prevBottom = display.lineDiv.offsetTop; |
|
for (var i = 0; i < display.view.length; i++) { |
|
var cur = display.view[i], height; |
|
if (cur.hidden) continue; |
|
if (ie && ie_version < 8) { |
|
var bot = cur.node.offsetTop + cur.node.offsetHeight; |
|
height = bot - prevBottom; |
|
prevBottom = bot; |
|
} else { |
|
var box = cur.node.getBoundingClientRect(); |
|
height = box.bottom - box.top; |
|
} |
|
var diff = cur.line.height - height; |
|
if (height < 2) height = textHeight(display); |
|
if (diff > .001 || diff < -.001) { |
|
updateLineHeight(cur.line, height); |
|
updateWidgetHeight(cur.line); |
|
if (cur.rest) for (var j = 0; j < cur.rest.length; j++) |
|
updateWidgetHeight(cur.rest[j]); |
|
} |
|
} |
|
} |
|
|
|
// Read and store the height of line widgets associated with the |
|
// given line. |
|
function updateWidgetHeight(line) { |
|
if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) |
|
line.widgets[i].height = line.widgets[i].node.offsetHeight; |
|
} |
|
|
|
// Do a bulk-read of the DOM positions and sizes needed to draw the |
|
// view, so that we don't interleave reading and writing to the DOM. |
|
function getDimensions(cm) { |
|
var d = cm.display, left = {}, width = {}; |
|
var gutterLeft = d.gutters.clientLeft; |
|
for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { |
|
left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; |
|
width[cm.options.gutters[i]] = n.clientWidth; |
|
} |
|
return {fixedPos: compensateForHScroll(d), |
|
gutterTotalWidth: d.gutters.offsetWidth, |
|
gutterLeft: left, |
|
gutterWidth: width, |
|
wrapperWidth: d.wrapper.clientWidth}; |
|
} |
|
|
|
// Sync the actual display DOM structure with display.view, removing |
|
// nodes for lines that are no longer in view, and creating the ones |
|
// that are not there yet, and updating the ones that are out of |
|
// date. |
|
function patchDisplay(cm, updateNumbersFrom, dims) { |
|
var display = cm.display, lineNumbers = cm.options.lineNumbers; |
|
var container = display.lineDiv, cur = container.firstChild; |
|
|
|
function rm(node) { |
|
var next = node.nextSibling; |
|
// Works around a throw-scroll bug in OS X Webkit |
|
if (webkit && mac && cm.display.currentWheelTarget == node) |
|
node.style.display = "none"; |
|
else |
|
node.parentNode.removeChild(node); |
|
return next; |
|
} |
|
|
|
var view = display.view, lineN = display.viewFrom; |
|
// Loop over the elements in the view, syncing cur (the DOM nodes |
|
// in display.lineDiv) with the view as we go. |
|
for (var i = 0; i < view.length; i++) { |
|
var lineView = view[i]; |
|
if (lineView.hidden) { |
|
} else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet |
|
var node = buildLineElement(cm, lineView, lineN, dims); |
|
container.insertBefore(node, cur); |
|
} else { // Already drawn |
|
while (cur != lineView.node) cur = rm(cur); |
|
var updateNumber = lineNumbers && updateNumbersFrom != null && |
|
updateNumbersFrom <= lineN && lineView.lineNumber; |
|
if (lineView.changes) { |
|
if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; |
|
updateLineForChanges(cm, lineView, lineN, dims); |
|
} |
|
if (updateNumber) { |
|
removeChildren(lineView.lineNumber); |
|
lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); |
|
} |
|
cur = lineView.node.nextSibling; |
|
} |
|
lineN += lineView.size; |
|
} |
|
while (cur) cur = rm(cur); |
|
} |
|
|
|
// When an aspect of a line changes, a string is added to |
|
// lineView.changes. This updates the relevant part of the line's |
|
// DOM structure. |
|
function updateLineForChanges(cm, lineView, lineN, dims) { |
|
for (var j = 0; j < lineView.changes.length; j++) { |
|
var type = lineView.changes[j]; |
|
if (type == "text") updateLineText(cm, lineView); |
|
else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); |
|
else if (type == "class") updateLineClasses(lineView); |
|
else if (type == "widget") updateLineWidgets(cm, lineView, dims); |
|
} |
|
lineView.changes = null; |
|
} |
|
|
|
// Lines with gutter elements, widgets or a background class need to |
|
// be wrapped, and have the extra elements added to the wrapper div |
|
function ensureLineWrapped(lineView) { |
|
if (lineView.node == lineView.text) { |
|
lineView.node = elt("div", null, null, "position: relative"); |
|
if (lineView.text.parentNode) |
|
lineView.text.parentNode.replaceChild(lineView.node, lineView.text); |
|
lineView.node.appendChild(lineView.text); |
|
if (ie && ie_version < 8) lineView.node.style.zIndex = 2; |
|
} |
|
return lineView.node; |
|
} |
|
|
|
function updateLineBackground(lineView) { |
|
var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; |
|
if (cls) cls += " CodeMirror-linebackground"; |
|
if (lineView.background) { |
|
if (cls) lineView.background.className = cls; |
|
else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } |
|
} else if (cls) { |
|
var wrap = ensureLineWrapped(lineView); |
|
lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); |
|
} |
|
} |
|
|
|
// Wrapper around buildLineContent which will reuse the structure |
|
// in display.externalMeasured when possible. |
|
function getLineContent(cm, lineView) { |
|
var ext = cm.display.externalMeasured; |
|
if (ext && ext.line == lineView.line) { |
|
cm.display.externalMeasured = null; |
|
lineView.measure = ext.measure; |
|
return ext.built; |
|
} |
|
return buildLineContent(cm, lineView); |
|
} |
|
|
|
// Redraw the line's text. Interacts with the background and text |
|
// classes because the mode may output tokens that influence these |
|
// classes. |
|
function updateLineText(cm, lineView) { |
|
var cls = lineView.text.className; |
|
var built = getLineContent(cm, lineView); |
|
if (lineView.text == lineView.node) lineView.node = built.pre; |
|
lineView.text.parentNode.replaceChild(built.pre, lineView.text); |
|
lineView.text = built.pre; |
|
if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { |
|
lineView.bgClass = built.bgClass; |
|
lineView.textClass = built.textClass; |
|
updateLineClasses(lineView); |
|
} else if (cls) { |
|
lineView.text.className = cls; |
|
} |
|
} |
|
|
|
function updateLineClasses(lineView) { |
|
updateLineBackground(lineView); |
|
if (lineView.line.wrapClass) |
|
ensureLineWrapped(lineView).className = lineView.line.wrapClass; |
|
else if (lineView.node != lineView.text) |
|
lineView.node.className = ""; |
|
var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; |
|
lineView.text.className = textClass || ""; |
|
} |
|
|
|
function updateLineGutter(cm, lineView, lineN, dims) { |
|
if (lineView.gutter) { |
|
lineView.node.removeChild(lineView.gutter); |
|
lineView.gutter = null; |
|
} |
|
var markers = lineView.line.gutterMarkers; |
|
if (cm.options.lineNumbers || markers) { |
|
var wrap = ensureLineWrapped(lineView); |
|
var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + |
|
(cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + |
|
"px; width: " + dims.gutterTotalWidth + "px"); |
|
cm.display.input.setUneditable(gutterWrap); |
|
wrap.insertBefore(gutterWrap, lineView.text); |
|
if (lineView.line.gutterClass) |
|
gutterWrap.className += " " + lineView.line.gutterClass; |
|
if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) |
|
lineView.lineNumber = gutterWrap.appendChild( |
|
elt("div", lineNumberFor(cm.options, lineN), |
|
"CodeMirror-linenumber CodeMirror-gutter-elt", |
|
"left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " |
|
+ cm.display.lineNumInnerWidth + "px")); |
|
if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { |
|
var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; |
|
if (found) |
|
gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + |
|
dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); |
|
} |
|
} |
|
} |
|
|
|
function updateLineWidgets(cm, lineView, dims) { |
|
if (lineView.alignable) lineView.alignable = null; |
|
for (var node = lineView.node.firstChild, next; node; node = next) { |
|
var next = node.nextSibling; |
|
if (node.className == "CodeMirror-linewidget") |
|
lineView.node.removeChild(node); |
|
} |
|
insertLineWidgets(cm, lineView, dims); |
|
} |
|
|
|
// Build a line's DOM representation from scratch |
|
function buildLineElement(cm, lineView, lineN, dims) { |
|
var built = getLineContent(cm, lineView); |
|
lineView.text = lineView.node = built.pre; |
|
if (built.bgClass) lineView.bgClass = built.bgClass; |
|
if (built.textClass) lineView.textClass = built.textClass; |
|
|
|
updateLineClasses(lineView); |
|
updateLineGutter(cm, lineView, lineN, dims); |
|
insertLineWidgets(cm, lineView, dims); |
|
return lineView.node; |
|
} |
|
|
|
// A lineView may contain multiple logical lines (when merged by |
|
// collapsed spans). The widgets for all of them need to be drawn. |
|
function insertLineWidgets(cm, lineView, dims) { |
|
insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); |
|
if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) |
|
insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); |
|
} |
|
|
|
function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { |
|
if (!line.widgets) return; |
|
var wrap = ensureLineWrapped(lineView); |
|
for (var i = 0, ws = line.widgets; i < ws.length; ++i) { |
|
var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); |
|
if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); |
|
positionLineWidget(widget, node, lineView, dims); |
|
cm.display.input.setUneditable(node); |
|
if (allowAbove && widget.above) |
|
wrap.insertBefore(node, lineView.gutter || lineView.text); |
|
else |
|
wrap.appendChild(node); |
|
signalLater(widget, "redraw"); |
|
} |
|
} |
|
|
|
function positionLineWidget(widget, node, lineView, dims) { |
|
if (widget.noHScroll) { |
|
(lineView.alignable || (lineView.alignable = [])).push(node); |
|
var width = dims.wrapperWidth; |
|
node.style.left = dims.fixedPos + "px"; |
|
if (!widget.coverGutter) { |
|
width -= dims.gutterTotalWidth; |
|
node.style.paddingLeft = dims.gutterTotalWidth + "px"; |
|
} |
|
node.style.width = width + "px"; |
|
} |
|
if (widget.coverGutter) { |
|
node.style.zIndex = 5; |
|
node.style.position = "relative"; |
|
if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; |
|
} |
|
} |
|
|
|
// POSITION OBJECT |
|
|
|
// A Pos instance represents a position within the text. |
|
var Pos = CodeMirror.Pos = function(line, ch) { |
|
if (!(this instanceof Pos)) return new Pos(line, ch); |
|
this.line = line; this.ch = ch; |
|
}; |
|
|
|
// Compare two positions, return 0 if they are the same, a negative |
|
// number when a is less, and a positive number otherwise. |
|
var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; |
|
|
|
function copyPos(x) {return Pos(x.line, x.ch);} |
|
function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } |
|
function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } |
|
|
|
// INPUT HANDLING |
|
|
|
function ensureFocus(cm) { |
|
if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } |
|
} |
|
|
|
function isReadOnly(cm) { |
|
return cm.options.readOnly || cm.doc.cantEdit; |
|
} |
|
|
|
// This will be set to an array of strings when copying, so that, |
|
// when pasting, we know what kind of selections the copied text |
|
// was made out of. |
|
var lastCopied = null; |
|
|
|
function applyTextInput(cm, inserted, deleted, sel, origin) { |
|
var doc = cm.doc; |
|
cm.display.shift = false; |
|
if (!sel) sel = doc.sel; |
|
|
|
var paste = cm.state.pasteIncoming || origin == "paste"; |
|
var textLines = splitLines(inserted), multiPaste = null; |
|
// When pasing N lines into N selections, insert one line per selection |
|
if (paste && sel.ranges.length > 1) { |
|
if (lastCopied && lastCopied.join("\n") == inserted) |
|
multiPaste = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); |
|
else if (textLines.length == sel.ranges.length) |
|
multiPaste = map(textLines, function(l) { return [l]; }); |
|
} |
|
|
|
// Normal behavior is to insert the new text into every selection |
|
for (var i = sel.ranges.length - 1; i >= 0; i--) { |
|
var range = sel.ranges[i]; |
|
var from = range.from(), to = range.to(); |
|
if (range.empty()) { |
|
if (deleted && deleted > 0) // Handle deletion |
|
from = Pos(from.line, from.ch - deleted); |
|
else if (cm.state.overwrite && !paste) // Handle overwrite |
|
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); |
|
} |
|
var updateInput = cm.curOp.updateInput; |
|
var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, |
|
origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")}; |
|
makeChange(cm.doc, changeEvent); |
|
signalLater(cm, "inputRead", cm, changeEvent); |
|
} |
|
if (inserted && !paste) |
|
triggerElectric(cm, inserted); |
|
|
|
ensureCursorVisible(cm); |
|
cm.curOp.updateInput = updateInput; |
|
cm.curOp.typing = true; |
|
cm.state.pasteIncoming = cm.state.cutIncoming = false; |
|
} |
|
|
|
function handlePaste(e, cm) { |
|
var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); |
|
if (pasted) { |
|
e.preventDefault(); |
|
runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); }); |
|
return true; |
|
} |
|
} |
|
|
|
function triggerElectric(cm, inserted) { |
|
// When an 'electric' character is inserted, immediately trigger a reindent |
|
if (!cm.options.electricChars || !cm.options.smartIndent) return; |
|
var sel = cm.doc.sel; |
|
|
|
for (var i = sel.ranges.length - 1; i >= 0; i--) { |
|
var range = sel.ranges[i]; |
|
if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue; |
|
var mode = cm.getModeAt(range.head); |
|
var indented = false; |
|
if (mode.electricChars) { |
|
for (var j = 0; j < mode.electricChars.length; j++) |
|
if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { |
|
indented = indentLine(cm, range.head.line, "smart"); |
|
break; |
|
} |
|
} else if (mode.electricInput) { |
|
if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) |
|
indented = indentLine(cm, range.head.line, "smart"); |
|
} |
|
if (indented) signalLater(cm, "electricInput", cm, range.head.line); |
|
} |
|
} |
|
|
|
function copyableRanges(cm) { |
|
var text = [], ranges = []; |
|
for (var i = 0; i < cm.doc.sel.ranges.length; i++) { |
|
var line = cm.doc.sel.ranges[i].head.line; |
|
var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; |
|
ranges.push(lineRange); |
|
text.push(cm.getRange(lineRange.anchor, lineRange.head)); |
|
} |
|
return {text: text, ranges: ranges}; |
|
} |
|
|
|
function disableBrowserMagic(field) { |
|
field.setAttribute("autocorrect", "off"); |
|
field.setAttribute("autocapitalize", "off"); |
|
field.setAttribute("spellcheck", "false"); |
|
} |
|
|
|
// TEXTAREA INPUT STYLE |
|
|
|
function TextareaInput(cm) { |
|
this.cm = cm; |
|
// See input.poll and input.reset |
|
this.prevInput = ""; |
|
|
|
// Flag that indicates whether we expect input to appear real soon |
|
// now (after some event like 'keypress' or 'input') and are |
|
// polling intensively. |
|
this.pollingFast = false; |
|
// Self-resetting timeout for the poller |
|
this.polling = new Delayed(); |
|
// Tracks when input.reset has punted to just putting a short |
|
// string into the textarea instead of the full selection. |
|
this.inaccurateSelection = false; |
|
// Used to work around IE issue with selection being forgotten when focus moves away from textarea |
|
this.hasSelection = false; |
|
this.composing = null; |
|
}; |
|
|
|
function hiddenTextarea() { |
|
var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); |
|
var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); |
|
// The textarea is kept positioned near the cursor to prevent the |
|
// fact that it'll be scrolled into view on input from scrolling |
|
// our fake cursor out of view. On webkit, when wrap=off, paste is |
|
// very slow. So make the area wide instead. |
|
if (webkit) te.style.width = "1000px"; |
|
else te.setAttribute("wrap", "off"); |
|
// If border: 0; -- iOS fails to open keyboard (issue #1287) |
|
if (ios) te.style.border = "1px solid black"; |
|
disableBrowserMagic(te); |
|
return div; |
|
} |
|
|
|
TextareaInput.prototype = copyObj({ |
|
init: function(display) { |
|
var input = this, cm = this.cm; |
|
|
|
// Wraps and hides input textarea |
|
var div = this.wrapper = hiddenTextarea(); |
|
// The semihidden textarea that is focused when the editor is |
|
// focused, and receives input. |
|
var te = this.textarea = div.firstChild; |
|
display.wrapper.insertBefore(div, display.wrapper.firstChild); |
|
|
|
// Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) |
|
if (ios) te.style.width = "0px"; |
|
|
|
on(te, "input", function() { |
|
if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; |
|
input.poll(); |
|
}); |
|
|
|
on(te, "paste", function(e) { |
|
if (handlePaste(e, cm)) return true; |
|
|
|
cm.state.pasteIncoming = true; |
|
input.fastPoll(); |
|
}); |
|
|
|
function prepareCopyCut(e) { |
|
if (cm.somethingSelected()) { |
|
lastCopied = cm.getSelections(); |
|
if (input.inaccurateSelection) { |
|
input.prevInput = ""; |
|
input.inaccurateSelection = false; |
|
te.value = lastCopied.join("\n"); |
|
selectInput(te); |
|
} |
|
} else if (!cm.options.lineWiseCopyCut) { |
|
return; |
|
} else { |
|
var ranges = copyableRanges(cm); |
|
lastCopied = ranges.text; |
|
if (e.type == "cut") { |
|
cm.setSelections(ranges.ranges, null, sel_dontScroll); |
|
} else { |
|
input.prevInput = ""; |
|
te.value = ranges.text.join("\n"); |
|
selectInput(te); |
|
} |
|
} |
|
if (e.type == "cut") cm.state.cutIncoming = true; |
|
} |
|
on(te, "cut", prepareCopyCut); |
|
on(te, "copy", prepareCopyCut); |
|
|
|
on(display.scroller, "paste", function(e) { |
|
if (eventInWidget(display, e)) return; |
|
cm.state.pasteIncoming = true; |
|
input.focus(); |
|
}); |
|
|
|
// Prevent normal selection in the editor (we handle our own) |
|
on(display.lineSpace, "selectstart", function(e) { |
|
if (!eventInWidget(display, e)) e_preventDefault(e); |
|
}); |
|
|
|
on(te, "compositionstart", function() { |
|
var start = cm.getCursor("from"); |
|
input.composing = { |
|
start: start, |
|
range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) |
|
}; |
|
}); |
|
on(te, "compositionend", function() { |
|
if (input.composing) { |
|
input.poll(); |
|
input.composing.range.clear(); |
|
input.composing = null; |
|
} |
|
}); |
|
}, |
|
|
|
prepareSelection: function() { |
|
// Redraw the selection and/or cursor |
|
var cm = this.cm, display = cm.display, doc = cm.doc; |
|
var result = prepareSelection(cm); |
|
|
|
// Move the hidden textarea near the cursor to prevent scrolling artifacts |
|
if (cm.options.moveInputWithCursor) { |
|
var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); |
|
var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); |
|
result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, |
|
headPos.top + lineOff.top - wrapOff.top)); |
|
result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, |
|
headPos.left + lineOff.left - wrapOff.left)); |
|
} |
|
|
|
return result; |
|
}, |
|
|
|
showSelection: function(drawn) { |
|
var cm = this.cm, display = cm.display; |
|
removeChildrenAndAdd(display.cursorDiv, drawn.cursors); |
|
removeChildrenAndAdd(display.selectionDiv, drawn.selection); |
|
if (drawn.teTop != null) { |
|
this.wrapper.style.top = drawn.teTop + "px"; |
|
this.wrapper.style.left = drawn.teLeft + "px"; |
|
} |
|
}, |
|
|
|
// Reset the input to correspond to the selection (or to be empty, |
|
// when not typing and nothing is selected) |
|
reset: function(typing) { |
|
if (this.contextMenuPending) return; |
|
var minimal, selected, cm = this.cm, doc = cm.doc; |
|
if (cm.somethingSelected()) { |
|
this.prevInput = ""; |
|
var range = doc.sel.primary(); |
|
minimal = hasCopyEvent && |
|
(range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); |
|
var content = minimal ? "-" : selected || cm.getSelection(); |
|
this.textarea.value = content; |
|
if (cm.state.focused) selectInput(this.textarea); |
|
if (ie && ie_version >= 9) this.hasSelection = content; |
|
} else if (!typing) { |
|
this.prevInput = this.textarea.value = ""; |
|
if (ie && ie_version >= 9) this.hasSelection = null; |
|
} |
|
this.inaccurateSelection = minimal; |
|
}, |
|
|
|
getField: function() { return this.textarea; }, |
|
|
|
supportsTouch: function() { return false; }, |
|
|
|
focus: function() { |
|
if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { |
|
try { this.textarea.focus(); } |
|
catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM |
|
} |
|
}, |
|
|
|
blur: function() { this.textarea.blur(); }, |
|
|
|
resetPosition: function() { |
|
this.wrapper.style.top = this.wrapper.style.left = 0; |
|
}, |
|
|
|
receivedFocus: function() { this.slowPoll(); }, |
|
|
|
// Poll for input changes, using the normal rate of polling. This |
|
// runs as long as the editor is focused. |
|
slowPoll: function() { |
|
var input = this; |
|
if (input.pollingFast) return; |
|
input.polling.set(this.cm.options.pollInterval, function() { |
|
input.poll(); |
|
if (input.cm.state.focused) input.slowPoll(); |
|
}); |
|
}, |
|
|
|
// When an event has just come in that is likely to add or change |
|
// something in the input textarea, we poll faster, to ensure that |
|
// the change appears on the screen quickly. |
|
fastPoll: function() { |
|
var missed = false, input = this; |
|
input.pollingFast = true; |
|
function p() { |
|
var changed = input.poll(); |
|
if (!changed && !missed) {missed = true; input.polling.set(60, p);} |
|
else {input.pollingFast = false; input.slowPoll();} |
|
} |
|
input.polling.set(20, p); |
|
}, |
|
|
|
// Read input from the textarea, and update the document to match. |
|
// When something is selected, it is present in the textarea, and |
|
// selected (unless it is huge, in which case a placeholder is |
|
// used). When nothing is selected, the cursor sits after previously |
|
// seen text (can be empty), which is stored in prevInput (we must |
|
// not reset the textarea when typing, because that breaks IME). |
|
poll: function() { |
|
var cm = this.cm, input = this.textarea, prevInput = this.prevInput; |
|
// Since this is called a *lot*, try to bail out as cheaply as |
|
// possible when it is clear that nothing happened. hasSelection |
|
// will be the case when there is a lot of text in the textarea, |
|
// in which case reading its value would be expensive. |
|
if (this.contextMenuPending || !cm.state.focused || |
|
(hasSelection(input) && !prevInput) || |
|
isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) |
|
return false; |
|
|
|
var text = input.value; |
|
// If nothing changed, bail. |
|
if (text == prevInput && !cm.somethingSelected()) return false; |
|
// Work around nonsensical selection resetting in IE9/10, and |
|
// inexplicable appearance of private area unicode characters on |
|
// some key combos in Mac (#2689). |
|
if (ie && ie_version >= 9 && this.hasSelection === text || |
|
mac && /[\uf700-\uf7ff]/.test(text)) { |
|
cm.display.input.reset(); |
|
return false; |
|
} |
|
|
|
if (cm.doc.sel == cm.display.selForContextMenu) { |
|
var first = text.charCodeAt(0); |
|
if (first == 0x200b && !prevInput) prevInput = "\u200b"; |
|
if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); } |
|
} |
|
// Find the part of the input that is actually new |
|
var same = 0, l = Math.min(prevInput.length, text.length); |
|
while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; |
|
|
|
var self = this; |
|
runInOp(cm, function() { |
|
applyTextInput(cm, text.slice(same), prevInput.length - same, |
|
null, self.composing ? "*compose" : null); |
|
|
|
// Don't leave long text in the textarea, since it makes further polling slow |
|
if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = ""; |
|
else self.prevInput = text; |
|
|
|
if (self.composing) { |
|
self.composing.range.clear(); |
|
self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"), |
|
{className: "CodeMirror-composing"}); |
|
} |
|
}); |
|
return true; |
|
}, |
|
|
|
ensurePolled: function() { |
|
if (this.pollingFast && this.poll()) this.pollingFast = false; |
|
}, |
|
|
|
onKeyPress: function() { |
|
if (ie && ie_version >= 9) this.hasSelection = null; |
|
this.fastPoll(); |
|
}, |
|
|
|
onContextMenu: function(e) { |
|
var input = this, cm = input.cm, display = cm.display, te = input.textarea; |
|
var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; |
|
if (!pos || presto) return; // Opera is difficult. |
|
|
|
// Reset the current text selection only if the click is done outside of the selection |
|
// and 'resetSelectionOnContextMenu' option is true. |
|
var reset = cm.options.resetSelectionOnContextMenu; |
|
if (reset && cm.doc.sel.contains(pos) == -1) |
|
operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); |
|
|
|
var oldCSS = te.style.cssText; |
|
input.wrapper.style.position = "absolute"; |
|
te.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + |
|
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + |
|
(ie ? "rgba(255, 255, 255, .05)" : "transparent") + |
|
"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; |
|
if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) |
|
display.input.focus(); |
|
if (webkit) window.scrollTo(null, oldScrollY); |
|
display.input.reset(); |
|
// Adds "Select all" to context menu in FF |
|
if (!cm.somethingSelected()) te.value = input.prevInput = " "; |
|
input.contextMenuPending = true; |
|
display.selForContextMenu = cm.doc.sel; |
|
clearTimeout(display.detectingSelectAll); |
|
|
|
// Select-all will be greyed out if there's nothing to select, so |
|
// this adds a zero-width space so that we can later check whether |
|
// it got selected. |
|
function prepareSelectAllHack() { |
|
if (te.selectionStart != null) { |
|
var selected = cm.somethingSelected(); |
|
var extval = "\u200b" + (selected ? te.value : ""); |
|
te.value = "\u21da"; // Used to catch context-menu undo |
|
te.value = extval; |
|
input.prevInput = selected ? "" : "\u200b"; |
|
te.selectionStart = 1; te.selectionEnd = extval.length; |
|
// Re-set this, in case some other handler touched the |
|
// selection in the meantime. |
|
display.selForContextMenu = cm.doc.sel; |
|
} |
|
} |
|
function rehide() { |
|
input.contextMenuPending = false; |
|
input.wrapper.style.position = "relative"; |
|
te.style.cssText = oldCSS; |
|
if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); |
|
|
|
// Try to detect the user choosing select-all |
|
if (te.selectionStart != null) { |
|
if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); |
|
var i = 0, poll = function() { |
|
if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && |
|
te.selectionEnd > 0 && input.prevInput == "\u200b") |
|
operation(cm, commands.selectAll)(cm); |
|
else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); |
|
else display.input.reset(); |
|
}; |
|
display.detectingSelectAll = setTimeout(poll, 200); |
|
} |
|
} |
|
|
|
if (ie && ie_version >= 9) prepareSelectAllHack(); |
|
if (captureRightClick) { |
|
e_stop(e); |
|
var mouseup = function() { |
|
off(window, "mouseup", mouseup); |
|
setTimeout(rehide, 20); |
|
}; |
|
on(window, "mouseup", mouseup); |
|
} else { |
|
setTimeout(rehide, 50); |
|
} |
|
}, |
|
|
|
setUneditable: nothing, |
|
|
|
needsContentAttribute: false |
|
}, TextareaInput.prototype); |
|
|
|
// CONTENTEDITABLE INPUT STYLE |
|
|
|
function ContentEditableInput(cm) { |
|
this.cm = cm; |
|
this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; |
|
this.polling = new Delayed(); |
|
this.gracePeriod = false; |
|
} |
|
|
|
ContentEditableInput.prototype = copyObj({ |
|
init: function(display) { |
|
var input = this, cm = input.cm; |
|
var div = input.div = display.lineDiv; |
|
div.contentEditable = "true"; |
|
disableBrowserMagic(div); |
|
|
|
on(div, "paste", function(e) { handlePaste(e, cm); }) |
|
|
|
on(div, "compositionstart", function(e) { |
|
var data = e.data; |
|
input.composing = {sel: cm.doc.sel, data: data, startData: data}; |
|
if (!data) return; |
|
var prim = cm.doc.sel.primary(); |
|
var line = cm.getLine(prim.head.line); |
|
var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length)); |
|
if (found > -1 && found <= prim.head.ch) |
|
input.composing.sel = simpleSelection(Pos(prim.head.line, found), |
|
Pos(prim.head.line, found + data.length)); |
|
}); |
|
on(div, "compositionupdate", function(e) { |
|
input.composing.data = e.data; |
|
}); |
|
on(div, "compositionend", function(e) { |
|
var ours = input.composing; |
|
if (!ours) return; |
|
if (e.data != ours.startData && !/\u200b/.test(e.data)) |
|
ours.data = e.data; |
|
// Need a small delay to prevent other code (input event, |
|
// selection polling) from doing damage when fired right after |
|
// compositionend. |
|
setTimeout(function() { |
|
if (!ours.handled) |
|
input.applyComposition(ours); |
|
if (input.composing == ours) |
|
input.composing = null; |
|
}, 50); |
|
}); |
|
|
|
on(div, "touchstart", function() { |
|
input.forceCompositionEnd(); |
|
}); |
|
|
|
on(div, "input", function() { |
|
if (input.composing) return; |
|
if (!input.pollContent()) |
|
runInOp(input.cm, function() {regChange(cm);}); |
|
}); |
|
|
|
function onCopyCut(e) { |
|
if (cm.somethingSelected()) { |
|
lastCopied = cm.getSelections(); |
|
if (e.type == "cut") cm.replaceSelection("", null, "cut"); |
|
} else if (!cm.options.lineWiseCopyCut) { |
|
return; |
|
} else { |
|
var ranges = copyableRanges(cm); |
|
lastCopied = ranges.text; |
|
if (e.type == "cut") { |
|
cm.operation(function() { |
|
cm.setSelections(ranges.ranges, 0, sel_dontScroll); |
|
cm.replaceSelection("", null, "cut"); |
|
}); |
|
} |
|
} |
|
// iOS exposes the clipboard API, but seems to discard content inserted into it |
|
if (e.clipboardData && !ios) { |
|
e.preventDefault(); |
|
e.clipboardData.clearData(); |
|
e.clipboardData.setData("text/plain", lastCopied.join("\n")); |
|
} else { |
|
// Old-fashioned briefly-focus-a-textarea hack |
|
var kludge = hiddenTextarea(), te = kludge.firstChild; |
|
cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); |
|
te.value = lastCopied.join("\n"); |
|
var hadFocus = document.activeElement; |
|
selectInput(te); |
|
setTimeout(function() { |
|
cm.display.lineSpace.removeChild(kludge); |
|
hadFocus.focus(); |
|
}, 50); |
|
} |
|
} |
|
on(div, "copy", onCopyCut); |
|
on(div, "cut", onCopyCut); |
|
}, |
|
|
|
prepareSelection: function() { |
|
var result = prepareSelection(this.cm, false); |
|
result.focus = this.cm.state.focused; |
|
return result; |
|
}, |
|
|
|
showSelection: function(info) { |
|
if (!info || !this.cm.display.view.length) return; |
|
if (info.focus) this.showPrimarySelection(); |
|
this.showMultipleSelections(info); |
|
}, |
|
|
|
showPrimarySelection: function() { |
|
var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); |
|
var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); |
|
var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); |
|
if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && |
|
cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && |
|
cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) |
|
return; |
|
|
|
var start = posToDOM(this.cm, prim.from()); |
|
var end = posToDOM(this.cm, prim.to()); |
|
if (!start && !end) return; |
|
|
|
var view = this.cm.display.view; |
|
var old = sel.rangeCount && sel.getRangeAt(0); |
|
if (!start) { |
|
start = {node: view[0].measure.map[2], offset: 0}; |
|
} else if (!end) { // FIXME dangerously hacky |
|
var measure = view[view.length - 1].measure; |
|
var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; |
|
end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; |
|
} |
|
|
|
try { var rng = range(start.node, start.offset, end.offset, end.node); } |
|
catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible |
|
if (rng) { |
|
sel.removeAllRanges(); |
|
sel.addRange(rng); |
|
if (old && sel.anchorNode == null) sel.addRange(old); |
|
else if (gecko) this.startGracePeriod(); |
|
} |
|
this.rememberSelection(); |
|
}, |
|
|
|
startGracePeriod: function() { |
|
var input = this; |
|
clearTimeout(this.gracePeriod); |
|
this.gracePeriod = setTimeout(function() { |
|
input.gracePeriod = false; |
|
if (input.selectionChanged()) |
|
input.cm.operation(function() { input.cm.curOp.selectionChanged = true; }); |
|
}, 20); |
|
}, |
|
|
|
showMultipleSelections: function(info) { |
|
removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); |
|
removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); |
|
}, |
|
|
|
rememberSelection: function() { |
|
var sel = window.getSelection(); |
|
this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; |
|
this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; |
|
}, |
|
|
|
selectionInEditor: function() { |
|
var sel = window.getSelection(); |
|
if (!sel.rangeCount) return false; |
|
var node = sel.getRangeAt(0).commonAncestorContainer; |
|
return contains(this.div, node); |
|
}, |
|
|
|
focus: function() { |
|
if (this.cm.options.readOnly != "nocursor") this.div.focus(); |
|
}, |
|
blur: function() { this.div.blur(); }, |
|
getField: function() { return this.div; }, |
|
|
|
supportsTouch: function() { return true; }, |
|
|
|
receivedFocus: function() { |
|
var input = this; |
|
if (this.selectionInEditor()) |
|
this.pollSelection(); |
|
else |
|
runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); |
|
|
|
function poll() { |
|
if (input.cm.state.focused) { |
|
input.pollSelection(); |
|
input.polling.set(input.cm.options.pollInterval, poll); |
|
} |
|
} |
|
this.polling.set(this.cm.options.pollInterval, poll); |
|
}, |
|
|
|
selectionChanged: function() { |
|
var sel = window.getSelection(); |
|
return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || |
|
sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset; |
|
}, |
|
|
|
pollSelection: function() { |
|
if (!this.composing && !this.gracePeriod && this.selectionChanged()) { |
|
var sel = window.getSelection(), cm = this.cm; |
|
this.rememberSelection(); |
|
var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); |
|
var head = domToPos(cm, sel.focusNode, sel.focusOffset); |
|
if (anchor && head) runInOp(cm, function() { |
|
setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); |
|
if (anchor.bad || head.bad) cm.curOp.selectionChanged = true; |
|
}); |
|
} |
|
}, |
|
|
|
pollContent: function() { |
|
var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); |
|
var from = sel.from(), to = sel.to(); |
|
if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false; |
|
|
|
var fromIndex; |
|
if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { |
|
var fromLine = lineNo(display.view[0].line); |
|
var fromNode = display.view[0].node; |
|
} else { |
|
var fromLine = lineNo(display.view[fromIndex].line); |
|
var fromNode = display.view[fromIndex - 1].node.nextSibling; |
|
} |
|
var toIndex = findViewIndex(cm, to.line); |
|
if (toIndex == display.view.length - 1) { |
|
var toLine = display.viewTo - 1; |
|
var toNode = display.lineDiv.lastChild; |
|
} else { |
|
var toLine = lineNo(display.view[toIndex + 1].line) - 1; |
|
var toNode = display.view[toIndex + 1].node.previousSibling; |
|
} |
|
|
|
var newText = splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); |
|
var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); |
|
while (newText.length > 1 && oldText.length > 1) { |
|
if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } |
|
else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } |
|
else break; |
|
} |
|
|
|
var cutFront = 0, cutEnd = 0; |
|
var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); |
|
while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) |
|
++cutFront; |
|
var newBot = lst(newText), oldBot = lst(oldText); |
|
var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), |
|
oldBot.length - (oldText.length == 1 ? cutFront : 0)); |
|
while (cutEnd < maxCutEnd && |
|
newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) |
|
++cutEnd; |
|
|
|
newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd); |
|
newText[0] = newText[0].slice(cutFront); |
|
|
|
var chFrom = Pos(fromLine, cutFront); |
|
var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); |
|
if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { |
|
replaceRange(cm.doc, newText, chFrom, chTo, "+input"); |
|
return true; |
|
} |
|
}, |
|
|
|
ensurePolled: function() { |
|
this.forceCompositionEnd(); |
|
}, |
|
reset: function() { |
|
this.forceCompositionEnd(); |
|
}, |
|
forceCompositionEnd: function() { |
|
if (!this.composing || this.composing.handled) return; |
|
this.applyComposition(this.composing); |
|
this.composing.handled = true; |
|
this.div.blur(); |
|
this.div.focus(); |
|
}, |
|
applyComposition: function(composing) { |
|
if (composing.data && composing.data != composing.startData) |
|
operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); |
|
}, |
|
|
|
setUneditable: function(node) { |
|
node.setAttribute("contenteditable", "false"); |
|
}, |
|
|
|
onKeyPress: function(e) { |
|
e.preventDefault(); |
|
operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); |
|
}, |
|
|
|
onContextMenu: nothing, |
|
resetPosition: nothing, |
|
|
|
needsContentAttribute: true |
|
}, ContentEditableInput.prototype); |
|
|
|
function posToDOM(cm, pos) { |
|
var view = findViewForLine(cm, pos.line); |
|
if (!view || view.hidden) return null; |
|
var line = getLine(cm.doc, pos.line); |
|
var info = mapFromLineView(view, line, pos.line); |
|
|
|
var order = getOrder(line), side = "left"; |
|
if (order) { |
|
var partPos = getBidiPartAt(order, pos.ch); |
|
side = partPos % 2 ? "right" : "left"; |
|
} |
|
var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); |
|
result.offset = result.collapse == "right" ? result.end : result.start; |
|
return result; |
|
} |
|
|
|
function badPos(pos, bad) { if (bad) pos.bad = true; return pos; } |
|
|
|
function domToPos(cm, node, offset) { |
|
var lineNode; |
|
if (node == cm.display.lineDiv) { |
|
lineNode = cm.display.lineDiv.childNodes[offset]; |
|
if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true); |
|
node = null; offset = 0; |
|
} else { |
|
for (lineNode = node;; lineNode = lineNode.parentNode) { |
|
if (!lineNode || lineNode == cm.display.lineDiv) return null; |
|
if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; |
|
} |
|
} |
|
for (var i = 0; i < cm.display.view.length; i++) { |
|
var lineView = cm.display.view[i]; |
|
if (lineView.node == lineNode) |
|
return locateNodeInLineView(lineView, node, offset); |
|
} |
|
} |
|
|
|
function locateNodeInLineView(lineView, node, offset) { |
|
var wrapper = lineView.text.firstChild, bad = false; |
|
if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true); |
|
if (node == wrapper) { |
|
bad = true; |
|
node = wrapper.childNodes[offset]; |
|
offset = 0; |
|
if (!node) { |
|
var line = lineView.rest ? lst(lineView.rest) : lineView.line; |
|
return badPos(Pos(lineNo(line), line.text.length), bad); |
|
} |
|
} |
|
|
|
var textNode = node.nodeType == 3 ? node : null, topNode = node; |
|
if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { |
|
textNode = node.firstChild; |
|
if (offset) offset = textNode.nodeValue.length; |
|
} |
|
while (topNode.parentNode != wrapper) topNode = topNode.parentNode; |
|
var measure = lineView.measure, maps = measure.maps; |
|
|
|
function find(textNode, topNode, offset) { |
|
for (var i = -1; i < (maps ? maps.length : 0); i++) { |
|
var map = i < 0 ? measure.map : maps[i]; |
|
for (var j = 0; j < map.length; j += 3) { |
|
var curNode = map[j + 2]; |
|
if (curNode == textNode || curNode == topNode) { |
|
var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); |
|
var ch = map[j] + offset; |
|
if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]; |
|
return Pos(line, ch); |
|
} |
|
} |
|
} |
|
} |
|
var found = find(textNode, topNode, offset); |
|
if (found) return badPos(found, bad); |
|
|
|
// FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems |
|
for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { |
|
found = find(after, after.firstChild, 0); |
|
if (found) |
|
return badPos(Pos(found.line, found.ch - dist), bad); |
|
else |
|
dist += after.textContent.length; |
|
} |
|
for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { |
|
found = find(before, before.firstChild, -1); |
|
if (found) |
|
return badPos(Pos(found.line, found.ch + dist), bad); |
|
else |
|
dist += after.textContent.length; |
|
} |
|
} |
|
|
|
function domTextBetween(cm, from, to, fromLine, toLine) { |
|
var text = "", closing = false; |
|
function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } |
|
function walk(node) { |
|
if (node.nodeType == 1) { |
|
var cmText = node.getAttribute("cm-text"); |
|
if (cmText != null) { |
|
if (cmText == "") cmText = node.textContent.replace(/\u200b/g, ""); |
|
text += cmText; |
|
return; |
|
} |
|
var markerID = node.getAttribute("cm-marker"), range; |
|
if (markerID) { |
|
var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); |
|
if (found.length && (range = found[0].find())) |
|
text += getBetween(cm.doc, range.from, range.to).join("\n"); |
|
return; |
|
} |
|
if (node.getAttribute("contenteditable") == "false") return; |
|
for (var i = 0; i < node.childNodes.length; i++) |
|
walk(node.childNodes[i]); |
|
if (/^(pre|div|p)$/i.test(node.nodeName)) |
|
closing = true; |
|
} else if (node.nodeType == 3) { |
|
var val = node.nodeValue; |
|
if (!val) return; |
|
if (closing) { |
|
text += "\n"; |
|
closing = false; |
|
} |
|
text += val; |
|
} |
|
} |
|
for (;;) { |
|
walk(from); |
|
if (from == to) break; |
|
from = from.nextSibling; |
|
} |
|
return text; |
|
} |
|
|
|
CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; |
|
|
|
// SELECTION / CURSOR |
|
|
|
// Selection objects are immutable. A new one is created every time |
|
// the selection changes. A selection is one or more non-overlapping |
|
// (and non-touching) ranges, sorted, and an integer that indicates |
|
// which one is the primary selection (the one that's scrolled into |
|
// view, that getCursor returns, etc). |
|
function Selection(ranges, primIndex) { |
|
this.ranges = ranges; |
|
this.primIndex = primIndex; |
|
} |
|
|
|
Selection.prototype = { |
|
primary: function() { return this.ranges[this.primIndex]; }, |
|
equals: function(other) { |
|
if (other == this) return true; |
|
if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; |
|
for (var i = 0; i < this.ranges.length; i++) { |
|
var here = this.ranges[i], there = other.ranges[i]; |
|
if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; |
|
} |
|
return true; |
|
}, |
|
deepCopy: function() { |
|
for (var out = [], i = 0; i < this.ranges.length; i++) |
|
out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); |
|
return new Selection(out, this.primIndex); |
|
}, |
|
somethingSelected: function() { |
|
for (var i = 0; i < this.ranges.length; i++) |
|
if (!this.ranges[i].empty()) return true; |
|
return false; |
|
}, |
|
contains: function(pos, end) { |
|
if (!end) end = pos; |
|
for (var i = 0; i < this.ranges.length; i++) { |
|
var range = this.ranges[i]; |
|
if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) |
|
return i; |
|
} |
|
return -1; |
|
} |
|
}; |
|
|
|
function Range(anchor, head) { |
|
this.anchor = anchor; this.head = head; |
|
} |
|
|
|
Range.prototype = { |
|
from: function() { return minPos(this.anchor, this.head); }, |
|
to: function() { return maxPos(this.anchor, this.head); }, |
|
empty: function() { |
|
return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; |
|
} |
|
}; |
|
|
|
// Take an unsorted, potentially overlapping set of ranges, and |
|
// build a selection out of it. 'Consumes' ranges array (modifying |
|
// it). |
|
function normalizeSelection(ranges, primIndex) { |
|
var prim = ranges[primIndex]; |
|
ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); |
|
primIndex = indexOf(ranges, prim); |
|
for (var i = 1; i < ranges.length; i++) { |
|
var cur = ranges[i], prev = ranges[i - 1]; |
|
if (cmp(prev.to(), cur.from()) >= 0) { |
|
var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); |
|
var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; |
|
if (i <= primIndex) --primIndex; |
|
ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); |
|
} |
|
} |
|
return new Selection(ranges, primIndex); |
|
} |
|
|
|
function simpleSelection(anchor, head) { |
|
return new Selection([new Range(anchor, head || anchor)], 0); |
|
} |
|
|
|
// Most of the external API clips given positions to make sure they |
|
// actually exist within the document. |
|
function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} |
|
function clipPos(doc, pos) { |
|
if (pos.line < doc.first) return Pos(doc.first, 0); |
|
var last = doc.first + doc.size - 1; |
|
if (pos.line > last) return Pos(last, getLine(doc, last).text.length); |
|
return clipToLen(pos, getLine(doc, pos.line).text.length); |
|
} |
|
function clipToLen(pos, linelen) { |
|
var ch = pos.ch; |
|
if (ch == null || ch > linelen) return Pos(pos.line, linelen); |
|
else if (ch < 0) return Pos(pos.line, 0); |
|
else return pos; |
|
} |
|
function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} |
|
function clipPosArray(doc, array) { |
|
for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); |
|
return out; |
|
} |
|
|
|
// SELECTION UPDATES |
|
|
|
// The 'scroll' parameter given to many of these indicated whether |
|
// the new cursor position should be scrolled into view after |
|
// modifying the selection. |
|
|
|
// If shift is held or the extend flag is set, extends a range to |
|
// include a given position (and optionally a second position). |
|
// Otherwise, simply returns the range between the given positions. |
|
// Used for cursor motion and such. |
|
function extendRange(doc, range, head, other) { |
|
if (doc.cm && doc.cm.display.shift || doc.extend) { |
|
var anchor = range.anchor; |
|
if (other) { |
|
var posBefore = cmp(head, anchor) < 0; |
|
if (posBefore != (cmp(other, anchor) < 0)) { |
|
anchor = head; |
|
head = other; |
|
} else if (posBefore != (cmp(head, other) < 0)) { |
|
head = other; |
|
} |
|
} |
|
return new Range(anchor, head); |
|
} else { |
|
return new Range(other || head, head); |
|
} |
|
} |
|
|
|
// Extend the primary selection range, discard the rest. |
|
function extendSelection(doc, head, other, options) { |
|
setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); |
|
} |
|
|
|
// Extend all selections (pos is an array of selections with length |
|
// equal the number of selections) |
|
function extendSelections(doc, heads, options) { |
|
for (var out = [], i = 0; i < doc.sel.ranges.length; i++) |
|
out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); |
|
var newSel = normalizeSelection(out, doc.sel.primIndex); |
|
setSelection(doc, newSel, options); |
|
} |
|
|
|
// Updates a single range in the selection. |
|
function replaceOneSelection(doc, i, range, options) { |
|
var ranges = doc.sel.ranges.slice(0); |
|
ranges[i] = range; |
|
setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); |
|
} |
|
|
|
// Reset the selection to a single range. |
|
function setSimpleSelection(doc, anchor, head, options) { |
|
setSelection(doc, simpleSelection(anchor, head), options); |
|
} |
|
|
|
// Give beforeSelectionChange handlers a change to influence a |
|
// selection update. |
|
function filterSelectionChange(doc, sel) { |
|
var obj = { |
|
ranges: sel.ranges, |
|
update: function(ranges) { |
|
this.ranges = []; |
|
for (var i = 0; i < ranges.length; i++) |
|
this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), |
|
clipPos(doc, ranges[i].head)); |
|
} |
|
}; |
|
signal(doc, "beforeSelectionChange", doc, obj); |
|
if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); |
|
if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); |
|
else return sel; |
|
} |
|
|
|
function setSelectionReplaceHistory(doc, sel, options) { |
|
var done = doc.history.done, last = lst(done); |
|
if (last && last.ranges) { |
|
done[done.length - 1] = sel; |
|
setSelectionNoUndo(doc, sel, options); |
|
} else { |
|
setSelection(doc, sel, options); |
|
} |
|
} |
|
|
|
// Set a new selection. |
|
function setSelection(doc, sel, options) { |
|
setSelectionNoUndo(doc, sel, options); |
|
addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); |
|
} |
|
|
|
function setSelectionNoUndo(doc, sel, options) { |
|
if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) |
|
sel = filterSelectionChange(doc, sel); |
|
|
|
var bias = options && options.bias || |
|
(cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); |
|
setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); |
|
|
|
if (!(options && options.scroll === false) && doc.cm) |
|
ensureCursorVisible(doc.cm); |
|
} |
|
|
|
function setSelectionInner(doc, sel) { |
|
if (sel.equals(doc.sel)) return; |
|
|
|
doc.sel = sel; |
|
|
|
if (doc.cm) { |
|
doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; |
|
signalCursorActivity(doc.cm); |
|
} |
|
signalLater(doc, "cursorActivity", doc); |
|
} |
|
|
|
// Verify that the selection does not partially select any atomic |
|
// marked ranges. |
|
function reCheckSelection(doc) { |
|
setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); |
|
} |
|
|
|
// Return a selection that does not partially select any atomic |
|
// ranges. |
|
function skipAtomicInSelection(doc, sel, bias, mayClear) { |
|
var out; |
|
for (var i = 0; i < sel.ranges.length; i++) { |
|
var range = sel.ranges[i]; |
|
var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); |
|
var newHead = skipAtomic(doc, range.head, bias, mayClear); |
|
if (out || newAnchor != range.anchor || newHead != range.head) { |
|
if (!out) out = sel.ranges.slice(0, i); |
|
out[i] = new Range(newAnchor, newHead); |
|
} |
|
} |
|
return out ? normalizeSelection(out, sel.primIndex) : sel; |
|
} |
|
|
|
// Ensure a given position is not inside an atomic range. |
|
function skipAtomic(doc, pos, bias, mayClear) { |
|
var flipped = false, curPos = pos; |
|
var dir = bias || 1; |
|
doc.cantEdit = false; |
|
search: for (;;) { |
|
var line = getLine(doc, curPos.line); |
|
if (line.markedSpans) { |
|
for (var i = 0; i < line.markedSpans.length; ++i) { |
|
var sp = line.markedSpans[i], m = sp.marker; |
|
if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && |
|
(sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { |
|
if (mayClear) { |
|
signal(m, "beforeCursorEnter"); |
|
if (m.explicitlyCleared) { |
|
if (!line.markedSpans) break; |
|
else {--i; continue;} |
|
} |
|
} |
|
if (!m.atomic) continue; |
|
var newPos = m.find(dir < 0 ? -1 : 1); |
|
if (cmp(newPos, curPos) == 0) { |
|
newPos.ch += dir; |
|
if (newPos.ch < 0) { |
|
if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); |
|
else newPos = null; |
|
} else if (newPos.ch > line.text.length) { |
|
if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); |
|
else newPos = null; |
|
} |
|
if (!newPos) { |
|
if (flipped) { |
|
// Driven in a corner -- no valid cursor position found at all |
|
// -- try again *with* clearing, if we didn't already |
|
if (!mayClear) return skipAtomic(doc, pos, bias, true); |
|
// Otherwise, turn off editing until further notice, and return the start of the doc |
|
doc.cantEdit = true; |
|
return Pos(doc.first, 0); |
|
} |
|
flipped = true; newPos = pos; dir = -dir; |
|
} |
|
} |
|
curPos = newPos; |
|
continue search; |
|
} |
|
} |
|
} |
|
return curPos; |
|
} |
|
} |
|
|
|
// SELECTION DRAWING |
|
|
|
function updateSelection(cm) { |
|
cm.display.input.showSelection(cm.display.input.prepareSelection()); |
|
} |
|
|
|
function prepareSelection(cm, primary) { |
|
var doc = cm.doc, result = {}; |
|
var curFragment = result.cursors = document.createDocumentFragment(); |
|
var selFragment = result.selection = document.createDocumentFragment(); |
|
|
|
for (var i = 0; i < doc.sel.ranges.length; i++) { |
|
if (primary === false && i == doc.sel.primIndex) continue; |
|
var range = doc.sel.ranges[i]; |
|
var collapsed = range.empty(); |
|
if (collapsed || cm.options.showCursorWhenSelecting) |
|
drawSelectionCursor(cm, range, curFragment); |
|
if (!collapsed) |
|
drawSelectionRange(cm, range, selFragment); |
|
} |
|
return result; |
|
} |
|
|
|
// Draws a cursor for the given range |
|
function drawSelectionCursor(cm, range, output) { |
|
var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); |
|
|
|
var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); |
|
cursor.style.left = pos.left + "px"; |
|
cursor.style.top = pos.top + "px"; |
|
cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; |
|
|
|
if (pos.other) { |
|
// Secondary cursor, shown when on a 'jump' in bi-directional text |
|
var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); |
|
otherCursor.style.display = ""; |
|
otherCursor.style.left = pos.other.left + "px"; |
|
otherCursor.style.top = pos.other.top + "px"; |
|
otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; |
|
} |
|
} |
|
|
|
// Draws the given range as a highlighted selection |
|
function drawSelectionRange(cm, range, output) { |
|
var display = cm.display, doc = cm.doc; |
|
var fragment = document.createDocumentFragment(); |
|
var padding = paddingH(cm.display), leftSide = padding.left; |
|
var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; |
|
|
|
function add(left, top, width, bottom) { |
|
if (top < 0) top = 0; |
|
top = Math.round(top); |
|
bottom = Math.round(bottom); |
|
fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + |
|
"px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + |
|
"px; height: " + (bottom - top) + "px")); |
|
} |
|
|
|
function drawForLine(line, fromArg, toArg) { |
|
var lineObj = getLine(doc, line); |
|
var lineLen = lineObj.text.length; |
|
var start, end; |
|
function coords(ch, bias) { |
|
return charCoords(cm, Pos(line, ch), "div", lineObj, bias); |
|
} |
|
|
|
iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { |
|
var leftPos = coords(from, "left"), rightPos, left, right; |
|
if (from == to) { |
|
rightPos = leftPos; |
|
left = right = leftPos.left; |
|
} else { |
|
rightPos = coords(to - 1, "right"); |
|
if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } |
|
left = leftPos.left; |
|
right = rightPos.right; |
|
} |
|
if (fromArg == null && from == 0) left = leftSide; |
|
if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part |
|
add(left, leftPos.top, null, leftPos.bottom); |
|
left = leftSide; |
|
if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); |
|
} |
|
if (toArg == null && to == lineLen) right = rightSide; |
|
if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) |
|
start = leftPos; |
|
if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) |
|
end = rightPos; |
|
if (left < leftSide + 1) left = leftSide; |
|
add(left, rightPos.top, right - left, rightPos.bottom); |
|
}); |
|
return {start: start, end: end}; |
|
} |
|
|
|
var sFrom = range.from(), sTo = range.to(); |
|
if (sFrom.line == sTo.line) { |
|
drawForLine(sFrom.line, sFrom.ch, sTo.ch); |
|
} else { |
|
var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); |
|
var singleVLine = visualLine(fromLine) == visualLine(toLine); |
|
var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; |
|
var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; |
|
if (singleVLine) { |
|
if (leftEnd.top < rightStart.top - 2) { |
|
add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); |
|
add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); |
|
} else { |
|
add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); |
|
} |
|
} |
|
if (leftEnd.bottom < rightStart.top) |
|
add(leftSide, leftEnd.bottom, null, rightStart.top); |
|
} |
|
|
|
output.appendChild(fragment); |
|
} |
|
|
|
// Cursor-blinking |
|
function restartBlink(cm) { |
|
if (!cm.state.focused) return; |
|
var display = cm.display; |
|
clearInterval(display.blinker); |
|
var on = true; |
|
display.cursorDiv.style.visibility = ""; |
|
if (cm.options.cursorBlinkRate > 0) |
|
display.blinker = setInterval(function() { |
|
display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; |
|
}, cm.options.cursorBlinkRate); |
|
else if (cm.options.cursorBlinkRate < 0) |
|
display.cursorDiv.style.visibility = "hidden"; |
|
} |
|
|
|
// HIGHLIGHT WORKER |
|
|
|
function startWorker(cm, time) { |
|
if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) |
|
cm.state.highlight.set(time, bind(highlightWorker, cm)); |
|
} |
|
|
|
function highlightWorker(cm) { |
|
var doc = cm.doc; |
|
if (doc.frontier < doc.first) doc.frontier = doc.first; |
|
if (doc.frontier >= cm.display.viewTo) return; |
|
var end = +new Date + cm.options.workTime; |
|
var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); |
|
var changedLines = []; |
|
|
|
doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { |
|
if (doc.frontier >= cm.display.viewFrom) { // Visible |
|
var oldStyles = line.styles; |
|
var highlighted = highlightLine(cm, line, state, true); |
|
line.styles = highlighted.styles; |
|
var oldCls = line.styleClasses, newCls = highlighted.classes; |
|
if (newCls) line.styleClasses = newCls; |
|
else if (oldCls) line.styleClasses = null; |
|
var ischange = !oldStyles || oldStyles.length != line.styles.length || |
|
oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); |
|
for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; |
|
if (ischange) changedLines.push(doc.frontier); |
|
line.stateAfter = copyState(doc.mode, state); |
|
} else { |
|
processLine(cm, line.text, state); |
|
line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; |
|
} |
|
++doc.frontier; |
|
if (+new Date > end) { |
|
startWorker(cm, cm.options.workDelay); |
|
return true; |
|
} |
|
}); |
|
if (changedLines.length) runInOp(cm, function() { |
|
for (var i = 0; i < changedLines.length; i++) |
|
regLineChange(cm, changedLines[i], "text"); |
|
}); |
|
} |
|
|
|
// Finds the line to start with when starting a parse. Tries to |
|
// find a line with a stateAfter, so that it can start with a |
|
// valid state. If that fails, it returns the line with the |
|
// smallest indentation, which tends to need the least context to |
|
// parse correctly. |
|
function findStartLine(cm, n, precise) { |
|
var minindent, minline, doc = cm.doc; |
|
var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); |
|
for (var search = n; search > lim; --search) { |
|
if (search <= doc.first) return doc.first; |
|
var line = getLine(doc, search - 1); |
|
if (line.stateAfter && (!precise || search <= doc.frontier)) return search; |
|
var indented = countColumn(line.text, null, cm.options.tabSize); |
|
if (minline == null || minindent > indented) { |
|
minline = search - 1; |
|
minindent = indented; |
|
} |
|
} |
|
return minline; |
|
} |
|
|
|
function getStateBefore(cm, n, precise) { |
|
var doc = cm.doc, display = cm.display; |
|
if (!doc.mode.startState) return true; |
|
var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; |
|
if (!state) state = startState(doc.mode); |
|
else state = copyState(doc.mode, state); |
|
doc.iter(pos, n, function(line) { |
|
processLine(cm, line.text, state); |
|
var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; |
|
line.stateAfter = save ? copyState(doc.mode, state) : null; |
|
++pos; |
|
}); |
|
if (precise) doc.frontier = pos; |
|
return state; |
|
} |
|
|
|
// POSITION MEASUREMENT |
|
|
|
function paddingTop(display) {return display.lineSpace.offsetTop;} |
|
function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} |
|
function paddingH(display) { |
|
if (display.cachedPaddingH) return display.cachedPaddingH; |
|
var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); |
|
var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; |
|
var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; |
|
if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; |
|
return data; |
|
} |
|
|
|
function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; } |
|
function displayWidth(cm) { |
|
return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth; |
|
} |
|
function displayHeight(cm) { |
|
return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight; |
|
} |
|
|
|
// Ensure the lineView.wrapping.heights array is populated. This is |
|
// an array of bottom offsets for the lines that make up a drawn |
|
// line. When lineWrapping is on, there might be more than one |
|
// height. |
|
function ensureLineHeights(cm, lineView, rect) { |
|
var wrapping = cm.options.lineWrapping; |
|
var curWidth = wrapping && displayWidth(cm); |
|
if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { |
|
var heights = lineView.measure.heights = []; |
|
if (wrapping) { |
|
lineView.measure.width = curWidth; |
|
var rects = lineView.text.firstChild.getClientRects(); |
|
for (var i = 0; i < rects.length - 1; i++) { |
|
var cur = rects[i], next = rects[i + 1]; |
|
if (Math.abs(cur.bottom - next.bottom) > 2) |
|
heights.push((cur.bottom + next.top) / 2 - rect.top); |
|
} |
|
} |
|
heights.push(rect.bottom - rect.top); |
|
} |
|
} |
|
|
|
// Find a line map (mapping character offsets to text nodes) and a |
|
// measurement cache for the given line number. (A line view might |
|
// contain multiple lines when collapsed ranges are present.) |
|
function mapFromLineView(lineView, line, lineN) { |
|
if (lineView.line == line) |
|
return {map: lineView.measure.map, cache: lineView.measure.cache}; |
|
for (var i = 0; i < lineView.rest.length; i++) |
|
if (lineView.rest[i] == line) |
|
return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; |
|
for (var i = 0; i < lineView.rest.length; i++) |
|
if (lineNo(lineView.rest[i]) > lineN) |
|
return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; |
|
} |
|
|
|
// Render a line into the hidden node display.externalMeasured. Used |
|
// when measurement is needed for a line that's not in the viewport. |
|
function updateExternalMeasurement(cm, line) { |
|
line = visualLine(line); |
|
var lineN = lineNo(line); |
|
var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); |
|
view.lineN = lineN; |
|
var built = view.built = buildLineContent(cm, view); |
|
view.text = built.pre; |
|
removeChildrenAndAdd(cm.display.lineMeasure, built.pre); |
|
return view; |
|
} |
|
|
|
// Get a {top, bottom, left, right} box (in line-local coordinates) |
|
// for a given character. |
|
function measureChar(cm, line, ch, bias) { |
|
return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); |
|
} |
|
|
|
// Find a line view that corresponds to the given line number. |
|
function findViewForLine(cm, lineN) { |
|
if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) |
|
return cm.display.view[findViewIndex(cm, lineN)]; |
|
var ext = cm.display.externalMeasured; |
|
if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) |
|
return ext; |
|
} |
|
|
|
// Measurement can be split in two steps, the set-up work that |
|
// applies to the whole line, and the measurement of the actual |
|
// character. Functions like coordsChar, that need to do a lot of |
|
// measurements in a row, can thus ensure that the set-up work is |
|
// only done once. |
|
function prepareMeasureForLine(cm, line) { |
|
var lineN = lineNo(line); |
|
var view = findViewForLine(cm, lineN); |
|
if (view && !view.text) |
|
view = null; |
|
else if (view && view.changes) |
|
updateLineForChanges(cm, view, lineN, getDimensions(cm)); |
|
if (!view) |
|
view = updateExternalMeasurement(cm, line); |
|
|
|
var info = mapFromLineView(view, line, lineN); |
|
return { |
|
line: line, view: view, rect: null, |
|
map: info.map, cache: info.cache, before: info.before, |
|
hasHeights: false |
|
}; |
|
} |
|
|
|
// Given a prepared measurement object, measures the position of an |
|
// actual character (or fetches it from the cache). |
|
function measureCharPrepared(cm, prepared, ch, bias, varHeight) { |
|
if (prepared.before) ch = -1; |
|
var key = ch + (bias || ""), found; |
|
if (prepared.cache.hasOwnProperty(key)) { |
|
found = prepared.cache[key]; |
|
} else { |
|
if (!prepared.rect) |
|
prepared.rect = prepared.view.text.getBoundingClientRect(); |
|
if (!prepared.hasHeights) { |
|
ensureLineHeights(cm, prepared.view, prepared.rect); |
|
prepared.hasHeights = true; |
|
} |
|
found = measureCharInner(cm, prepared, ch, bias); |
|
if (!found.bogus) prepared.cache[key] = found; |
|
} |
|
return {left: found.left, right: found.right, |
|
top: varHeight ? found.rtop : found.top, |
|
bottom: varHeight ? found.rbottom : found.bottom}; |
|
} |
|
|
|
var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; |
|
|
|
function nodeAndOffsetInLineMap(map, ch, bias) { |
|
var node, start, end, collapse; |
|
// First, search the line map for the text node corresponding to, |
|
// or closest to, the target character. |
|
for (var i = 0; i < map.length; i += 3) { |
|
var mStart = map[i], mEnd = map[i + 1]; |
|
if (ch < mStart) { |
|
start = 0; end = 1; |
|
collapse = "left"; |
|
} else if (ch < mEnd) { |
|
start = ch - mStart; |
|
end = start + 1; |
|
} else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { |
|
end = mEnd - mStart; |
|
start = end - 1; |
|
if (ch >= mEnd) collapse = "right"; |
|
} |
|
if (start != null) { |
|
node = map[i + 2]; |
|
if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) |
|
collapse = bias; |
|
if (bias == "left" && start == 0) |
|
while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { |
|
node = map[(i -= 3) + 2]; |
|
collapse = "left"; |
|
} |
|
if (bias == "right" && start == mEnd - mStart) |
|
while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { |
|
node = map[(i += 3) + 2]; |
|
collapse = "right"; |
|
} |
|
break; |
|
} |
|
} |
|
return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; |
|
} |
|
|
|
function measureCharInner(cm, prepared, ch, bias) { |
|
var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); |
|
var node = place.node, start = place.start, end = place.end, collapse = place.collapse; |
|
|
|
var rect; |
|
if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. |
|
for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned |
|
while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start; |
|
while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end; |
|
if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) { |
|
rect = node.parentNode.getBoundingClientRect(); |
|
} else if (ie && cm.options.lineWrapping) { |
|
var rects = range(node, start, end).getClientRects(); |
|
if (rects.length) |
|
rect = rects[bias == "right" ? rects.length - 1 : 0]; |
|
else |
|
rect = nullRect; |
|
} else { |
|
rect = range(node, start, end).getBoundingClientRect() || nullRect; |
|
} |
|
if (rect.left || rect.right || start == 0) break; |
|
end = start; |
|
start = start - 1; |
|
collapse = "right"; |
|
} |
|
if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); |
|
} else { // If it is a widget, simply get the box for the whole widget. |
|
if (start > 0) collapse = bias = "right"; |
|
var rects; |
|
if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) |
|
rect = rects[bias == "right" ? rects.length - 1 : 0]; |
|
else |
|
rect = node.getBoundingClientRect(); |
|
} |
|
if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { |
|
var rSpan = node.parentNode.getClientRects()[0]; |
|
if (rSpan) |
|
rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; |
|
else |
|
rect = nullRect; |
|
} |
|
|
|
var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; |
|
var mid = (rtop + rbot) / 2; |
|
var heights = prepared.view.measure.heights; |
|
for (var i = 0; i < heights.length - 1; i++) |
|
if (mid < heights[i]) break; |
|
var top = i ? heights[i - 1] : 0, bot = heights[i]; |
|
var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, |
|
right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, |
|
top: top, bottom: bot}; |
|
if (!rect.left && !rect.right) result.bogus = true; |
|
if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } |
|
|
|
return result; |
|
} |
|
|
|
// Work around problem with bounding client rects on ranges being |
|
// returned incorrectly when zoomed on IE10 and below. |
|
function maybeUpdateRectForZooming(measure, rect) { |
|
if (!window.screen || screen.logicalXDPI == null || |
|
screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) |
|
return rect; |
|
var scaleX = screen.logicalXDPI / screen.deviceXDPI; |
|
var scaleY = screen.logicalYDPI / screen.deviceYDPI; |
|
return {left: rect.left * scaleX, right: rect.right * scaleX, |
|
top: rect.top * scaleY, bottom: rect.bottom * scaleY}; |
|
} |
|
|
|
function clearLineMeasurementCacheFor(lineView) { |
|
if (lineView.measure) { |
|
lineView.measure.cache = {}; |
|
lineView.measure.heights = null; |
|
if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) |
|
lineView.measure.caches[i] = {}; |
|
} |
|
} |
|
|
|
function clearLineMeasurementCache(cm) { |
|
cm.display.externalMeasure = null; |
|
removeChildren(cm.display.lineMeasure); |
|
for (var i = 0; i < cm.display.view.length; i++) |
|
clearLineMeasurementCacheFor(cm.display.view[i]); |
|
} |
|
|
|
function clearCaches(cm) { |
|
clearLineMeasurementCache(cm); |
|
cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; |
|
if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; |
|
cm.display.lineNumChars = null; |
|
} |
|
|
|
function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } |
|
function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } |
|
|
|
// Converts a {top, bottom, left, right} box from line-local |
|
// coordinates into another coordinate system. Context may be one of |
|
// "line", "div" (display.lineDiv), "local"/null (editor), "window", |
|
// or "page". |
|
function intoCoordSystem(cm, lineObj, rect, context) { |
|
if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { |
|
var size = widgetHeight(lineObj.widgets[i]); |
|
rect.top += size; rect.bottom += size; |
|
} |
|
if (context == "line") return rect; |
|
if (!context) context = "local"; |
|
var yOff = heightAtLine(lineObj); |
|
if (context == "local") yOff += paddingTop(cm.display); |
|
else yOff -= cm.display.viewOffset; |
|
if (context == "page" || context == "window") { |
|
var lOff = cm.display.lineSpace.getBoundingClientRect(); |
|
yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); |
|
var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); |
|
rect.left += xOff; rect.right += xOff; |
|
} |
|
rect.top += yOff; rect.bottom += yOff; |
|
return rect; |
|
} |
|
|
|
// Coverts a box from "div" coords to another coordinate system. |
|
// Context may be "window", "page", "div", or "local"/null. |
|
function fromCoordSystem(cm, coords, context) { |
|
if (context == "div") return coords; |
|
var left = coords.left, top = coords.top; |
|
// First move into "page" coordinate system |
|
if (context == "page") { |
|
left -= pageScrollX(); |
|
top -= pageScrollY(); |
|
} else if (context == "local" || !context) { |
|
var localBox = cm.display.sizer.getBoundingClientRect(); |
|
left += localBox.left; |
|
top += localBox.top; |
|
} |
|
|
|
var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); |
|
return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; |
|
} |
|
|
|
function charCoords(cm, pos, context, lineObj, bias) { |
|
if (!lineObj) lineObj = getLine(cm.doc, pos.line); |
|
return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); |
|
} |
|
|
|
// Returns a box for a given cursor position, which may have an |
|
// 'other' property containing the position of the secondary cursor |
|
// on a bidi boundary. |
|
function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { |
|
lineObj = lineObj || getLine(cm.doc, pos.line); |
|
if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); |
|
function get(ch, right) { |
|
var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); |
|
if (right) m.left = m.right; else m.right = m.left; |
|
return intoCoordSystem(cm, lineObj, m, context); |
|
} |
|
function getBidi(ch, partPos) { |
|
var part = order[partPos], right = part.level % 2; |
|
if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { |
|
part = order[--partPos]; |
|
ch = bidiRight(part) - (part.level % 2 ? 0 : 1); |
|
right = true; |
|
} else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { |
|
part = order[++partPos]; |
|
ch = bidiLeft(part) - part.level % 2; |
|
right = false; |
|
} |
|
if (right && ch == part.to && ch > part.from) return get(ch - 1); |
|
return get(ch, right); |
|
} |
|
var order = getOrder(lineObj), ch = pos.ch; |
|
if (!order) return get(ch); |
|
var partPos = getBidiPartAt(order, ch); |
|
var val = getBidi(ch, partPos); |
|
if (bidiOther != null) val.other = getBidi(ch, bidiOther); |
|
return val; |
|
} |
|
|
|
// Used to cheaply estimate the coordinates for a position. Used for |
|
// intermediate scroll updates. |
|
function estimateCoords(cm, pos) { |
|
var left = 0, pos = clipPos(cm.doc, pos); |
|
if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; |
|
var lineObj = getLine(cm.doc, pos.line); |
|
var top = heightAtLine(lineObj) + paddingTop(cm.display); |
|
return {left: left, right: left, top: top, bottom: top + lineObj.height}; |
|
} |
|
|
|
// Positions returned by coordsChar contain some extra information. |
|
// xRel is the relative x position of the input coordinates compared |
|
// to the found position (so xRel > 0 means the coordinates are to |
|
// the right of the character position, for example). When outside |
|
// is true, that means the coordinates lie outside the line's |
|
// vertical range. |
|
function PosWithInfo(line, ch, outside, xRel) { |
|
var pos = Pos(line, ch); |
|
pos.xRel = xRel; |
|
if (outside) pos.outside = true; |
|
return pos; |
|
} |
|
|
|
// Compute the character position closest to the given coordinates. |
|
// Input must be lineSpace-local ("div" coordinate system). |
|
function coordsChar(cm, x, y) { |
|
var doc = cm.doc; |
|
y += cm.display.viewOffset; |
|
if (y < 0) return PosWithInfo(doc.first, 0, true, -1); |
|
var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; |
|
if (lineN > last) |
|
return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); |
|
if (x < 0) x = 0; |
|
|
|
var lineObj = getLine(doc, lineN); |
|
for (;;) { |
|
var found = coordsCharInner(cm, lineObj, lineN, x, y); |
|
var merged = collapsedSpanAtEnd(lineObj); |
|
var mergedPos = merged && merged.find(0, true); |
|
if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) |
|
lineN = lineNo(lineObj = mergedPos.to.line); |
|
else |
|
return found; |
|
} |
|
} |
|
|
|
function coordsCharInner(cm, lineObj, lineNo, x, y) { |
|
var innerOff = y - heightAtLine(lineObj); |
|
var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; |
|
var preparedMeasure = prepareMeasureForLine(cm, lineObj); |
|
|
|
function getX(ch) { |
|
var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); |
|
wrongLine = true; |
|
if (innerOff > sp.bottom) return sp.left - adjust; |
|
else if (innerOff < sp.top) return sp.left + adjust; |
|
else wrongLine = false; |
|
return sp.left; |
|
} |
|
|
|
var bidi = getOrder(lineObj), dist = lineObj.text.length; |
|
var from = lineLeft(lineObj), to = lineRight(lineObj); |
|
var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; |
|
|
|
if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); |
|
// Do a binary search between these bounds. |
|
for (;;) { |
|
if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { |
|
var ch = x < fromX || x - fromX <= toX - x ? from : to; |
|
var xDiff = x - (ch == from ? fromX : toX); |
|
while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; |
|
var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, |
|
xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); |
|
return pos; |
|
} |
|
var step = Math.ceil(dist / 2), middle = from + step; |
|
if (bidi) { |
|
middle = from; |
|
for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); |
|
} |
|
var middleX = getX(middle); |
|
if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} |
|
else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} |
|
} |
|
} |
|
|
|
var measureText; |
|
// Compute the default text height. |
|
function textHeight(display) { |
|
if (display.cachedTextHeight != null) return display.cachedTextHeight; |
|
if (measureText == null) { |
|
measureText = elt("pre"); |
|
// Measure a bunch of lines, for browsers that compute |
|
// fractional heights. |
|
for (var i = 0; i < 49; ++i) { |
|
measureText.appendChild(document.createTextNode("x")); |
|
measureText.appendChild(elt("br")); |
|
} |
|
measureText.appendChild(document.createTextNode("x")); |
|
} |
|
removeChildrenAndAdd(display.measure, measureText); |
|
var height = measureText.offsetHeight / 50; |
|
if (height > 3) display.cachedTextHeight = height; |
|
removeChildren(display.measure); |
|
return height || 1; |
|
} |
|
|
|
// Compute the default character width. |
|
function charWidth(display) { |
|
if (display.cachedCharWidth != null) return display.cachedCharWidth; |
|
var anchor = elt("span", "xxxxxxxxxx"); |
|
var pre = elt("pre", [anchor]); |
|
removeChildrenAndAdd(display.measure, pre); |
|
var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; |
|
if (width > 2) display.cachedCharWidth = width; |
|
return width || 10; |
|
} |
|
|
|
// OPERATIONS |
|
|
|
// Operations are used to wrap a series of changes to the editor |
|
// state in such a way that each change won't have to update the |
|
// cursor and display (which would be awkward, slow, and |
|
// error-prone). Instead, display updates are batched and then all |
|
// combined and executed at once. |
|
|
|
var operationGroup = null; |
|
|
|
var nextOpId = 0; |
|
// Start a new operation. |
|
function startOperation(cm) { |
|
cm.curOp = { |
|
cm: cm, |
|
viewChanged: false, // Flag that indicates that lines might need to be redrawn |
|
startHeight: cm.doc.height, // Used to detect need to update scrollbar |
|
forceUpdate: false, // Used to force a redraw |
|
updateInput: null, // Whether to reset the input textarea |
|
typing: false, // Whether this reset should be careful to leave existing text (for compositing) |
|
changeObjs: null, // Accumulated changes, for firing change events |
|
cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on |
|
cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already |
|
selectionChanged: false, // Whether the selection needs to be redrawn |
|
updateMaxLine: false, // Set when the widest line needs to be determined anew |
|
scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet |
|
scrollToPos: null, // Used to scroll to a specific position |
|
focus: false, |
|
id: ++nextOpId // Unique ID |
|
}; |
|
if (operationGroup) { |
|
operationGroup.ops.push(cm.curOp); |
|
} else { |
|
cm.curOp.ownsGroup = operationGroup = { |
|
ops: [cm.curOp], |
|
delayedCallbacks: [] |
|
}; |
|
} |
|
} |
|
|
|
function fireCallbacksForOps(group) { |
|
// Calls delayed callbacks and cursorActivity handlers until no |
|
// new ones appear |
|
var callbacks = group.delayedCallbacks, i = 0; |
|
do { |
|
for (; i < callbacks.length; i++) |
|
callbacks[i](); |
|
for (var j = 0; j < group.ops.length; j++) { |
|
var op = group.ops[j]; |
|
if (op.cursorActivityHandlers) |
|
while (op.cursorActivityCalled < op.cursorActivityHandlers.length) |
|
op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); |
|
} |
|
} while (i < callbacks.length); |
|
} |
|
|
|
// Finish an operation, updating the display and signalling delayed events |
|
function endOperation(cm) { |
|
var op = cm.curOp, group = op.ownsGroup; |
|
if (!group) return; |
|
|
|
try { fireCallbacksForOps(group); } |
|
finally { |
|
operationGroup = null; |
|
for (var i = 0; i < group.ops.length; i++) |
|
group.ops[i].cm.curOp = null; |
|
endOperations(group); |
|
} |
|
} |
|
|
|
// The DOM updates done when an operation finishes are batched so |
|
// that the minimum number of relayouts are required. |
|
function endOperations(group) { |
|
var ops = group.ops; |
|
for (var i = 0; i < ops.length; i++) // Read DOM |
|
endOperation_R1(ops[i]); |
|
for (var i = 0; i < ops.length; i++) // Write DOM (maybe) |
|
endOperation_W1(ops[i]); |
|
for (var i = 0; i < ops.length; i++) // Read DOM |
|
endOperation_R2(ops[i]); |
|
for (var i = 0; i < ops.length; i++) // Write DOM (maybe) |
|
endOperation_W2(ops[i]); |
|
for (var i = 0; i < ops.length; i++) // Read DOM |
|
endOperation_finish(ops[i]); |
|
} |
|
|
|
function endOperation_R1(op) { |
|
var cm = op.cm, display = cm.display; |
|
maybeClipScrollbars(cm); |
|
if (op.updateMaxLine) findMaxLine(cm); |
|
|
|
op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || |
|
op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || |
|
op.scrollToPos.to.line >= display.viewTo) || |
|
display.maxLineChanged && cm.options.lineWrapping; |
|
op.update = op.mustUpdate && |
|
new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); |
|
} |
|
|
|
function endOperation_W1(op) { |
|
op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); |
|
} |
|
|
|
function endOperation_R2(op) { |
|
var cm = op.cm, display = cm.display; |
|
if (op.updatedDisplay) updateHeightsInViewport(cm); |
|
|
|
op.barMeasure = measureForScrollbars(cm); |
|
|
|
// If the max line changed since it was last measured, measure it, |
|
// and ensure the document's width matches it. |
|
// updateDisplay_W2 will use these properties to do the actual resizing |
|
if (display.maxLineChanged && !cm.options.lineWrapping) { |
|
op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; |
|
cm.display.sizerWidth = op.adjustWidthTo; |
|
op.barMeasure.scrollWidth = |
|
Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); |
|
op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); |
|
} |
|
|
|
if (op.updatedDisplay || op.selectionChanged) |
|
op.preparedSelection = display.input.prepareSelection(); |
|
} |
|
|
|
function endOperation_W2(op) { |
|
var cm = op.cm; |
|
|
|
if (op.adjustWidthTo != null) { |
|
cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; |
|
if (op.maxScrollLeft < cm.doc.scrollLeft) |
|
setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); |
|
cm.display.maxLineChanged = false; |
|
} |
|
|
|
if (op.preparedSelection) |
|
cm.display.input.showSelection(op.preparedSelection); |
|
if (op.updatedDisplay) |
|
setDocumentHeight(cm, op.barMeasure); |
|
if (op.updatedDisplay || op.startHeight != cm.doc.height) |
|
updateScrollbars(cm, op.barMeasure); |
|
|
|
if (op.selectionChanged) restartBlink(cm); |
|
|
|
if (cm.state.focused && op.updateInput) |
|
cm.display.input.reset(op.typing); |
|
if (op.focus && op.focus == activeElt()) ensureFocus(op.cm); |
|
} |
|
|
|
function endOperation_finish(op) { |
|
var cm = op.cm, display = cm.display, doc = cm.doc; |
|
|
|
if (op.updatedDisplay) postUpdateDisplay(cm, op.update); |
|
|
|
// Abort mouse wheel delta measurement, when scrolling explicitly |
|
if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) |
|
display.wheelStartX = display.wheelStartY = null; |
|
|
|
// Propagate the scroll position to the actual DOM scroller |
|
if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { |
|
doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); |
|
display.scrollbars.setScrollTop(doc.scrollTop); |
|
display.scroller.scrollTop = doc.scrollTop; |
|
} |
|
if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { |
|
doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); |
|
display.scrollbars.setScrollLeft(doc.scrollLeft); |
|
display.scroller.scrollLeft = doc.scrollLeft; |
|
alignHorizontally(cm); |
|
} |
|
// If we need to scroll a specific position into view, do so. |
|
if (op.scrollToPos) { |
|
var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), |
|
clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); |
|
if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); |
|
} |
|
|
|
// Fire events for markers that are hidden/unidden by editing or |
|
// undoing |
|
var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; |
|
if (hidden) for (var i = 0; i < hidden.length; ++i) |
|
if (!hidden[i].lines.length) signal(hidden[i], "hide"); |
|
if (unhidden) for (var i = 0; i < unhidden.length; ++i) |
|
if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); |
|
|
|
if (display.wrapper.offsetHeight) |
|
doc.scrollTop = cm.display.scroller.scrollTop; |
|
|
|
// Fire change events, and delayed event handlers |
|
if (op.changeObjs) |
|
signal(cm, "changes", cm, op.changeObjs); |
|
if (op.update) |
|
op.update.finish(); |
|
} |
|
|
|
// Run the given function in an operation |
|
function runInOp(cm, f) { |
|
if (cm.curOp) return f(); |
|
startOperation(cm); |
|
try { return f(); } |
|
finally { endOperation(cm); } |
|
} |
|
// Wraps a function in an operation. Returns the wrapped function. |
|
function operation(cm, f) { |
|
return function() { |
|
if (cm.curOp) return f.apply(cm, arguments); |
|
startOperation(cm); |
|
try { return f.apply(cm, arguments); } |
|
finally { endOperation(cm); } |
|
}; |
|
} |
|
// Used to add methods to editor and doc instances, wrapping them in |
|
// operations. |
|
function methodOp(f) { |
|
return function() { |
|
if (this.curOp) return f.apply(this, arguments); |
|
startOperation(this); |
|
try { return f.apply(this, arguments); } |
|
finally { endOperation(this); } |
|
}; |
|
} |
|
function docMethodOp(f) { |
|
return function() { |
|
var cm = this.cm; |
|
if (!cm || cm.curOp) return f.apply(this, arguments); |
|
startOperation(cm); |
|
try { return f.apply(this, arguments); } |
|
finally { endOperation(cm); } |
|
}; |
|
} |
|
|
|
// VIEW TRACKING |
|
|
|
// These objects are used to represent the visible (currently drawn) |
|
// part of the document. A LineView may correspond to multiple |
|
// logical lines, if those are connected by collapsed ranges. |
|
function LineView(doc, line, lineN) { |
|
// The starting line |
|
this.line = line; |
|
// Continuing lines, if any |
|
this.rest = visualLineContinued(line); |
|
// Number of logical lines in this visual line |
|
this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; |
|
this.node = this.text = null; |
|
this.hidden = lineIsHidden(doc, line); |
|
} |
|
|
|
// Create a range of LineView objects for the given lines. |
|
function buildViewArray(cm, from, to) { |
|
var array = [], nextPos; |
|
for (var pos = from; pos < to; pos = nextPos) { |
|
var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); |
|
nextPos = pos + view.size; |
|
array.push(view); |
|
} |
|
return array; |
|
} |
|
|
|
// Updates the display.view data structure for a given change to the |
|
// document. From and to are in pre-change coordinates. Lendiff is |
|
// the amount of lines added or subtracted by the change. This is |
|
// used for changes that span multiple lines, or change the way |
|
// lines are divided into visual lines. regLineChange (below) |
|
// registers single-line changes. |
|
function regChange(cm, from, to, lendiff) { |
|
if (from == null) from = cm.doc.first; |
|
if (to == null) to = cm.doc.first + cm.doc.size; |
|
if (!lendiff) lendiff = 0; |
|
|
|
var display = cm.display; |
|
if (lendiff && to < display.viewTo && |
|
(display.updateLineNumbers == null || display.updateLineNumbers > from)) |
|
display.updateLineNumbers = from; |
|
|
|
cm.curOp.viewChanged = true; |
|
|
|
if (from >= display.viewTo) { // Change after |
|
if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) |
|
resetView(cm); |
|
} else if (to <= display.viewFrom) { // Change before |
|
if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { |
|
resetView(cm); |
|
} else { |
|
display.viewFrom += lendiff; |
|
display.viewTo += lendiff; |
|
} |
|
} else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap |
|
resetView(cm); |
|
} else if (from <= display.viewFrom) { // Top overlap |
|
var cut = viewCuttingPoint(cm, to, to + lendiff, 1); |
|
if (cut) { |
|
display.view = display.view.slice(cut.index); |
|
display.viewFrom = cut.lineN; |
|
display.viewTo += lendiff; |
|
} else { |
|
resetView(cm); |
|
} |
|
} else if (to >= display.viewTo) { // Bottom overlap |
|
var cut = viewCuttingPoint(cm, from, from, -1); |
|
if (cut) { |
|
display.view = display.view.slice(0, cut.index); |
|
display.viewTo = cut.lineN; |
|
} else { |
|
resetView(cm); |
|
} |
|
} else { // Gap in the middle |
|
var cutTop = viewCuttingPoint(cm, from, from, -1); |
|
var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); |
|
if (cutTop && cutBot) { |
|
display.view = display.view.slice(0, cutTop.index) |
|
.concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) |
|
.concat(display.view.slice(cutBot.index)); |
|
display.viewTo += lendiff; |
|
} else { |
|
resetView(cm); |
|
} |
|
} |
|
|
|
var ext = display.externalMeasured; |
|
if (ext) { |
|
if (to < ext.lineN) |
|
ext.lineN += lendiff; |
|
else if (from < ext.lineN + ext.size) |
|
display.externalMeasured = null; |
|
} |
|
} |
|
|
|
// Register a change to a single line. Type must be one of "text", |
|
// "gutter", "class", "widget" |
|
function regLineChange(cm, line, type) { |
|
cm.curOp.viewChanged = true; |
|
var display = cm.display, ext = cm.display.externalMeasured; |
|
if (ext && line >= ext.lineN && line < ext.lineN + ext.size) |
|
display.externalMeasured = null; |
|
|
|
if (line < display.viewFrom || line >= display.viewTo) return; |
|
var lineView = display.view[findViewIndex(cm, line)]; |
|
if (lineView.node == null) return; |
|
var arr = lineView.changes || (lineView.changes = []); |
|
if (indexOf(arr, type) == -1) arr.push(type); |
|
} |
|
|
|
// Clear the view. |
|
function resetView(cm) { |
|
cm.display.viewFrom = cm.display.viewTo = cm.doc.first; |
|
cm.display.view = []; |
|
cm.display.viewOffset = 0; |
|
} |
|
|
|
// Find the view element corresponding to a given line. Return null |
|
// when the line isn't visible. |
|
function findViewIndex(cm, n) { |
|
if (n >= cm.display.viewTo) return null; |
|
n -= cm.display.viewFrom; |
|
if (n < 0) return null; |
|
var view = cm.display.view; |
|
for (var i = 0; i < view.length; i++) { |
|
n -= view[i].size; |
|
if (n < 0) return i; |
|
} |
|
} |
|
|
|
function viewCuttingPoint(cm, oldN, newN, dir) { |
|
var index = findViewIndex(cm, oldN), diff, view = cm.display.view; |
|
if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) |
|
return {index: index, lineN: newN}; |
|
for (var i = 0, n = cm.display.viewFrom; i < index; i++) |
|
n += view[i].size; |
|
if (n != oldN) { |
|
if (dir > 0) { |
|
if (index == view.length - 1) return null; |
|
diff = (n + view[index].size) - oldN; |
|
index++; |
|
} else { |
|
diff = n - oldN; |
|
} |
|
oldN += diff; newN += diff; |
|
} |
|
while (visualLineNo(cm.doc, newN) != newN) { |
|
if (index == (dir < 0 ? 0 : view.length - 1)) return null; |
|
newN += dir * view[index - (dir < 0 ? 1 : 0)].size; |
|
index += dir; |
|
} |
|
return {index: index, lineN: newN}; |
|
} |
|
|
|
// Force the view to cover a given range, adding empty view element |
|
// or clipping off existing ones as needed. |
|
function adjustView(cm, from, to) { |
|
var display = cm.display, view = display.view; |
|
if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { |
|
display.view = buildViewArray(cm, from, to); |
|
display.viewFrom = from; |
|
} else { |
|
if (display.viewFrom > from) |
|
display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); |
|
else if (display.viewFrom < from) |
|
display.view = display.view.slice(findViewIndex(cm, from)); |
|
display.viewFrom = from; |
|
if (display.viewTo < to) |
|
display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); |
|
else if (display.viewTo > to) |
|
display.view = display.view.slice(0, findViewIndex(cm, to)); |
|
} |
|
display.viewTo = to; |
|
} |
|
|
|
// Count the number of lines in the view whose DOM representation is |
|
// out of date (or nonexistent). |
|
function countDirtyView(cm) { |
|
var view = cm.display.view, dirty = 0; |
|
for (var i = 0; i < view.length; i++) { |
|
var lineView = view[i]; |
|
if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; |
|
} |
|
return dirty; |
|
} |
|
|
|
// EVENT HANDLERS |
|
|
|
// Attach the necessary event handlers when initializing the editor |
|
function registerEventHandlers(cm) { |
|
var d = cm.display; |
|
on(d.scroller, "mousedown", operation(cm, onMouseDown)); |
|
// Older IE's will not fire a second mousedown for a double click |
|
if (ie && ie_version < 11) |
|
on(d.scroller, "dblclick", operation(cm, function(e) { |
|
if (signalDOMEvent(cm, e)) return; |
|
var pos = posFromMouse(cm, e); |
|
if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; |
|
e_preventDefault(e); |
|
var word = cm.findWordAt(pos); |
|
extendSelection(cm.doc, word.anchor, word.head); |
|
})); |
|
else |
|
on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); |
|
// Some browsers fire contextmenu *after* opening the menu, at |
|
// which point we can't mess with it anymore. Context menu is |
|
// handled in onMouseDown for these browsers. |
|
if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); |
|
|
|
// Used to suppress mouse event handling when a touch happens |
|
var touchFinished, prevTouch = {end: 0}; |
|
function finishTouch() { |
|
if (d.activeTouch) { |
|
touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000); |
|
prevTouch = d.activeTouch; |
|
prevTouch.end = +new Date; |
|
} |
|
}; |
|
function isMouseLikeTouchEvent(e) { |
|
if (e.touches.length != 1) return false; |
|
var touch = e.touches[0]; |
|
return touch.radiusX <= 1 && touch.radiusY <= 1; |
|
} |
|
function farAway(touch, other) { |
|
if (other.left == null) return true; |
|
var dx = other.left - touch.left, dy = other.top - touch.top; |
|
return dx * dx + dy * dy > 20 * 20; |
|
} |
|
on(d.scroller, "touchstart", function(e) { |
|
if (!isMouseLikeTouchEvent(e)) { |
|
clearTimeout(touchFinished); |
|
var now = +new Date; |
|
d.activeTouch = {start: now, moved: false, |
|
prev: now - prevTouch.end <= 300 ? prevTouch : null}; |
|
if (e.touches.length == 1) { |
|
d.activeTouch.left = e.touches[0].pageX; |
|
d.activeTouch.top = e.touches[0].pageY; |
|
} |
|
} |
|
}); |
|
on(d.scroller, "touchmove", function() { |
|
if (d.activeTouch) d.activeTouch.moved = true; |
|
}); |
|
on(d.scroller, "touchend", function(e) { |
|
var touch = d.activeTouch; |
|
if (touch && !eventInWidget(d, e) && touch.left != null && |
|
!touch.moved && new Date - touch.start < 300) { |
|
var pos = cm.coordsChar(d.activeTouch, "page"), range; |
|
if (!touch.prev || farAway(touch, touch.prev)) // Single tap |
|
range = new Range(pos, pos); |
|
else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap |
|
range = cm.findWordAt(pos); |
|
else // Triple tap |
|
range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); |
|
cm.setSelection(range.anchor, range.head); |
|
cm.focus(); |
|
e_preventDefault(e); |
|
} |
|
finishTouch(); |
|
}); |
|
on(d.scroller, "touchcancel", finishTouch); |
|
|
|
// Sync scrolling between fake scrollbars and real scrollable |
|
// area, ensure viewport is updated when scrolling. |
|
on(d.scroller, "scroll", function() { |
|
if (d.scroller.clientHeight) { |
|
setScrollTop(cm, d.scroller.scrollTop); |
|
setScrollLeft(cm, d.scroller.scrollLeft, true); |
|
signal(cm, "scroll", cm); |
|
} |
|
}); |
|
|
|
// Listen to wheel events in order to try and update the viewport on time. |
|
on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); |
|
on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); |
|
|
|
// Prevent wrapper from ever scrolling |
|
on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); |
|
|
|
d.dragFunctions = { |
|
simple: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, |
|
start: function(e){onDragStart(cm, e);}, |
|
drop: operation(cm, onDrop) |
|
}; |
|
|
|
var inp = d.input.getField(); |
|
on(inp, "keyup", function(e) { onKeyUp.call(cm, e); }); |
|
on(inp, "keydown", operation(cm, onKeyDown)); |
|
on(inp, "keypress", operation(cm, onKeyPress)); |
|
on(inp, "focus", bind(onFocus, cm)); |
|
on(inp, "blur", bind(onBlur, cm)); |
|
} |
|
|
|
function dragDropChanged(cm, value, old) { |
|
var wasOn = old && old != CodeMirror.Init; |
|
if (!value != !wasOn) { |
|
var funcs = cm.display.dragFunctions; |
|
var toggle = value ? on : off; |
|
toggle(cm.display.scroller, "dragstart", funcs.start); |
|
toggle(cm.display.scroller, "dragenter", funcs.simple); |
|
toggle(cm.display.scroller, "dragover", funcs.simple); |
|
toggle(cm.display.scroller, "drop", funcs.drop); |
|
} |
|
} |
|
|
|
// Called when the window resizes |
|
function onResize(cm) { |
|
var d = cm.display; |
|
if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) |
|
return; |
|
// Might be a text scaling operation, clear size caches. |
|
d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; |
|
d.scrollbarsClipped = false; |
|
cm.setSize(); |
|
} |
|
|
|
// MOUSE EVENTS |
|
|
|
// Return true when the given mouse event happened in a widget |
|
function eventInWidget(display, e) { |
|
for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { |
|
if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || |
|
(n.parentNode == display.sizer && n != display.mover)) |
|
return true; |
|
} |
|
} |
|
|
|
// Given a mouse event, find the corresponding position. If liberal |
|
// is false, it checks whether a gutter or scrollbar was clicked, |
|
// and returns null if it was. forRect is used by rectangular |
|
// selections, and tries to estimate a character position even for |
|
// coordinates beyond the right of the text. |
|
function posFromMouse(cm, e, liberal, forRect) { |
|
var display = cm.display; |
|
if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null; |
|
|
|
var x, y, space = display.lineSpace.getBoundingClientRect(); |
|
// Fails unpredictably on IE[67] when mouse is dragged around quickly. |
|
try { x = e.clientX - space.left; y = e.clientY - space.top; } |
|
catch (e) { return null; } |
|
var coords = coordsChar(cm, x, y), line; |
|
if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { |
|
var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; |
|
coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); |
|
} |
|
return coords; |
|
} |
|
|
|
// A mouse down can be a single click, double click, triple click, |
|
// start of selection drag, start of text drag, new cursor |
|
// (ctrl-click), rectangle drag (alt-drag), or xwin |
|
// middle-click-paste. Or it might be a click on something we should |
|
// not interfere with, such as a scrollbar or widget. |
|
function onMouseDown(e) { |
|
var cm = this, display = cm.display; |
|
if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return; |
|
display.shift = e.shiftKey; |
|
|
|
if (eventInWidget(display, e)) { |
|
if (!webkit) { |
|
// Briefly turn off draggability, to allow widgets to do |
|
// normal dragging things. |
|
display.scroller.draggable = false; |
|
setTimeout(function(){display.scroller.draggable = true;}, 100); |
|
} |
|
return; |
|
} |
|
if (clickInGutter(cm, e)) return; |
|
var start = posFromMouse(cm, e); |
|
window.focus(); |
|
|
|
switch (e_button(e)) { |
|
case 1: |
|
if (start) |
|
leftButtonDown(cm, e, start); |
|
else if (e_target(e) == display.scroller) |
|
e_preventDefault(e); |
|
break; |
|
case 2: |
|
if (webkit) cm.state.lastMiddleDown = +new Date; |
|
if (start) extendSelection(cm.doc, start); |
|
setTimeout(function() {display.input.focus();}, 20); |
|
e_preventDefault(e); |
|
break; |
|
case 3: |
|
if (captureRightClick) onContextMenu(cm, e); |
|
else delayBlurEvent(cm); |
|
break; |
|
} |
|
} |
|
|
|
var lastClick, lastDoubleClick; |
|
function leftButtonDown(cm, e, start) { |
|
if (ie) setTimeout(bind(ensureFocus, cm), 0); |
|
else cm.curOp.focus = activeElt(); |
|
|
|
var now = +new Date, type; |
|
if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { |
|
type = "triple"; |
|
} else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { |
|
type = "double"; |
|
lastDoubleClick = {time: now, pos: start}; |
|
} else { |
|
type = "single"; |
|
lastClick = {time: now, pos: start}; |
|
} |
|
|
|
var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; |
|
if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && |
|
type == "single" && (contained = sel.contains(start)) > -1 && |
|
(cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) && |
|
(cmp(contained.to(), start) > 0 || start.xRel < 0)) |
|
leftButtonStartDrag(cm, e, start, modifier); |
|
else |
|
leftButtonSelect(cm, e, start, type, modifier); |
|
} |
|
|
|
// Start a text drag. When it ends, see if any dragging actually |
|
// happen, and treat as a click if it didn't. |
|
function leftButtonStartDrag(cm, e, start, modifier) { |
|
var display = cm.display, startTime = +new Date; |
|
var dragEnd = operation(cm, function(e2) { |
|
if (webkit) display.scroller.draggable = false; |
|
cm.state.draggingText = false; |
|
off(document, "mouseup", dragEnd); |
|
off(display.scroller, "drop", dragEnd); |
|
if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { |
|
e_preventDefault(e2); |
|
if (!modifier && +new Date - 200 < startTime) |
|
extendSelection(cm.doc, start); |
|
// Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) |
|
if (webkit || ie && ie_version == 9) |
|
setTimeout(function() {document.body.focus(); display.input.focus();}, 20); |
|
else |
|
display.input.focus(); |
|
} |
|
}); |
|
// Let the drag handler handle this. |
|
if (webkit) display.scroller.draggable = true; |
|
cm.state.draggingText = dragEnd; |
|
// IE's approach to draggable |
|
if (display.scroller.dragDrop) display.scroller.dragDrop(); |
|
on(document, "mouseup", dragEnd); |
|
on(display.scroller, "drop", dragEnd); |
|
} |
|
|
|
// Normal selection, as opposed to text dragging. |
|
function leftButtonSelect(cm, e, start, type, addNew) { |
|
var display = cm.display, doc = cm.doc; |
|
e_preventDefault(e); |
|
|
|
var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; |
|
if (addNew && !e.shiftKey) { |
|
ourIndex = doc.sel.contains(start); |
|
if (ourIndex > -1) |
|
ourRange = ranges[ourIndex]; |
|
else |
|
ourRange = new Range(start, start); |
|
} else { |
|
ourRange = doc.sel.primary(); |
|
ourIndex = doc.sel.primIndex; |
|
} |
|
|
|
if (e.altKey) { |
|
type = "rect"; |
|
if (!addNew) ourRange = new Range(start, start); |
|
start = posFromMouse(cm, e, true, true); |
|
ourIndex = -1; |
|
} else if (type == "double") { |
|
var word = cm.findWordAt(start); |
|
if (cm.display.shift || doc.extend) |
|
ourRange = extendRange(doc, ourRange, word.anchor, word.head); |
|
else |
|
ourRange = word; |
|
} else if (type == "triple") { |
|
var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); |
|
if (cm.display.shift || doc.extend) |
|
ourRange = extendRange(doc, ourRange, line.anchor, line.head); |
|
else |
|
ourRange = line; |
|
} else { |
|
ourRange = extendRange(doc, ourRange, start); |
|
} |
|
|
|
if (!addNew) { |
|
ourIndex = 0; |
|
setSelection(doc, new Selection([ourRange], 0), sel_mouse); |
|
startSel = doc.sel; |
|
} else if (ourIndex == -1) { |
|
ourIndex = ranges.length; |
|
setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), |
|
{scroll: false, origin: "*mouse"}); |
|
} else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { |
|
setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); |
|
startSel = doc.sel; |
|
} else { |
|
replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); |
|
} |
|
|
|
var lastPos = start; |
|
function extendTo(pos) { |
|
if (cmp(lastPos, pos) == 0) return; |
|
lastPos = pos; |
|
|
|
if (type == "rect") { |
|
var ranges = [], tabSize = cm.options.tabSize; |
|
var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); |
|
var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); |
|
var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); |
|
for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); |
|
line <= end; line++) { |
|
var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); |
|
if (left == right) |
|
ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); |
|
else if (text.length > leftPos) |
|
ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); |
|
} |
|
if (!ranges.length) ranges.push(new Range(start, start)); |
|
setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), |
|
{origin: "*mouse", scroll: false}); |
|
cm.scrollIntoView(pos); |
|
} else { |
|
var oldRange = ourRange; |
|
var anchor = oldRange.anchor, head = pos; |
|
if (type != "single") { |
|
if (type == "double") |
|
var range = cm.findWordAt(pos); |
|
else |
|
var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); |
|
if (cmp(range.anchor, anchor) > 0) { |
|
head = range.head; |
|
anchor = minPos(oldRange.from(), range.anchor); |
|
} else { |
|
head = range.anchor; |
|
anchor = maxPos(oldRange.to(), range.head); |
|
} |
|
} |
|
var ranges = startSel.ranges.slice(0); |
|
ranges[ourIndex] = new Range(clipPos(doc, anchor), head); |
|
setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); |
|
} |
|
} |
|
|
|
var editorSize = display.wrapper.getBoundingClientRect(); |
|
// Used to ensure timeout re-tries don't fire when another extend |
|
// happened in the meantime (clearTimeout isn't reliable -- at |
|
// least on Chrome, the timeouts still happen even when cleared, |
|
// if the clear happens after their scheduled firing time). |
|
var counter = 0; |
|
|
|
function extend(e) { |
|
var curCount = ++counter; |
|
var cur = posFromMouse(cm, e, true, type == "rect"); |
|
if (!cur) return; |
|
if (cmp(cur, lastPos) != 0) { |
|
cm.curOp.focus = activeElt(); |
|
extendTo(cur); |
|
var visible = visibleLines(display, doc); |
|
if (cur.line >= visible.to || cur.line < visible.from) |
|
setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); |
|
} else { |
|
var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; |
|
if (outside) setTimeout(operation(cm, function() { |
|
if (counter != curCount) return; |
|
display.scroller.scrollTop += outside; |
|
extend(e); |
|
}), 50); |
|
} |
|
} |
|
|
|
function done(e) { |
|
counter = Infinity; |
|
e_preventDefault(e); |
|
display.input.focus(); |
|
off(document, "mousemove", move); |
|
off(document, "mouseup", up); |
|
doc.history.lastSelOrigin = null; |
|
} |
|
|
|
var move = operation(cm, function(e) { |
|
if (!e_button(e)) done(e); |
|
else extend(e); |
|
}); |
|
var up = operation(cm, done); |
|
on(document, "mousemove", move); |
|
on(document, "mouseup", up); |
|
} |
|
|
|
// Determines whether an event happened in the gutter, and fires the |
|
// handlers for the corresponding event. |
|
function gutterEvent(cm, e, type, prevent, signalfn) { |
|
try { var mX = e.clientX, mY = e.clientY; } |
|
catch(e) { return false; } |
|
if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; |
|
if (prevent) e_preventDefault(e); |
|
|
|
var display = cm.display; |
|
var lineBox = display.lineDiv.getBoundingClientRect(); |
|
|
|
if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); |
|
mY -= lineBox.top - display.viewOffset; |
|
|
|
for (var i = 0; i < cm.options.gutters.length; ++i) { |
|
var g = display.gutters.childNodes[i]; |
|
if (g && g.getBoundingClientRect().right >= mX) { |
|
var line = lineAtHeight(cm.doc, mY); |
|
var gutter = cm.options.gutters[i]; |
|
signalfn(cm, type, cm, line, gutter, e); |
|
return e_defaultPrevented(e); |
|
} |
|
} |
|
} |
|
|
|
function clickInGutter(cm, e) { |
|
return gutterEvent(cm, e, "gutterClick", true, signalLater); |
|
} |
|
|
|
// Kludge to work around strange IE behavior where it'll sometimes |
|
// re-fire a series of drag-related events right after the drop (#1551) |
|
var lastDrop = 0; |
|
|
|
function onDrop(e) { |
|
var cm = this; |
|
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) |
|
return; |
|
e_preventDefault(e); |
|
if (ie) lastDrop = +new Date; |
|
var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; |
|
if (!pos || isReadOnly(cm)) return; |
|
// Might be a file drop, in which case we simply extract the text |
|
// and insert it. |
|
if (files && files.length && window.FileReader && window.File) { |
|
var n = files.length, text = Array(n), read = 0; |
|
var loadFile = function(file, i) { |
|
var reader = new FileReader; |
|
reader.onload = operation(cm, function() { |
|
text[i] = reader.result; |
|
if (++read == n) { |
|
pos = clipPos(cm.doc, pos); |
|
var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; |
|
makeChange(cm.doc, change); |
|
setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); |
|
} |
|
}); |
|
reader.readAsText(file); |
|
}; |
|
for (var i = 0; i < n; ++i) loadFile(files[i], i); |
|
} else { // Normal drop |
|
// Don't do a replace if the drop happened inside of the selected text. |
|
if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { |
|
cm.state.draggingText(e); |
|
// Ensure the editor is re-focused |
|
setTimeout(function() {cm.display.input.focus();}, 20); |
|
return; |
|
} |
|
try { |
|
var text = e.dataTransfer.getData("Text"); |
|
if (text) { |
|
if (cm.state.draggingText && !(mac ? e.altKey : e.ctrlKey)) |
|
var selected = cm.listSelections(); |
|
setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); |
|
if (selected) for (var i = 0; i < selected.length; ++i) |
|
replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); |
|
cm.replaceSelection(text, "around", "paste"); |
|
cm.display.input.focus(); |
|
} |
|
} |
|
catch(e){} |
|
} |
|
} |
|
|
|
function onDragStart(cm, e) { |
|
if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } |
|
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; |
|
|
|
e.dataTransfer.setData("Text", cm.getSelection()); |
|
|
|
// Use dummy image instead of default browsers image. |
|
// Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. |
|
if (e.dataTransfer.setDragImage && !safari) { |
|
var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); |
|
img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; |
|
if (presto) { |
|
img.width = img.height = 1; |
|
cm.display.wrapper.appendChild(img); |
|
// Force a relayout, or Opera won't use our image for some obscure reason |
|
img._top = img.offsetTop; |
|
} |
|
e.dataTransfer.setDragImage(img, 0, 0); |
|
if (presto) img.parentNode.removeChild(img); |
|
} |
|
} |
|
|
|
// SCROLL EVENTS |
|
|
|
// Sync the scrollable area and scrollbars, ensure the viewport |
|
// covers the visible area. |
|
function setScrollTop(cm, val) { |
|
if (Math.abs(cm.doc.scrollTop - val) < 2) return; |
|
cm.doc.scrollTop = val; |
|
if (!gecko) updateDisplaySimple(cm, {top: val}); |
|
if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; |
|
cm.display.scrollbars.setScrollTop(val); |
|
if (gecko) updateDisplaySimple(cm); |
|
startWorker(cm, 100); |
|
} |
|
// Sync scroller and scrollbar, ensure the gutter elements are |
|
// aligned. |
|
function setScrollLeft(cm, val, isScroller) { |
|
if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; |
|
val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); |
|
cm.doc.scrollLeft = val; |
|
alignHorizontally(cm); |
|
if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; |
|
cm.display.scrollbars.setScrollLeft(val); |
|
} |
|
|
|
// Since the delta values reported on mouse wheel events are |
|
// unstandardized between browsers and even browser versions, and |
|
// generally horribly unpredictable, this code starts by measuring |
|
// the scroll effect that the first few mouse wheel events have, |
|
// and, from that, detects the way it can convert deltas to pixel |
|
// offsets afterwards. |
|
// |
|
// The reason we want to know the amount a wheel event will scroll |
|
// is that it gives us a chance to update the display before the |
|
// actual scrolling happens, reducing flickering. |
|
|
|
var wheelSamples = 0, wheelPixelsPerUnit = null; |
|
// Fill in a browser-detected starting value on browsers where we |
|
// know one. These don't have to be accurate -- the result of them |
|
// being wrong would just be a slight flicker on the first wheel |
|
// scroll (if it is large enough). |
|
if (ie) wheelPixelsPerUnit = -.53; |
|
else if (gecko) wheelPixelsPerUnit = 15; |
|
else if (chrome) wheelPixelsPerUnit = -.7; |
|
else if (safari) wheelPixelsPerUnit = -1/3; |
|
|
|
var wheelEventDelta = function(e) { |
|
var dx = e.wheelDeltaX, dy = e.wheelDeltaY; |
|
if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; |
|
if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; |
|
else if (dy == null) dy = e.wheelDelta; |
|
return {x: dx, y: dy}; |
|
}; |
|
CodeMirror.wheelEventPixels = function(e) { |
|
var delta = wheelEventDelta(e); |
|
delta.x *= wheelPixelsPerUnit; |
|
delta.y *= wheelPixelsPerUnit; |
|
return delta; |
|
}; |
|
|
|
function onScrollWheel(cm, e) { |
|
var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; |
|
|
|
var display = cm.display, scroll = display.scroller; |
|
// Quit if there's nothing to scroll here |
|
if (!(dx && scroll.scrollWidth > scroll.clientWidth || |
|
dy && scroll.scrollHeight > scroll.clientHeight)) return; |
|
|
|
// Webkit browsers on OS X abort momentum scrolls when the target |
|
// of the scroll event is removed from the scrollable element. |
|
// This hack (see related code in patchDisplay) makes sure the |
|
// element is kept around. |
|
if (dy && mac && webkit) { |
|
outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { |
|
for (var i = 0; i < view.length; i++) { |
|
if (view[i].node == cur) { |
|
cm.display.currentWheelTarget = cur; |
|
break outer; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// On some browsers, horizontal scrolling will cause redraws to |
|
// happen before the gutter has been realigned, causing it to |
|
// wriggle around in a most unseemly way. When we have an |
|
// estimated pixels/delta value, we just handle horizontal |
|
// scrolling entirely here. It'll be slightly off from native, but |
|
// better than glitching out. |
|
if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { |
|
if (dy) |
|
setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); |
|
setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); |
|
e_preventDefault(e); |
|
display.wheelStartX = null; // Abort measurement, if in progress |
|
return; |
|
} |
|
|
|
// 'Project' the visible viewport to cover the area that is being |
|
// scrolled into view (if we know enough to estimate it). |
|
if (dy && wheelPixelsPerUnit != null) { |
|
var pixels = dy * wheelPixelsPerUnit; |
|
var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; |
|
if (pixels < 0) top = Math.max(0, top + pixels - 50); |
|
else bot = Math.min(cm.doc.height, bot + pixels + 50); |
|
updateDisplaySimple(cm, {top: top, bottom: bot}); |
|
} |
|
|
|
if (wheelSamples < 20) { |
|
if (display.wheelStartX == null) { |
|
display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; |
|
display.wheelDX = dx; display.wheelDY = dy; |
|
setTimeout(function() { |
|
if (display.wheelStartX == null) return; |
|
var movedX = scroll.scrollLeft - display.wheelStartX; |
|
var movedY = scroll.scrollTop - display.wheelStartY; |
|
var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || |
|
(movedX && display.wheelDX && movedX / display.wheelDX); |
|
display.wheelStartX = display.wheelStartY = null; |
|
if (!sample) return; |
|
wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); |
|
++wheelSamples; |
|
}, 200); |
|
} else { |
|
display.wheelDX += dx; display.wheelDY += dy; |
|
} |
|
} |
|
} |
|
|
|
// KEY EVENTS |
|
|
|
// Run a handler that was bound to a key. |
|
function doHandleBinding(cm, bound, dropShift) { |
|
if (typeof bound == "string") { |
|
bound = commands[bound]; |
|
if (!bound) return false; |
|
} |
|
// Ensure previous input has been read, so that the handler sees a |
|
// consistent view of the document |
|
cm.display.input.ensurePolled(); |
|
var prevShift = cm.display.shift, done = false; |
|
try { |
|
if (isReadOnly(cm)) cm.state.suppressEdits = true; |
|
if (dropShift) cm.display.shift = false; |
|
done = bound(cm) != Pass; |
|
} finally { |
|
cm.display.shift = prevShift; |
|
cm.state.suppressEdits = false; |
|
} |
|
return done; |
|
} |
|
|
|
function lookupKeyForEditor(cm, name, handle) { |
|
for (var i = 0; i < cm.state.keyMaps.length; i++) { |
|
var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); |
|
if (result) return result; |
|
} |
|
return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) |
|
|| lookupKey(name, cm.options.keyMap, handle, cm); |
|
} |
|
|
|
var stopSeq = new Delayed; |
|
function dispatchKey(cm, name, e, handle) { |
|
var seq = cm.state.keySeq; |
|
if (seq) { |
|
if (isModifierKey(name)) return "handled"; |
|
stopSeq.set(50, function() { |
|
if (cm.state.keySeq == seq) { |
|
cm.state.keySeq = null; |
|
cm.display.input.reset(); |
|
} |
|
}); |
|
name = seq + " " + name; |
|
} |
|
var result = lookupKeyForEditor(cm, name, handle); |
|
|
|
if (result == "multi") |
|
cm.state.keySeq = name; |
|
if (result == "handled") |
|
signalLater(cm, "keyHandled", cm, name, e); |
|
|
|
if (result == "handled" || result == "multi") { |
|
e_preventDefault(e); |
|
restartBlink(cm); |
|
} |
|
|
|
if (seq && !result && /\'$/.test(name)) { |
|
e_preventDefault(e); |
|
return true; |
|
} |
|
return !!result; |
|
} |
|
|
|
// Handle a key from the keydown event. |
|
function handleKeyBinding(cm, e) { |
|
var name = keyName(e, true); |
|
if (!name) return false; |
|
|
|
if (e.shiftKey && !cm.state.keySeq) { |
|
// First try to resolve full name (including 'Shift-'). Failing |
|
// that, see if there is a cursor-motion command (starting with |
|
// 'go') bound to the keyname without 'Shift-'. |
|
return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);}) |
|
|| dispatchKey(cm, name, e, function(b) { |
|
if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) |
|
return doHandleBinding(cm, b); |
|
}); |
|
} else { |
|
return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); }); |
|
} |
|
} |
|
|
|
// Handle a key from the keypress event |
|
function handleCharBinding(cm, e, ch) { |
|
return dispatchKey(cm, "'" + ch + "'", e, |
|
function(b) { return doHandleBinding(cm, b, true); }); |
|
} |
|
|
|
var lastStoppedKey = null; |
|
function onKeyDown(e) { |
|
var cm = this; |
|
cm.curOp.focus = activeElt(); |
|
if (signalDOMEvent(cm, e)) return; |
|
// IE does strange things with escape. |
|
if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; |
|
var code = e.keyCode; |
|
cm.display.shift = code == 16 || e.shiftKey; |
|
var handled = handleKeyBinding(cm, e); |
|
if (presto) { |
|
lastStoppedKey = handled ? code : null; |
|
// Opera has no cut event... we try to at least catch the key combo |
|
if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) |
|
cm.replaceSelection("", null, "cut"); |
|
} |
|
|
|
// Turn mouse into crosshair when Alt is held on Mac. |
|
if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) |
|
showCrossHair(cm); |
|
} |
|
|
|
function showCrossHair(cm) { |
|
var lineDiv = cm.display.lineDiv; |
|
addClass(lineDiv, "CodeMirror-crosshair"); |
|
|
|
function up(e) { |
|
if (e.keyCode == 18 || !e.altKey) { |
|
rmClass(lineDiv, "CodeMirror-crosshair"); |
|
off(document, "keyup", up); |
|
off(document, "mouseover", up); |
|
} |
|
} |
|
on(document, "keyup", up); |
|
on(document, "mouseover", up); |
|
} |
|
|
|
function onKeyUp(e) { |
|
if (e.keyCode == 16) this.doc.sel.shift = false; |
|
signalDOMEvent(this, e); |
|
} |
|
|
|
function onKeyPress(e) { |
|
var cm = this; |
|
if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; |
|
var keyCode = e.keyCode, charCode = e.charCode; |
|
if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} |
|
if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return; |
|
var ch = String.fromCharCode(charCode == null ? keyCode : charCode); |
|
if (handleCharBinding(cm, e, ch)) return; |
|
cm.display.input.onKeyPress(e); |
|
} |
|
|
|
// FOCUS/BLUR EVENTS |
|
|
|
function delayBlurEvent(cm) { |
|
cm.state.delayingBlurEvent = true; |
|
setTimeout(function() { |
|
if (cm.state.delayingBlurEvent) { |
|
cm.state.delayingBlurEvent = false; |
|
onBlur(cm); |
|
} |
|
}, 100); |
|
} |
|
|
|
function onFocus(cm) { |
|
if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false; |
|
|
|
if (cm.options.readOnly == "nocursor") return; |
|
if (!cm.state.focused) { |
|
signal(cm, "focus", cm); |
|
cm.state.focused = true; |
|
addClass(cm.display.wrapper, "CodeMirror-focused"); |
|
// This test prevents this from firing when a context |
|
// menu is closed (since the input reset would kill the |
|
// select-all detection hack) |
|
if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { |
|
cm.display.input.reset(); |
|
if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 |
|
} |
|
cm.display.input.receivedFocus(); |
|
} |
|
restartBlink(cm); |
|
} |
|
function onBlur(cm) { |
|
if (cm.state.delayingBlurEvent) return; |
|
|
|
if (cm.state.focused) { |
|
signal(cm, "blur", cm); |
|
cm.state.focused = false; |
|
rmClass(cm.display.wrapper, "CodeMirror-focused"); |
|
} |
|
clearInterval(cm.display.blinker); |
|
setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); |
|
} |
|
|
|
// CONTEXT MENU HANDLING |
|
|
|
// To make the context menu work, we need to briefly unhide the |
|
// textarea (making it as unobtrusive as possible) to let the |
|
// right-click take effect on it. |
|
function onContextMenu(cm, e) { |
|
if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; |
|
cm.display.input.onContextMenu(e); |
|
} |
|
|
|
function contextMenuInGutter(cm, e) { |
|
if (!hasHandler(cm, "gutterContextMenu")) return false; |
|
return gutterEvent(cm, e, "gutterContextMenu", false, signal); |
|
} |
|
|
|
// UPDATING |
|
|
|
// Compute the position of the end of a change (its 'to' property |
|
// refers to the pre-change end). |
|
var changeEnd = CodeMirror.changeEnd = function(change) { |
|
if (!change.text) return change.to; |
|
return Pos(change.from.line + change.text.length - 1, |
|
lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); |
|
}; |
|
|
|
// Adjust a position to refer to the post-change position of the |
|
// same text, or the end of the change if the change covers it. |
|
function adjustForChange(pos, change) { |
|
if (cmp(pos, change.from) < 0) return pos; |
|
if (cmp(pos, change.to) <= 0) return changeEnd(change); |
|
|
|
var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; |
|
if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; |
|
return Pos(line, ch); |
|
} |
|
|
|
function computeSelAfterChange(doc, change) { |
|
var out = []; |
|
for (var i = 0; i < doc.sel.ranges.length; i++) { |
|
var range = doc.sel.ranges[i]; |
|
out.push(new Range(adjustForChange(range.anchor, change), |
|
adjustForChange(range.head, change))); |
|
} |
|
return normalizeSelection(out, doc.sel.primIndex); |
|
} |
|
|
|
function offsetPos(pos, old, nw) { |
|
if (pos.line == old.line) |
|
return Pos(nw.line, pos.ch - old.ch + nw.ch); |
|
else |
|
return Pos(nw.line + (pos.line - old.line), pos.ch); |
|
} |
|
|
|
// Used by replaceSelections to allow moving the selection to the |
|
// start or around the replaced test. Hint may be "start" or "around". |
|
function computeReplacedSel(doc, changes, hint) { |
|
var out = []; |
|
var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; |
|
for (var i = 0; i < changes.length; i++) { |
|
var change = changes[i]; |
|
var from = offsetPos(change.from, oldPrev, newPrev); |
|
var to = offsetPos(changeEnd(change), oldPrev, newPrev); |
|
oldPrev = change.to; |
|
newPrev = to; |
|
if (hint == "around") { |
|
var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; |
|
out[i] = new Range(inv ? to : from, inv ? from : to); |
|
} else { |
|
out[i] = new Range(from, from); |
|
} |
|
} |
|
return new Selection(out, doc.sel.primIndex); |
|
} |
|
|
|
// Allow "beforeChange" event handlers to influence a change |
|
function filterChange(doc, change, update) { |
|
var obj = { |
|
canceled: false, |
|
from: change.from, |
|
to: change.to, |
|
text: change.text, |
|
origin: change.origin, |
|
cancel: function() { this.canceled = true; } |
|
}; |
|
if (update) obj.update = function(from, to, text, origin) { |
|
if (from) this.from = clipPos(doc, from); |
|
if (to) this.to = clipPos(doc, to); |
|
if (text) this.text = text; |
|
if (origin !== undefined) this.origin = origin; |
|
}; |
|
signal(doc, "beforeChange", doc, obj); |
|
if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); |
|
|
|
if (obj.canceled) return null; |
|
return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; |
|
} |
|
|
|
// Apply a change to a document, and add it to the document's |
|
// history, and propagating it to all linked documents. |
|
function makeChange(doc, change, ignoreReadOnly) { |
|
if (doc.cm) { |
|
if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); |
|
if (doc.cm.state.suppressEdits) return; |
|
} |
|
|
|
if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { |
|
change = filterChange(doc, change, true); |
|
if (!change) return; |
|
} |
|
|
|
// Possibly split or suppress the update based on the presence |
|
// of read-only spans in its range. |
|
var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); |
|
if (split) { |
|
for (var i = split.length - 1; i >= 0; --i) |
|
makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); |
|
} else { |
|
makeChangeInner(doc, change); |
|
} |
|
} |
|
|
|
function makeChangeInner(doc, change) { |
|
if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; |
|
var selAfter = computeSelAfterChange(doc, change); |
|
addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); |
|
|
|
makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); |
|
var rebased = []; |
|
|
|
linkedDocs(doc, function(doc, sharedHist) { |
|
if (!sharedHist && indexOf(rebased, doc.history) == -1) { |
|
rebaseHist(doc.history, change); |
|
rebased.push(doc.history); |
|
} |
|
makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); |
|
}); |
|
} |
|
|
|
// Revert a change stored in a document's history. |
|
function makeChangeFromHistory(doc, type, allowSelectionOnly) { |
|
if (doc.cm && doc.cm.state.suppressEdits) return; |
|
|
|
var hist = doc.history, event, selAfter = doc.sel; |
|
var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; |
|
|
|
// Verify that there is a useable event (so that ctrl-z won't |
|
// needlessly clear selection events) |
|
for (var i = 0; i < source.length; i++) { |
|
event = source[i]; |
|
if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) |
|
break; |
|
} |
|
if (i == source.length) return; |
|
hist.lastOrigin = hist.lastSelOrigin = null; |
|
|
|
for (;;) { |
|
event = source.pop(); |
|
if (event.ranges) { |
|
pushSelectionToHistory(event, dest); |
|
if (allowSelectionOnly && !event.equals(doc.sel)) { |
|
setSelection(doc, event, {clearRedo: false}); |
|
return; |
|
} |
|
selAfter = event; |
|
} |
|
else break; |
|
} |
|
|
|
// Build up a reverse change object to add to the opposite history |
|
// stack (redo when undoing, and vice versa). |
|
var antiChanges = []; |
|
pushSelectionToHistory(selAfter, dest); |
|
dest.push({changes: antiChanges, generation: hist.generation}); |
|
hist.generation = event.generation || ++hist.maxGeneration; |
|
|
|
var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); |
|
|
|
for (var i = event.changes.length - 1; i >= 0; --i) { |
|
var change = event.changes[i]; |
|
change.origin = type; |
|
if (filter && !filterChange(doc, change, false)) { |
|
source.length = 0; |
|
return; |
|
} |
|
|
|
antiChanges.push(historyChangeFromChange(doc, change)); |
|
|
|
var after = i ? computeSelAfterChange(doc, change) : lst(source); |
|
makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); |
|
if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); |
|
var rebased = []; |
|
|
|
// Propagate to the linked documents |
|
linkedDocs(doc, function(doc, sharedHist) { |
|
if (!sharedHist && indexOf(rebased, doc.history) == -1) { |
|
rebaseHist(doc.history, change); |
|
rebased.push(doc.history); |
|
} |
|
makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); |
|
}); |
|
} |
|
} |
|
|
|
// Sub-views need their line numbers shifted when text is added |
|
// above or below them in the parent document. |
|
function shiftDoc(doc, distance) { |
|
if (distance == 0) return; |
|
doc.first += distance; |
|
doc.sel = new Selection(map(doc.sel.ranges, function(range) { |
|
return new Range(Pos(range.anchor.line + distance, range.anchor.ch), |
|
Pos(range.head.line + distance, range.head.ch)); |
|
}), doc.sel.primIndex); |
|
if (doc.cm) { |
|
regChange(doc.cm, doc.first, doc.first - distance, distance); |
|
for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) |
|
regLineChange(doc.cm, l, "gutter"); |
|
} |
|
} |
|
|
|
// More lower-level change function, handling only a single document |
|
// (not linked ones). |
|
function makeChangeSingleDoc(doc, change, selAfter, spans) { |
|
if (doc.cm && !doc.cm.curOp) |
|
return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); |
|
|
|
if (change.to.line < doc.first) { |
|
shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); |
|
return; |
|
} |
|
if (change.from.line > doc.lastLine()) return; |
|
|
|
// Clip the change to the size of this doc |
|
if (change.from.line < doc.first) { |
|
var shift = change.text.length - 1 - (doc.first - change.from.line); |
|
shiftDoc(doc, shift); |
|
change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), |
|
text: [lst(change.text)], origin: change.origin}; |
|
} |
|
var last = doc.lastLine(); |
|
if (change.to.line > last) { |
|
change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), |
|
text: [change.text[0]], origin: change.origin}; |
|
} |
|
|
|
change.removed = getBetween(doc, change.from, change.to); |
|
|
|
if (!selAfter) selAfter = computeSelAfterChange(doc, change); |
|
if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); |
|
else updateDoc(doc, change, spans); |
|
setSelectionNoUndo(doc, selAfter, sel_dontScroll); |
|
} |
|
|
|
// Handle the interaction of a change to a document with the editor |
|
// that this document is part of. |
|
function makeChangeSingleDocInEditor(cm, change, spans) { |
|
var doc = cm.doc, display = cm.display, from = change.from, to = change.to; |
|
|
|
var recomputeMaxLength = false, checkWidthStart = from.line; |
|
if (!cm.options.lineWrapping) { |
|
checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); |
|
doc.iter(checkWidthStart, to.line + 1, function(line) { |
|
if (line == display.maxLine) { |
|
recomputeMaxLength = true; |
|
return true; |
|
} |
|
}); |
|
} |
|
|
|
if (doc.sel.contains(change.from, change.to) > -1) |
|
signalCursorActivity(cm); |
|
|
|
updateDoc(doc, change, spans, estimateHeight(cm)); |
|
|
|
if (!cm.options.lineWrapping) { |
|
doc.iter(checkWidthStart, from.line + change.text.length, function(line) { |
|
var len = lineLength(line); |
|
if (len > display.maxLineLength) { |
|
display.maxLine = line; |
|
display.maxLineLength = len; |
|
display.maxLineChanged = true; |
|
recomputeMaxLength = false; |
|
} |
|
}); |
|
if (recomputeMaxLength) cm.curOp.updateMaxLine = true; |
|
} |
|
|
|
// Adjust frontier, schedule worker |
|
doc.frontier = Math.min(doc.frontier, from.line); |
|
startWorker(cm, 400); |
|
|
|
var lendiff = change.text.length - (to.line - from.line) - 1; |
|
// Remember that these lines changed, for updating the display |
|
if (change.full) |
|
regChange(cm); |
|
else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) |
|
regLineChange(cm, from.line, "text"); |
|
else |
|
regChange(cm, from.line, to.line + 1, lendiff); |
|
|
|
var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); |
|
if (changeHandler || changesHandler) { |
|
var obj = { |
|
from: from, to: to, |
|
text: change.text, |
|
removed: change.removed, |
|
origin: change.origin |
|
}; |
|
if (changeHandler) signalLater(cm, "change", cm, obj); |
|
if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); |
|
} |
|
cm.display.selForContextMenu = null; |
|
} |
|
|
|
function replaceRange(doc, code, from, to, origin) { |
|
if (!to) to = from; |
|
if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } |
|
if (typeof code == "string") code = splitLines(code); |
|
makeChange(doc, {from: from, to: to, text: code, origin: origin}); |
|
} |
|
|
|
// SCROLLING THINGS INTO VIEW |
|
|
|
// If an editor sits on the top or bottom of the window, partially |
|
// scrolled out of view, this ensures that the cursor is visible. |
|
function maybeScrollWindow(cm, coords) { |
|
if (signalDOMEvent(cm, "scrollCursorIntoView")) return; |
|
|
|
var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; |
|
if (coords.top + box.top < 0) doScroll = true; |
|
else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; |
|
if (doScroll != null && !phantom) { |
|
var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + |
|
(coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + |
|
(coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " + |
|
coords.left + "px; width: 2px;"); |
|
cm.display.lineSpace.appendChild(scrollNode); |
|
scrollNode.scrollIntoView(doScroll); |
|
cm.display.lineSpace.removeChild(scrollNode); |
|
} |
|
} |
|
|
|
// Scroll a given position into view (immediately), verifying that |
|
// it actually became visible (as line heights are accurately |
|
// measured, the position of something may 'drift' during drawing). |
|
function scrollPosIntoView(cm, pos, end, margin) { |
|
if (margin == null) margin = 0; |
|
for (var limit = 0; limit < 5; limit++) { |
|
var changed = false, coords = cursorCoords(cm, pos); |
|
var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); |
|
var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), |
|
Math.min(coords.top, endCoords.top) - margin, |
|
Math.max(coords.left, endCoords.left), |
|
Math.max(coords.bottom, endCoords.bottom) + margin); |
|
var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; |
|
if (scrollPos.scrollTop != null) { |
|
setScrollTop(cm, scrollPos.scrollTop); |
|
if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; |
|
} |
|
if (scrollPos.scrollLeft != null) { |
|
setScrollLeft(cm, scrollPos.scrollLeft); |
|
if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; |
|
} |
|
if (!changed) break; |
|
} |
|
return coords; |
|
} |
|
|
|
// Scroll a given set of coordinates into view (immediately). |
|
function scrollIntoView(cm, x1, y1, x2, y2) { |
|
var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); |
|
if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); |
|
if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); |
|
} |
|
|
|
// Calculate a new scroll position needed to scroll the given |
|
// rectangle into view. Returns an object with scrollTop and |
|
// scrollLeft properties. When these are undefined, the |
|
// vertical/horizontal position does not need to be adjusted. |
|
function calculateScrollPos(cm, x1, y1, x2, y2) { |
|
var display = cm.display, snapMargin = textHeight(cm.display); |
|
if (y1 < 0) y1 = 0; |
|
var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; |
|
var screen = displayHeight(cm), result = {}; |
|
if (y2 - y1 > screen) y2 = y1 + screen; |
|
var docBottom = cm.doc.height + paddingVert(display); |
|
var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; |
|
if (y1 < screentop) { |
|
result.scrollTop = atTop ? 0 : y1; |
|
} else if (y2 > screentop + screen) { |
|
var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); |
|
if (newTop != screentop) result.scrollTop = newTop; |
|
} |
|
|
|
var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; |
|
var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); |
|
var tooWide = x2 - x1 > screenw; |
|
if (tooWide) x2 = x1 + screenw; |
|
if (x1 < 10) |
|
result.scrollLeft = 0; |
|
else if (x1 < screenleft) |
|
result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); |
|
else if (x2 > screenw + screenleft - 3) |
|
result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; |
|
return result; |
|
} |
|
|
|
// Store a relative adjustment to the scroll position in the current |
|
// operation (to be applied when the operation finishes). |
|
function addToScrollPos(cm, left, top) { |
|
if (left != null || top != null) resolveScrollToPos(cm); |
|
if (left != null) |
|
cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; |
|
if (top != null) |
|
cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; |
|
} |
|
|
|
// Make sure that at the end of the operation the current cursor is |
|
// shown. |
|
function ensureCursorVisible(cm) { |
|
resolveScrollToPos(cm); |
|
var cur = cm.getCursor(), from = cur, to = cur; |
|
if (!cm.options.lineWrapping) { |
|
from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; |
|
to = Pos(cur.line, cur.ch + 1); |
|
} |
|
cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; |
|
} |
|
|
|
// When an operation has its scrollToPos property set, and another |
|
// scroll action is applied before the end of the operation, this |
|
// 'simulates' scrolling that position into view in a cheap way, so |
|
// that the effect of intermediate scroll commands is not ignored. |
|
function resolveScrollToPos(cm) { |
|
var range = cm.curOp.scrollToPos; |
|
if (range) { |
|
cm.curOp.scrollToPos = null; |
|
var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); |
|
var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), |
|
Math.min(from.top, to.top) - range.margin, |
|
Math.max(from.right, to.right), |
|
Math.max(from.bottom, to.bottom) + range.margin); |
|
cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); |
|
} |
|
} |
|
|
|
// API UTILITIES |
|
|
|
// Indent the given line. The how parameter can be "smart", |
|
// "add"/null, "subtract", or "prev". When aggressive is false |
|
// (typically set to true for forced single-line indents), empty |
|
// lines are not indented, and places where the mode returns Pass |
|
// are left alone. |
|
function indentLine(cm, n, how, aggressive) { |
|
var doc = cm.doc, state; |
|
if (how == null) how = "add"; |
|
if (how == "smart") { |
|
// Fall back to "prev" when the mode doesn't have an indentation |
|
// method. |
|
if (!doc.mode.indent) how = "prev"; |
|
else state = getStateBefore(cm, n); |
|
} |
|
|
|
var tabSize = cm.options.tabSize; |
|
var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); |
|
if (line.stateAfter) line.stateAfter = null; |
|
var curSpaceString = line.text.match(/^\s*/)[0], indentation; |
|
if (!aggressive && !/\S/.test(line.text)) { |
|
indentation = 0; |
|
how = "not"; |
|
} else if (how == "smart") { |
|
indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); |
|
if (indentation == Pass || indentation > 150) { |
|
if (!aggressive) return; |
|
how = "prev"; |
|
} |
|
} |
|
if (how == "prev") { |
|
if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); |
|
else indentation = 0; |
|
} else if (how == "add") { |
|
indentation = curSpace + cm.options.indentUnit; |
|
} else if (how == "subtract") { |
|
indentation = curSpace - cm.options.indentUnit; |
|
} else if (typeof how == "number") { |
|
indentation = curSpace + how; |
|
} |
|
indentation = Math.max(0, indentation); |
|
|
|
var indentString = "", pos = 0; |
|
if (cm.options.indentWithTabs) |
|
for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} |
|
if (pos < indentation) indentString += spaceStr(indentation - pos); |
|
|
|
if (indentString != curSpaceString) { |
|
replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); |
|
line.stateAfter = null; |
|
return true; |
|
} else { |
|
// Ensure that, if the cursor was in the whitespace at the start |
|
// of the line, it is moved to the end of that space. |
|
for (var i = 0; i < doc.sel.ranges.length; i++) { |
|
var range = doc.sel.ranges[i]; |
|
if (range.head.line == n && range.head.ch < curSpaceString.length) { |
|
var pos = Pos(n, curSpaceString.length); |
|
replaceOneSelection(doc, i, new Range(pos, pos)); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Utility for applying a change to a line by handle or number, |
|
// returning the number and optionally registering the line as |
|
// changed. |
|
function changeLine(doc, handle, changeType, op) { |
|
var no = handle, line = handle; |
|
if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); |
|
else no = lineNo(handle); |
|
if (no == null) return null; |
|
if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); |
|
return line; |
|
} |
|
|
|
// Helper for deleting text near the selection(s), used to implement |
|
// backspace, delete, and similar functionality. |
|
function deleteNearSelection(cm, compute) { |
|
var ranges = cm.doc.sel.ranges, kill = []; |
|
// Build up a set of ranges to kill first, merging overlapping |
|
// ranges. |
|
for (var i = 0; i < ranges.length; i++) { |
|
var toKill = compute(ranges[i]); |
|
while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { |
|
var replaced = kill.pop(); |
|
if (cmp(replaced.from, toKill.from) < 0) { |
|
toKill.from = replaced.from; |
|
break; |
|
} |
|
} |
|
kill.push(toKill); |
|
} |
|
// Next, remove those actual ranges. |
|
runInOp(cm, function() { |
|
for (var i = kill.length - 1; i >= 0; i--) |
|
replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); |
|
ensureCursorVisible(cm); |
|
}); |
|
} |
|
|
|
// Used for horizontal relative motion. Dir is -1 or 1 (left or |
|
// right), unit can be "char", "column" (like char, but doesn't |
|
// cross line boundaries), "word" (across next word), or "group" (to |
|
// the start of next group of word or non-word-non-whitespace |
|
// chars). The visually param controls whether, in right-to-left |
|
// text, direction 1 means to move towards the next index in the |
|
// string, or towards the character to the right of the current |
|
// position. The resulting position will have a hitSide=true |
|
// property if it reached the end of the document. |
|
function findPosH(doc, pos, dir, unit, visually) { |
|
var line = pos.line, ch = pos.ch, origDir = dir; |
|
var lineObj = getLine(doc, line); |
|
var possible = true; |
|
function findNextLine() { |
|
var l = line + dir; |
|
if (l < doc.first || l >= doc.first + doc.size) return (possible = false); |
|
line = l; |
|
return lineObj = getLine(doc, l); |
|
} |
|
function moveOnce(boundToLine) { |
|
var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); |
|
if (next == null) { |
|
if (!boundToLine && findNextLine()) { |
|
if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); |
|
else ch = dir < 0 ? lineObj.text.length : 0; |
|
} else return (possible = false); |
|
} else ch = next; |
|
return true; |
|
} |
|
|
|
if (unit == "char") moveOnce(); |
|
else if (unit == "column") moveOnce(true); |
|
else if (unit == "word" || unit == "group") { |
|
var sawType = null, group = unit == "group"; |
|
var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); |
|
for (var first = true;; first = false) { |
|
if (dir < 0 && !moveOnce(!first)) break; |
|
var cur = lineObj.text.charAt(ch) || "\n"; |
|
var type = isWordChar(cur, helper) ? "w" |
|
: group && cur == "\n" ? "n" |
|
: !group || /\s/.test(cur) ? null |
|
: "p"; |
|
if (group && !first && !type) type = "s"; |
|
if (sawType && sawType != type) { |
|
if (dir < 0) {dir = 1; moveOnce();} |
|
break; |
|
} |
|
|
|
if (type) sawType = type; |
|
if (dir > 0 && !moveOnce(!first)) break; |
|
} |
|
} |
|
var result = skipAtomic(doc, Pos(line, ch), origDir, true); |
|
if (!possible) result.hitSide = true; |
|
return result; |
|
} |
|
|
|
// For relative vertical movement. Dir may be -1 or 1. Unit can be |
|
// "page" or "line". The resulting position will have a hitSide=true |
|
// property if it reached the end of the document. |
|
function findPosV(cm, pos, dir, unit) { |
|
var doc = cm.doc, x = pos.left, y; |
|
if (unit == "page") { |
|
var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); |
|
y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); |
|
} else if (unit == "line") { |
|
y = dir > 0 ? pos.bottom + 3 : pos.top - 3; |
|
} |
|
for (;;) { |
|
var target = coordsChar(cm, x, y); |
|
if (!target.outside) break; |
|
if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } |
|
y += dir * 5; |
|
} |
|
return target; |
|
} |
|
|
|
// EDITOR METHODS |
|
|
|
// The publicly visible API. Note that methodOp(f) means |
|
// 'wrap f in an operation, performed on its `this` parameter'. |
|
|
|
// This is not the complete set of editor methods. Most of the |
|
// methods defined on the Doc type are also injected into |
|
// CodeMirror.prototype, for backwards compatibility and |
|
// convenience. |
|
|
|
CodeMirror.prototype = { |
|
constructor: CodeMirror, |
|
focus: function(){window.focus(); this.display.input.focus();}, |
|
|
|
setOption: function(option, value) { |
|
var options = this.options, old = options[option]; |
|
if (options[option] == value && option != "mode") return; |
|
options[option] = value; |
|
if (optionHandlers.hasOwnProperty(option)) |
|
operation(this, optionHandlers[option])(this, value, old); |
|
}, |
|
|
|
getOption: function(option) {return this.options[option];}, |
|
getDoc: function() {return this.doc;}, |
|
|
|
addKeyMap: function(map, bottom) { |
|
this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); |
|
}, |
|
removeKeyMap: function(map) { |
|
var maps = this.state.keyMaps; |
|
for (var i = 0; i < maps.length; ++i) |
|
if (maps[i] == map || maps[i].name == map) { |
|
maps.splice(i, 1); |
|
return true; |
|
} |
|
}, |
|
|
|
addOverlay: methodOp(function(spec, options) { |
|
var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); |
|
if (mode.startState) throw new Error("Overlays may not be stateful."); |
|
this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); |
|
this.state.modeGen++; |
|
regChange(this); |
|
}), |
|
removeOverlay: methodOp(function(spec) { |
|
var overlays = this.state.overlays; |
|
for (var i = 0; i < overlays.length; ++i) { |
|
var cur = overlays[i].modeSpec; |
|
if (cur == spec || typeof spec == "string" && cur.name == spec) { |
|
overlays.splice(i, 1); |
|
this.state.modeGen++; |
|
regChange(this); |
|
return; |
|
} |
|
} |
|
}), |
|
|
|
indentLine: methodOp(function(n, dir, aggressive) { |
|
if (typeof dir != "string" && typeof dir != "number") { |
|
if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; |
|
else dir = dir ? "add" : "subtract"; |
|
} |
|
if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); |
|
}), |
|
indentSelection: methodOp(function(how) { |
|
var ranges = this.doc.sel.ranges, end = -1; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var range = ranges[i]; |
|
if (!range.empty()) { |
|
var from = range.from(), to = range.to(); |
|
var start = Math.max(end, from.line); |
|
end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; |
|
for (var j = start; j < end; ++j) |
|
indentLine(this, j, how); |
|
var newRanges = this.doc.sel.ranges; |
|
if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) |
|
replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); |
|
} else if (range.head.line > end) { |
|
indentLine(this, range.head.line, how, true); |
|
end = range.head.line; |
|
if (i == this.doc.sel.primIndex) ensureCursorVisible(this); |
|
} |
|
} |
|
}), |
|
|
|
// Fetch the parser token for a given character. Useful for hacks |
|
// that want to inspect the mode state (say, for completion). |
|
getTokenAt: function(pos, precise) { |
|
return takeToken(this, pos, precise); |
|
}, |
|
|
|
getLineTokens: function(line, precise) { |
|
return takeToken(this, Pos(line), precise, true); |
|
}, |
|
|
|
getTokenTypeAt: function(pos) { |
|
pos = clipPos(this.doc, pos); |
|
var styles = getLineStyles(this, getLine(this.doc, pos.line)); |
|
var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; |
|
var type; |
|
if (ch == 0) type = styles[2]; |
|
else for (;;) { |
|
var mid = (before + after) >> 1; |
|
if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; |
|
else if (styles[mid * 2 + 1] < ch) before = mid + 1; |
|
else { type = styles[mid * 2 + 2]; break; } |
|
} |
|
var cut = type ? type.indexOf("cm-overlay ") : -1; |
|
return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); |
|
}, |
|
|
|
getModeAt: function(pos) { |
|
var mode = this.doc.mode; |
|
if (!mode.innerMode) return mode; |
|
return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; |
|
}, |
|
|
|
getHelper: function(pos, type) { |
|
return this.getHelpers(pos, type)[0]; |
|
}, |
|
|
|
getHelpers: function(pos, type) { |
|
var found = []; |
|
if (!helpers.hasOwnProperty(type)) return found; |
|
var help = helpers[type], mode = this.getModeAt(pos); |
|
if (typeof mode[type] == "string") { |
|
if (help[mode[type]]) found.push(help[mode[type]]); |
|
} else if (mode[type]) { |
|
for (var i = 0; i < mode[type].length; i++) { |
|
var val = help[mode[type][i]]; |
|
if (val) found.push(val); |
|
} |
|
} else if (mode.helperType && help[mode.helperType]) { |
|
found.push(help[mode.helperType]); |
|
} else if (help[mode.name]) { |
|
found.push(help[mode.name]); |
|
} |
|
for (var i = 0; i < help._global.length; i++) { |
|
var cur = help._global[i]; |
|
if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) |
|
found.push(cur.val); |
|
} |
|
return found; |
|
}, |
|
|
|
getStateAfter: function(line, precise) { |
|
var doc = this.doc; |
|
line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); |
|
return getStateBefore(this, line + 1, precise); |
|
}, |
|
|
|
cursorCoords: function(start, mode) { |
|
var pos, range = this.doc.sel.primary(); |
|
if (start == null) pos = range.head; |
|
else if (typeof start == "object") pos = clipPos(this.doc, start); |
|
else pos = start ? range.from() : range.to(); |
|
return cursorCoords(this, pos, mode || "page"); |
|
}, |
|
|
|
charCoords: function(pos, mode) { |
|
return charCoords(this, clipPos(this.doc, pos), mode || "page"); |
|
}, |
|
|
|
coordsChar: function(coords, mode) { |
|
coords = fromCoordSystem(this, coords, mode || "page"); |
|
return coordsChar(this, coords.left, coords.top); |
|
}, |
|
|
|
lineAtHeight: function(height, mode) { |
|
height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; |
|
return lineAtHeight(this.doc, height + this.display.viewOffset); |
|
}, |
|
heightAtLine: function(line, mode) { |
|
var end = false, lineObj; |
|
if (typeof line == "number") { |
|
var last = this.doc.first + this.doc.size - 1; |
|
if (line < this.doc.first) line = this.doc.first; |
|
else if (line > last) { line = last; end = true; } |
|
lineObj = getLine(this.doc, line); |
|
} else { |
|
lineObj = line; |
|
} |
|
return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + |
|
(end ? this.doc.height - heightAtLine(lineObj) : 0); |
|
}, |
|
|
|
defaultTextHeight: function() { return textHeight(this.display); }, |
|
defaultCharWidth: function() { return charWidth(this.display); }, |
|
|
|
setGutterMarker: methodOp(function(line, gutterID, value) { |
|
return changeLine(this.doc, line, "gutter", function(line) { |
|
var markers = line.gutterMarkers || (line.gutterMarkers = {}); |
|
markers[gutterID] = value; |
|
if (!value && isEmpty(markers)) line.gutterMarkers = null; |
|
return true; |
|
}); |
|
}), |
|
|
|
clearGutter: methodOp(function(gutterID) { |
|
var cm = this, doc = cm.doc, i = doc.first; |
|
doc.iter(function(line) { |
|
if (line.gutterMarkers && line.gutterMarkers[gutterID]) { |
|
line.gutterMarkers[gutterID] = null; |
|
regLineChange(cm, i, "gutter"); |
|
if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; |
|
} |
|
++i; |
|
}); |
|
}), |
|
|
|
lineInfo: function(line) { |
|
if (typeof line == "number") { |
|
if (!isLine(this.doc, line)) return null; |
|
var n = line; |
|
line = getLine(this.doc, line); |
|
if (!line) return null; |
|
} else { |
|
var n = lineNo(line); |
|
if (n == null) return null; |
|
} |
|
return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, |
|
textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, |
|
widgets: line.widgets}; |
|
}, |
|
|
|
getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, |
|
|
|
addWidget: function(pos, node, scroll, vert, horiz) { |
|
var display = this.display; |
|
pos = cursorCoords(this, clipPos(this.doc, pos)); |
|
var top = pos.bottom, left = pos.left; |
|
node.style.position = "absolute"; |
|
node.setAttribute("cm-ignore-events", "true"); |
|
this.display.input.setUneditable(node); |
|
display.sizer.appendChild(node); |
|
if (vert == "over") { |
|
top = pos.top; |
|
} else if (vert == "above" || vert == "near") { |
|
var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), |
|
hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); |
|
// Default to positioning above (if specified and possible); otherwise default to positioning below |
|
if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) |
|
top = pos.top - node.offsetHeight; |
|
else if (pos.bottom + node.offsetHeight <= vspace) |
|
top = pos.bottom; |
|
if (left + node.offsetWidth > hspace) |
|
left = hspace - node.offsetWidth; |
|
} |
|
node.style.top = top + "px"; |
|
node.style.left = node.style.right = ""; |
|
if (horiz == "right") { |
|
left = display.sizer.clientWidth - node.offsetWidth; |
|
node.style.right = "0px"; |
|
} else { |
|
if (horiz == "left") left = 0; |
|
else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; |
|
node.style.left = left + "px"; |
|
} |
|
if (scroll) |
|
scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); |
|
}, |
|
|
|
triggerOnKeyDown: methodOp(onKeyDown), |
|
triggerOnKeyPress: methodOp(onKeyPress), |
|
triggerOnKeyUp: onKeyUp, |
|
|
|
execCommand: function(cmd) { |
|
if (commands.hasOwnProperty(cmd)) |
|
return commands[cmd](this); |
|
}, |
|
|
|
triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), |
|
|
|
findPosH: function(from, amount, unit, visually) { |
|
var dir = 1; |
|
if (amount < 0) { dir = -1; amount = -amount; } |
|
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { |
|
cur = findPosH(this.doc, cur, dir, unit, visually); |
|
if (cur.hitSide) break; |
|
} |
|
return cur; |
|
}, |
|
|
|
moveH: methodOp(function(dir, unit) { |
|
var cm = this; |
|
cm.extendSelectionsBy(function(range) { |
|
if (cm.display.shift || cm.doc.extend || range.empty()) |
|
return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); |
|
else |
|
return dir < 0 ? range.from() : range.to(); |
|
}, sel_move); |
|
}), |
|
|
|
deleteH: methodOp(function(dir, unit) { |
|
var sel = this.doc.sel, doc = this.doc; |
|
if (sel.somethingSelected()) |
|
doc.replaceSelection("", null, "+delete"); |
|
else |
|
deleteNearSelection(this, function(range) { |
|
var other = findPosH(doc, range.head, dir, unit, false); |
|
return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; |
|
}); |
|
}), |
|
|
|
findPosV: function(from, amount, unit, goalColumn) { |
|
var dir = 1, x = goalColumn; |
|
if (amount < 0) { dir = -1; amount = -amount; } |
|
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { |
|
var coords = cursorCoords(this, cur, "div"); |
|
if (x == null) x = coords.left; |
|
else coords.left = x; |
|
cur = findPosV(this, coords, dir, unit); |
|
if (cur.hitSide) break; |
|
} |
|
return cur; |
|
}, |
|
|
|
moveV: methodOp(function(dir, unit) { |
|
var cm = this, doc = this.doc, goals = []; |
|
var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); |
|
doc.extendSelectionsBy(function(range) { |
|
if (collapse) |
|
return dir < 0 ? range.from() : range.to(); |
|
var headPos = cursorCoords(cm, range.head, "div"); |
|
if (range.goalColumn != null) headPos.left = range.goalColumn; |
|
goals.push(headPos.left); |
|
var pos = findPosV(cm, headPos, dir, unit); |
|
if (unit == "page" && range == doc.sel.primary()) |
|
addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); |
|
return pos; |
|
}, sel_move); |
|
if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) |
|
doc.sel.ranges[i].goalColumn = goals[i]; |
|
}), |
|
|
|
// Find the word at the given position (as returned by coordsChar). |
|
findWordAt: function(pos) { |
|
var doc = this.doc, line = getLine(doc, pos.line).text; |
|
var start = pos.ch, end = pos.ch; |
|
if (line) { |
|
var helper = this.getHelper(pos, "wordChars"); |
|
if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; |
|
var startChar = line.charAt(start); |
|
var check = isWordChar(startChar, helper) |
|
? function(ch) { return isWordChar(ch, helper); } |
|
: /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} |
|
: function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; |
|
while (start > 0 && check(line.charAt(start - 1))) --start; |
|
while (end < line.length && check(line.charAt(end))) ++end; |
|
} |
|
return new Range(Pos(pos.line, start), Pos(pos.line, end)); |
|
}, |
|
|
|
toggleOverwrite: function(value) { |
|
if (value != null && value == this.state.overwrite) return; |
|
if (this.state.overwrite = !this.state.overwrite) |
|
addClass(this.display.cursorDiv, "CodeMirror-overwrite"); |
|
else |
|
rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); |
|
|
|
signal(this, "overwriteToggle", this, this.state.overwrite); |
|
}, |
|
hasFocus: function() { return this.display.input.getField() == activeElt(); }, |
|
|
|
scrollTo: methodOp(function(x, y) { |
|
if (x != null || y != null) resolveScrollToPos(this); |
|
if (x != null) this.curOp.scrollLeft = x; |
|
if (y != null) this.curOp.scrollTop = y; |
|
}), |
|
getScrollInfo: function() { |
|
var scroller = this.display.scroller; |
|
return {left: scroller.scrollLeft, top: scroller.scrollTop, |
|
height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, |
|
width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, |
|
clientHeight: displayHeight(this), clientWidth: displayWidth(this)}; |
|
}, |
|
|
|
scrollIntoView: methodOp(function(range, margin) { |
|
if (range == null) { |
|
range = {from: this.doc.sel.primary().head, to: null}; |
|
if (margin == null) margin = this.options.cursorScrollMargin; |
|
} else if (typeof range == "number") { |
|
range = {from: Pos(range, 0), to: null}; |
|
} else if (range.from == null) { |
|
range = {from: range, to: null}; |
|
} |
|
if (!range.to) range.to = range.from; |
|
range.margin = margin || 0; |
|
|
|
if (range.from.line != null) { |
|
resolveScrollToPos(this); |
|
this.curOp.scrollToPos = range; |
|
} else { |
|
var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), |
|
Math.min(range.from.top, range.to.top) - range.margin, |
|
Math.max(range.from.right, range.to.right), |
|
Math.max(range.from.bottom, range.to.bottom) + range.margin); |
|
this.scrollTo(sPos.scrollLeft, sPos.scrollTop); |
|
} |
|
}), |
|
|
|
setSize: methodOp(function(width, height) { |
|
var cm = this; |
|
function interpret(val) { |
|
return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; |
|
} |
|
if (width != null) cm.display.wrapper.style.width = interpret(width); |
|
if (height != null) cm.display.wrapper.style.height = interpret(height); |
|
if (cm.options.lineWrapping) clearLineMeasurementCache(this); |
|
var lineNo = cm.display.viewFrom; |
|
cm.doc.iter(lineNo, cm.display.viewTo, function(line) { |
|
if (line.widgets) for (var i = 0; i < line.widgets.length; i++) |
|
if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } |
|
++lineNo; |
|
}); |
|
cm.curOp.forceUpdate = true; |
|
signal(cm, "refresh", this); |
|
}), |
|
|
|
operation: function(f){return runInOp(this, f);}, |
|
|
|
refresh: methodOp(function() { |
|
var oldHeight = this.display.cachedTextHeight; |
|
regChange(this); |
|
this.curOp.forceUpdate = true; |
|
clearCaches(this); |
|
this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); |
|
updateGutterSpace(this); |
|
if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) |
|
estimateLineHeights(this); |
|
signal(this, "refresh", this); |
|
}), |
|
|
|
swapDoc: methodOp(function(doc) { |
|
var old = this.doc; |
|
old.cm = null; |
|
attachDoc(this, doc); |
|
clearCaches(this); |
|
this.display.input.reset(); |
|
this.scrollTo(doc.scrollLeft, doc.scrollTop); |
|
this.curOp.forceScroll = true; |
|
signalLater(this, "swapDoc", this, old); |
|
return old; |
|
}), |
|
|
|
getInputField: function(){return this.display.input.getField();}, |
|
getWrapperElement: function(){return this.display.wrapper;}, |
|
getScrollerElement: function(){return this.display.scroller;}, |
|
getGutterElement: function(){return this.display.gutters;} |
|
}; |
|
eventMixin(CodeMirror); |
|
|
|
// OPTION DEFAULTS |
|
|
|
// The default configuration options. |
|
var defaults = CodeMirror.defaults = {}; |
|
// Functions to run when options are changed. |
|
var optionHandlers = CodeMirror.optionHandlers = {}; |
|
|
|
function option(name, deflt, handle, notOnInit) { |
|
CodeMirror.defaults[name] = deflt; |
|
if (handle) optionHandlers[name] = |
|
notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; |
|
} |
|
|
|
// Passed to option handlers when there is no old value. |
|
var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; |
|
|
|
// These two are, on init, called from the constructor because they |
|
// have to be initialized before the editor can start at all. |
|
option("value", "", function(cm, val) { |
|
cm.setValue(val); |
|
}, true); |
|
option("mode", null, function(cm, val) { |
|
cm.doc.modeOption = val; |
|
loadMode(cm); |
|
}, true); |
|
|
|
option("indentUnit", 2, loadMode, true); |
|
option("indentWithTabs", false); |
|
option("smartIndent", true); |
|
option("tabSize", 4, function(cm) { |
|
resetModeState(cm); |
|
clearCaches(cm); |
|
regChange(cm); |
|
}, true); |
|
option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { |
|
cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); |
|
if (old != CodeMirror.Init) cm.refresh(); |
|
}); |
|
option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); |
|
option("electricChars", true); |
|
option("inputStyle", mobile ? "contenteditable" : "textarea", function() { |
|
throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME |
|
}, true); |
|
option("rtlMoveVisually", !windows); |
|
option("wholeLineUpdateBefore", true); |
|
|
|
option("theme", "default", function(cm) { |
|
themeChanged(cm); |
|
guttersChanged(cm); |
|
}, true); |
|
option("keyMap", "default", function(cm, val, old) { |
|
var next = getKeyMap(val); |
|
var prev = old != CodeMirror.Init && getKeyMap(old); |
|
if (prev && prev.detach) prev.detach(cm, next); |
|
if (next.attach) next.attach(cm, prev || null); |
|
}); |
|
option("extraKeys", null); |
|
|
|
option("lineWrapping", false, wrappingChanged, true); |
|
option("gutters", [], function(cm) { |
|
setGuttersForLineNumbers(cm.options); |
|
guttersChanged(cm); |
|
}, true); |
|
option("fixedGutter", true, function(cm, val) { |
|
cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; |
|
cm.refresh(); |
|
}, true); |
|
option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); |
|
option("scrollbarStyle", "native", function(cm) { |
|
initScrollbars(cm); |
|
updateScrollbars(cm); |
|
cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); |
|
cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); |
|
}, true); |
|
option("lineNumbers", false, function(cm) { |
|
setGuttersForLineNumbers(cm.options); |
|
guttersChanged(cm); |
|
}, true); |
|
option("firstLineNumber", 1, guttersChanged, true); |
|
option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); |
|
option("showCursorWhenSelecting", false, updateSelection, true); |
|
|
|
option("resetSelectionOnContextMenu", true); |
|
option("lineWiseCopyCut", true); |
|
|
|
option("readOnly", false, function(cm, val) { |
|
if (val == "nocursor") { |
|
onBlur(cm); |
|
cm.display.input.blur(); |
|
cm.display.disabled = true; |
|
} else { |
|
cm.display.disabled = false; |
|
if (!val) cm.display.input.reset(); |
|
} |
|
}); |
|
option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); |
|
option("dragDrop", true, dragDropChanged); |
|
|
|
option("cursorBlinkRate", 530); |
|
option("cursorScrollMargin", 0); |
|
option("cursorHeight", 1, updateSelection, true); |
|
option("singleCursorHeightPerLine", true, updateSelection, true); |
|
option("workTime", 100); |
|
option("workDelay", 100); |
|
option("flattenSpans", true, resetModeState, true); |
|
option("addModeClass", false, resetModeState, true); |
|
option("pollInterval", 100); |
|
option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); |
|
option("historyEventDelay", 1250); |
|
option("viewportMargin", 10, function(cm){cm.refresh();}, true); |
|
option("maxHighlightLength", 10000, resetModeState, true); |
|
option("moveInputWithCursor", true, function(cm, val) { |
|
if (!val) cm.display.input.resetPosition(); |
|
}); |
|
|
|
option("tabindex", null, function(cm, val) { |
|
cm.display.input.getField().tabIndex = val || ""; |
|
}); |
|
option("autofocus", null); |
|
|
|
// MODE DEFINITION AND QUERYING |
|
|
|
// Known modes, by name and by MIME |
|
var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; |
|
|
|
// Extra arguments are stored as the mode's dependencies, which is |
|
// used by (legacy) mechanisms like loadmode.js to automatically |
|
// load a mode. (Preferred mechanism is the require/define calls.) |
|
CodeMirror.defineMode = function(name, mode) { |
|
if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; |
|
if (arguments.length > 2) |
|
mode.dependencies = Array.prototype.slice.call(arguments, 2); |
|
modes[name] = mode; |
|
}; |
|
|
|
CodeMirror.defineMIME = function(mime, spec) { |
|
mimeModes[mime] = spec; |
|
}; |
|
|
|
// Given a MIME type, a {name, ...options} config object, or a name |
|
// string, return a mode config object. |
|
CodeMirror.resolveMode = function(spec) { |
|
if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { |
|
spec = mimeModes[spec]; |
|
} else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { |
|
var found = mimeModes[spec.name]; |
|
if (typeof found == "string") found = {name: found}; |
|
spec = createObj(found, spec); |
|
spec.name = found.name; |
|
} else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { |
|
return CodeMirror.resolveMode("application/xml"); |
|
} |
|
if (typeof spec == "string") return {name: spec}; |
|
else return spec || {name: "null"}; |
|
}; |
|
|
|
// Given a mode spec (anything that resolveMode accepts), find and |
|
// initialize an actual mode object. |
|
CodeMirror.getMode = function(options, spec) { |
|
var spec = CodeMirror.resolveMode(spec); |
|
var mfactory = modes[spec.name]; |
|
if (!mfactory) return CodeMirror.getMode(options, "text/plain"); |
|
var modeObj = mfactory(options, spec); |
|
if (modeExtensions.hasOwnProperty(spec.name)) { |
|
var exts = modeExtensions[spec.name]; |
|
for (var prop in exts) { |
|
if (!exts.hasOwnProperty(prop)) continue; |
|
if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; |
|
modeObj[prop] = exts[prop]; |
|
} |
|
} |
|
modeObj.name = spec.name; |
|
if (spec.helperType) modeObj.helperType = spec.helperType; |
|
if (spec.modeProps) for (var prop in spec.modeProps) |
|
modeObj[prop] = spec.modeProps[prop]; |
|
|
|
return modeObj; |
|
}; |
|
|
|
// Minimal default mode. |
|
CodeMirror.defineMode("null", function() { |
|
return {token: function(stream) {stream.skipToEnd();}}; |
|
}); |
|
CodeMirror.defineMIME("text/plain", "null"); |
|
|
|
// This can be used to attach properties to mode objects from |
|
// outside the actual mode definition. |
|
var modeExtensions = CodeMirror.modeExtensions = {}; |
|
CodeMirror.extendMode = function(mode, properties) { |
|
var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); |
|
copyObj(properties, exts); |
|
}; |
|
|
|
// EXTENSIONS |
|
|
|
CodeMirror.defineExtension = function(name, func) { |
|
CodeMirror.prototype[name] = func; |
|
}; |
|
CodeMirror.defineDocExtension = function(name, func) { |
|
Doc.prototype[name] = func; |
|
}; |
|
CodeMirror.defineOption = option; |
|
|
|
var initHooks = []; |
|
CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; |
|
|
|
var helpers = CodeMirror.helpers = {}; |
|
CodeMirror.registerHelper = function(type, name, value) { |
|
if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; |
|
helpers[type][name] = value; |
|
}; |
|
CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { |
|
CodeMirror.registerHelper(type, name, value); |
|
helpers[type]._global.push({pred: predicate, val: value}); |
|
}; |
|
|
|
// MODE STATE HANDLING |
|
|
|
// Utility functions for working with state. Exported because nested |
|
// modes need to do this for their inner modes. |
|
|
|
var copyState = CodeMirror.copyState = function(mode, state) { |
|
if (state === true) return state; |
|
if (mode.copyState) return mode.copyState(state); |
|
var nstate = {}; |
|
for (var n in state) { |
|
var val = state[n]; |
|
if (val instanceof Array) val = val.concat([]); |
|
nstate[n] = val; |
|
} |
|
return nstate; |
|
}; |
|
|
|
var startState = CodeMirror.startState = function(mode, a1, a2) { |
|
return mode.startState ? mode.startState(a1, a2) : true; |
|
}; |
|
|
|
// Given a mode and a state (for that mode), find the inner mode and |
|
// state at the position that the state refers to. |
|
CodeMirror.innerMode = function(mode, state) { |
|
while (mode.innerMode) { |
|
var info = mode.innerMode(state); |
|
if (!info || info.mode == mode) break; |
|
state = info.state; |
|
mode = info.mode; |
|
} |
|
return info || {mode: mode, state: state}; |
|
}; |
|
|
|
// STANDARD COMMANDS |
|
|
|
// Commands are parameter-less actions that can be performed on an |
|
// editor, mostly used for keybindings. |
|
var commands = CodeMirror.commands = { |
|
selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, |
|
singleSelection: function(cm) { |
|
cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); |
|
}, |
|
killLine: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
if (range.empty()) { |
|
var len = getLine(cm.doc, range.head.line).text.length; |
|
if (range.head.ch == len && range.head.line < cm.lastLine()) |
|
return {from: range.head, to: Pos(range.head.line + 1, 0)}; |
|
else |
|
return {from: range.head, to: Pos(range.head.line, len)}; |
|
} else { |
|
return {from: range.from(), to: range.to()}; |
|
} |
|
}); |
|
}, |
|
deleteLine: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
return {from: Pos(range.from().line, 0), |
|
to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; |
|
}); |
|
}, |
|
delLineLeft: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
return {from: Pos(range.from().line, 0), to: range.from()}; |
|
}); |
|
}, |
|
delWrappedLineLeft: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
var leftPos = cm.coordsChar({left: 0, top: top}, "div"); |
|
return {from: leftPos, to: range.from()}; |
|
}); |
|
}, |
|
delWrappedLineRight: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); |
|
return {from: range.from(), to: rightPos }; |
|
}); |
|
}, |
|
undo: function(cm) {cm.undo();}, |
|
redo: function(cm) {cm.redo();}, |
|
undoSelection: function(cm) {cm.undoSelection();}, |
|
redoSelection: function(cm) {cm.redoSelection();}, |
|
goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, |
|
goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, |
|
goLineStart: function(cm) { |
|
cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, |
|
{origin: "+move", bias: 1}); |
|
}, |
|
goLineStartSmart: function(cm) { |
|
cm.extendSelectionsBy(function(range) { |
|
return lineStartSmart(cm, range.head); |
|
}, {origin: "+move", bias: 1}); |
|
}, |
|
goLineEnd: function(cm) { |
|
cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, |
|
{origin: "+move", bias: -1}); |
|
}, |
|
goLineRight: function(cm) { |
|
cm.extendSelectionsBy(function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); |
|
}, sel_move); |
|
}, |
|
goLineLeft: function(cm) { |
|
cm.extendSelectionsBy(function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
return cm.coordsChar({left: 0, top: top}, "div"); |
|
}, sel_move); |
|
}, |
|
goLineLeftSmart: function(cm) { |
|
cm.extendSelectionsBy(function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
var pos = cm.coordsChar({left: 0, top: top}, "div"); |
|
if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); |
|
return pos; |
|
}, sel_move); |
|
}, |
|
goLineUp: function(cm) {cm.moveV(-1, "line");}, |
|
goLineDown: function(cm) {cm.moveV(1, "line");}, |
|
goPageUp: function(cm) {cm.moveV(-1, "page");}, |
|
goPageDown: function(cm) {cm.moveV(1, "page");}, |
|
goCharLeft: function(cm) {cm.moveH(-1, "char");}, |
|
goCharRight: function(cm) {cm.moveH(1, "char");}, |
|
goColumnLeft: function(cm) {cm.moveH(-1, "column");}, |
|
goColumnRight: function(cm) {cm.moveH(1, "column");}, |
|
goWordLeft: function(cm) {cm.moveH(-1, "word");}, |
|
goGroupRight: function(cm) {cm.moveH(1, "group");}, |
|
goGroupLeft: function(cm) {cm.moveH(-1, "group");}, |
|
goWordRight: function(cm) {cm.moveH(1, "word");}, |
|
delCharBefore: function(cm) {cm.deleteH(-1, "char");}, |
|
delCharAfter: function(cm) {cm.deleteH(1, "char");}, |
|
delWordBefore: function(cm) {cm.deleteH(-1, "word");}, |
|
delWordAfter: function(cm) {cm.deleteH(1, "word");}, |
|
delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, |
|
delGroupAfter: function(cm) {cm.deleteH(1, "group");}, |
|
indentAuto: function(cm) {cm.indentSelection("smart");}, |
|
indentMore: function(cm) {cm.indentSelection("add");}, |
|
indentLess: function(cm) {cm.indentSelection("subtract");}, |
|
insertTab: function(cm) {cm.replaceSelection("\t");}, |
|
insertSoftTab: function(cm) { |
|
var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var pos = ranges[i].from(); |
|
var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); |
|
spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); |
|
} |
|
cm.replaceSelections(spaces); |
|
}, |
|
defaultTab: function(cm) { |
|
if (cm.somethingSelected()) cm.indentSelection("add"); |
|
else cm.execCommand("insertTab"); |
|
}, |
|
transposeChars: function(cm) { |
|
runInOp(cm, function() { |
|
var ranges = cm.listSelections(), newSel = []; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; |
|
if (line) { |
|
if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); |
|
if (cur.ch > 0) { |
|
cur = new Pos(cur.line, cur.ch + 1); |
|
cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), |
|
Pos(cur.line, cur.ch - 2), cur, "+transpose"); |
|
} else if (cur.line > cm.doc.first) { |
|
var prev = getLine(cm.doc, cur.line - 1).text; |
|
if (prev) |
|
cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), |
|
Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); |
|
} |
|
} |
|
newSel.push(new Range(cur, cur)); |
|
} |
|
cm.setSelections(newSel); |
|
}); |
|
}, |
|
newlineAndIndent: function(cm) { |
|
runInOp(cm, function() { |
|
var len = cm.listSelections().length; |
|
for (var i = 0; i < len; i++) { |
|
var range = cm.listSelections()[i]; |
|
cm.replaceRange("\n", range.anchor, range.head, "+input"); |
|
cm.indentLine(range.from().line + 1, null, true); |
|
ensureCursorVisible(cm); |
|
} |
|
}); |
|
}, |
|
toggleOverwrite: function(cm) {cm.toggleOverwrite();} |
|
}; |
|
|
|
|
|
// STANDARD KEYMAPS |
|
|
|
var keyMap = CodeMirror.keyMap = {}; |
|
|
|
keyMap.basic = { |
|
"Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", |
|
"End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", |
|
"Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", |
|
"Tab": "defaultTab", "Shift-Tab": "indentAuto", |
|
"Enter": "newlineAndIndent", "Insert": "toggleOverwrite", |
|
"Esc": "singleSelection" |
|
}; |
|
// Note that the save and find-related commands aren't defined by |
|
// default. User code or addons can define them. Unknown commands |
|
// are simply ignored. |
|
keyMap.pcDefault = { |
|
"Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", |
|
"Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", |
|
"Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", |
|
"Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", |
|
"Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", |
|
"Ctrl-[": "indentLess", "Ctrl-]": "indentMore", |
|
"Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", |
|
fallthrough: "basic" |
|
}; |
|
// Very basic readline/emacs-style bindings, which are standard on Mac. |
|
keyMap.emacsy = { |
|
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", |
|
"Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", |
|
"Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", |
|
"Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" |
|
}; |
|
keyMap.macDefault = { |
|
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", |
|
"Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", |
|
"Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", |
|
"Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", |
|
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", |
|
"Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", |
|
"Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", |
|
fallthrough: ["basic", "emacsy"] |
|
}; |
|
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; |
|
|
|
// KEYMAP DISPATCH |
|
|
|
function normalizeKeyName(name) { |
|
var parts = name.split(/-(?!$)/), name = parts[parts.length - 1]; |
|
var alt, ctrl, shift, cmd; |
|
for (var i = 0; i < parts.length - 1; i++) { |
|
var mod = parts[i]; |
|
if (/^(cmd|meta|m)$/i.test(mod)) cmd = true; |
|
else if (/^a(lt)?$/i.test(mod)) alt = true; |
|
else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; |
|
else if (/^s(hift)$/i.test(mod)) shift = true; |
|
else throw new Error("Unrecognized modifier name: " + mod); |
|
} |
|
if (alt) name = "Alt-" + name; |
|
if (ctrl) name = "Ctrl-" + name; |
|
if (cmd) name = "Cmd-" + name; |
|
if (shift) name = "Shift-" + name; |
|
return name; |
|
} |
|
|
|
// This is a kludge to keep keymaps mostly working as raw objects |
|
// (backwards compatibility) while at the same time support features |
|
// like normalization and multi-stroke key bindings. It compiles a |
|
// new normalized keymap, and then updates the old object to reflect |
|
// this. |
|
CodeMirror.normalizeKeyMap = function(keymap) { |
|
var copy = {}; |
|
for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) { |
|
var value = keymap[keyname]; |
|
if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue; |
|
if (value == "...") { delete keymap[keyname]; continue; } |
|
|
|
var keys = map(keyname.split(" "), normalizeKeyName); |
|
for (var i = 0; i < keys.length; i++) { |
|
var val, name; |
|
if (i == keys.length - 1) { |
|
name = keys.join(" "); |
|
val = value; |
|
} else { |
|
name = keys.slice(0, i + 1).join(" "); |
|
val = "..."; |
|
} |
|
var prev = copy[name]; |
|
if (!prev) copy[name] = val; |
|
else if (prev != val) throw new Error("Inconsistent bindings for " + name); |
|
} |
|
delete keymap[keyname]; |
|
} |
|
for (var prop in copy) keymap[prop] = copy[prop]; |
|
return keymap; |
|
}; |
|
|
|
var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) { |
|
map = getKeyMap(map); |
|
var found = map.call ? map.call(key, context) : map[key]; |
|
if (found === false) return "nothing"; |
|
if (found === "...") return "multi"; |
|
if (found != null && handle(found)) return "handled"; |
|
|
|
if (map.fallthrough) { |
|
if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") |
|
return lookupKey(key, map.fallthrough, handle, context); |
|
for (var i = 0; i < map.fallthrough.length; i++) { |
|
var result = lookupKey(key, map.fallthrough[i], handle, context); |
|
if (result) return result; |
|
} |
|
} |
|
}; |
|
|
|
// Modifier key presses don't count as 'real' key presses for the |
|
// purpose of keymap fallthrough. |
|
var isModifierKey = CodeMirror.isModifierKey = function(value) { |
|
var name = typeof value == "string" ? value : keyNames[value.keyCode]; |
|
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; |
|
}; |
|
|
|
// Look up the name of a key as indicated by an event object. |
|
var keyName = CodeMirror.keyName = function(event, noShift) { |
|
if (presto && event.keyCode == 34 && event["char"]) return false; |
|
var base = keyNames[event.keyCode], name = base; |
|
if (name == null || event.altGraphKey) return false; |
|
if (event.altKey && base != "Alt") name = "Alt-" + name; |
|
if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name; |
|
if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name; |
|
if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name; |
|
return name; |
|
}; |
|
|
|
function getKeyMap(val) { |
|
return typeof val == "string" ? keyMap[val] : val; |
|
} |
|
|
|
// FROMTEXTAREA |
|
|
|
CodeMirror.fromTextArea = function(textarea, options) { |
|
options = options ? copyObj(options) : {}; |
|
options.value = textarea.value; |
|
if (!options.tabindex && textarea.tabIndex) |
|
options.tabindex = textarea.tabIndex; |
|
if (!options.placeholder && textarea.placeholder) |
|
options.placeholder = textarea.placeholder; |
|
// Set autofocus to true if this textarea is focused, or if it has |
|
// autofocus and no other element is focused. |
|
if (options.autofocus == null) { |
|
var hasFocus = activeElt(); |
|
options.autofocus = hasFocus == textarea || |
|
textarea.getAttribute("autofocus") != null && hasFocus == document.body; |
|
} |
|
|
|
function save() {textarea.value = cm.getValue();} |
|
if (textarea.form) { |
|
on(textarea.form, "submit", save); |
|
// Deplorable hack to make the submit method do the right thing. |
|
if (!options.leaveSubmitMethodAlone) { |
|
var form = textarea.form, realSubmit = form.submit; |
|
try { |
|
var wrappedSubmit = form.submit = function() { |
|
save(); |
|
form.submit = realSubmit; |
|
form.submit(); |
|
form.submit = wrappedSubmit; |
|
}; |
|
} catch(e) {} |
|
} |
|
} |
|
|
|
options.finishInit = function(cm) { |
|
cm.save = save; |
|
cm.getTextArea = function() { return textarea; }; |
|
cm.toTextArea = function() { |
|
cm.toTextArea = isNaN; // Prevent this from being ran twice |
|
save(); |
|
textarea.parentNode.removeChild(cm.getWrapperElement()); |
|
textarea.style.display = ""; |
|
if (textarea.form) { |
|
off(textarea.form, "submit", save); |
|
if (typeof textarea.form.submit == "function") |
|
textarea.form.submit = realSubmit; |
|
} |
|
}; |
|
}; |
|
|
|
textarea.style.display = "none"; |
|
var cm = CodeMirror(function(node) { |
|
textarea.parentNode.insertBefore(node, textarea.nextSibling); |
|
}, options); |
|
return cm; |
|
}; |
|
|
|
// STRING STREAM |
|
|
|
// Fed to the mode parsers, provides helper functions to make |
|
// parsers more succinct. |
|
|
|
var StringStream = CodeMirror.StringStream = function(string, tabSize) { |
|
this.pos = this.start = 0; |
|
this.string = string; |
|
this.tabSize = tabSize || 8; |
|
this.lastColumnPos = this.lastColumnValue = 0; |
|
this.lineStart = 0; |
|
}; |
|
|
|
StringStream.prototype = { |
|
eol: function() {return this.pos >= this.string.length;}, |
|
sol: function() {return this.pos == this.lineStart;}, |
|
peek: function() {return this.string.charAt(this.pos) || undefined;}, |
|
next: function() { |
|
if (this.pos < this.string.length) |
|
return this.string.charAt(this.pos++); |
|
}, |
|
eat: function(match) { |
|
var ch = this.string.charAt(this.pos); |
|
if (typeof match == "string") var ok = ch == match; |
|
else var ok = ch && (match.test ? match.test(ch) : match(ch)); |
|
if (ok) {++this.pos; return ch;} |
|
}, |
|
eatWhile: function(match) { |
|
var start = this.pos; |
|
while (this.eat(match)){} |
|
return this.pos > start; |
|
}, |
|
eatSpace: function() { |
|
var start = this.pos; |
|
while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; |
|
return this.pos > start; |
|
}, |
|
skipToEnd: function() {this.pos = this.string.length;}, |
|
skipTo: function(ch) { |
|
var found = this.string.indexOf(ch, this.pos); |
|
if (found > -1) {this.pos = found; return true;} |
|
}, |
|
backUp: function(n) {this.pos -= n;}, |
|
column: function() { |
|
if (this.lastColumnPos < this.start) { |
|
this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); |
|
this.lastColumnPos = this.start; |
|
} |
|
return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); |
|
}, |
|
indentation: function() { |
|
return countColumn(this.string, null, this.tabSize) - |
|
(this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); |
|
}, |
|
match: function(pattern, consume, caseInsensitive) { |
|
if (typeof pattern == "string") { |
|
var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; |
|
var substr = this.string.substr(this.pos, pattern.length); |
|
if (cased(substr) == cased(pattern)) { |
|
if (consume !== false) this.pos += pattern.length; |
|
return true; |
|
} |
|
} else { |
|
var match = this.string.slice(this.pos).match(pattern); |
|
if (match && match.index > 0) return null; |
|
if (match && consume !== false) this.pos += match[0].length; |
|
return match; |
|
} |
|
}, |
|
current: function(){return this.string.slice(this.start, this.pos);}, |
|
hideFirstChars: function(n, inner) { |
|
this.lineStart += n; |
|
try { return inner(); } |
|
finally { this.lineStart -= n; } |
|
} |
|
}; |
|
|
|
// TEXTMARKERS |
|
|
|
// Created with markText and setBookmark methods. A TextMarker is a |
|
// handle that can be used to clear or find a marked position in the |
|
// document. Line objects hold arrays (markedSpans) containing |
|
// {from, to, marker} object pointing to such marker objects, and |
|
// indicating that such a marker is present on that line. Multiple |
|
// lines may point to the same marker when it spans across lines. |
|
// The spans will have null for their from/to properties when the |
|
// marker continues beyond the start/end of the line. Markers have |
|
// links back to the lines they currently touch. |
|
|
|
var nextMarkerId = 0; |
|
|
|
var TextMarker = CodeMirror.TextMarker = function(doc, type) { |
|
this.lines = []; |
|
this.type = type; |
|
this.doc = doc; |
|
this.id = ++nextMarkerId; |
|
}; |
|
eventMixin(TextMarker); |
|
|
|
// Clear the marker. |
|
TextMarker.prototype.clear = function() { |
|
if (this.explicitlyCleared) return; |
|
var cm = this.doc.cm, withOp = cm && !cm.curOp; |
|
if (withOp) startOperation(cm); |
|
if (hasHandler(this, "clear")) { |
|
var found = this.find(); |
|
if (found) signalLater(this, "clear", found.from, found.to); |
|
} |
|
var min = null, max = null; |
|
for (var i = 0; i < this.lines.length; ++i) { |
|
var line = this.lines[i]; |
|
var span = getMarkedSpanFor(line.markedSpans, this); |
|
if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); |
|
else if (cm) { |
|
if (span.to != null) max = lineNo(line); |
|
if (span.from != null) min = lineNo(line); |
|
} |
|
line.markedSpans = removeMarkedSpan(line.markedSpans, span); |
|
if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) |
|
updateLineHeight(line, textHeight(cm.display)); |
|
} |
|
if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { |
|
var visual = visualLine(this.lines[i]), len = lineLength(visual); |
|
if (len > cm.display.maxLineLength) { |
|
cm.display.maxLine = visual; |
|
cm.display.maxLineLength = len; |
|
cm.display.maxLineChanged = true; |
|
} |
|
} |
|
|
|
if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); |
|
this.lines.length = 0; |
|
this.explicitlyCleared = true; |
|
if (this.atomic && this.doc.cantEdit) { |
|
this.doc.cantEdit = false; |
|
if (cm) reCheckSelection(cm.doc); |
|
} |
|
if (cm) signalLater(cm, "markerCleared", cm, this); |
|
if (withOp) endOperation(cm); |
|
if (this.parent) this.parent.clear(); |
|
}; |
|
|
|
// Find the position of the marker in the document. Returns a {from, |
|
// to} object by default. Side can be passed to get a specific side |
|
// -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the |
|
// Pos objects returned contain a line object, rather than a line |
|
// number (used to prevent looking up the same line twice). |
|
TextMarker.prototype.find = function(side, lineObj) { |
|
if (side == null && this.type == "bookmark") side = 1; |
|
var from, to; |
|
for (var i = 0; i < this.lines.length; ++i) { |
|
var line = this.lines[i]; |
|
var span = getMarkedSpanFor(line.markedSpans, this); |
|
if (span.from != null) { |
|
from = Pos(lineObj ? line : lineNo(line), span.from); |
|
if (side == -1) return from; |
|
} |
|
if (span.to != null) { |
|
to = Pos(lineObj ? line : lineNo(line), span.to); |
|
if (side == 1) return to; |
|
} |
|
} |
|
return from && {from: from, to: to}; |
|
}; |
|
|
|
// Signals that the marker's widget changed, and surrounding layout |
|
// should be recomputed. |
|
TextMarker.prototype.changed = function() { |
|
var pos = this.find(-1, true), widget = this, cm = this.doc.cm; |
|
if (!pos || !cm) return; |
|
runInOp(cm, function() { |
|
var line = pos.line, lineN = lineNo(pos.line); |
|
var view = findViewForLine(cm, lineN); |
|
if (view) { |
|
clearLineMeasurementCacheFor(view); |
|
cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; |
|
} |
|
cm.curOp.updateMaxLine = true; |
|
if (!lineIsHidden(widget.doc, line) && widget.height != null) { |
|
var oldHeight = widget.height; |
|
widget.height = null; |
|
var dHeight = widgetHeight(widget) - oldHeight; |
|
if (dHeight) |
|
updateLineHeight(line, line.height + dHeight); |
|
} |
|
}); |
|
}; |
|
|
|
TextMarker.prototype.attachLine = function(line) { |
|
if (!this.lines.length && this.doc.cm) { |
|
var op = this.doc.cm.curOp; |
|
if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) |
|
(op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); |
|
} |
|
this.lines.push(line); |
|
}; |
|
TextMarker.prototype.detachLine = function(line) { |
|
this.lines.splice(indexOf(this.lines, line), 1); |
|
if (!this.lines.length && this.doc.cm) { |
|
var op = this.doc.cm.curOp; |
|
(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); |
|
} |
|
}; |
|
|
|
// Collapsed markers have unique ids, in order to be able to order |
|
// them, which is needed for uniquely determining an outer marker |
|
// when they overlap (they may nest, but not partially overlap). |
|
var nextMarkerId = 0; |
|
|
|
// Create a marker, wire it up to the right lines, and |
|
function markText(doc, from, to, options, type) { |
|
// Shared markers (across linked documents) are handled separately |
|
// (markTextShared will call out to this again, once per |
|
// document). |
|
if (options && options.shared) return markTextShared(doc, from, to, options, type); |
|
// Ensure we are in an operation. |
|
if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); |
|
|
|
var marker = new TextMarker(doc, type), diff = cmp(from, to); |
|
if (options) copyObj(options, marker, false); |
|
// Don't connect empty markers unless clearWhenEmpty is false |
|
if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) |
|
return marker; |
|
if (marker.replacedWith) { |
|
// Showing up as a widget implies collapsed (widget replaces text) |
|
marker.collapsed = true; |
|
marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); |
|
if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true"); |
|
if (options.insertLeft) marker.widgetNode.insertLeft = true; |
|
} |
|
if (marker.collapsed) { |
|
if (conflictingCollapsedRange(doc, from.line, from, to, marker) || |
|
from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) |
|
throw new Error("Inserting collapsed marker partially overlapping an existing one"); |
|
sawCollapsedSpans = true; |
|
} |
|
|
|
if (marker.addToHistory) |
|
addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); |
|
|
|
var curLine = from.line, cm = doc.cm, updateMaxLine; |
|
doc.iter(curLine, to.line + 1, function(line) { |
|
if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) |
|
updateMaxLine = true; |
|
if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); |
|
addMarkedSpan(line, new MarkedSpan(marker, |
|
curLine == from.line ? from.ch : null, |
|
curLine == to.line ? to.ch : null)); |
|
++curLine; |
|
}); |
|
// lineIsHidden depends on the presence of the spans, so needs a second pass |
|
if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { |
|
if (lineIsHidden(doc, line)) updateLineHeight(line, 0); |
|
}); |
|
|
|
if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); |
|
|
|
if (marker.readOnly) { |
|
sawReadOnlySpans = true; |
|
if (doc.history.done.length || doc.history.undone.length) |
|
doc.clearHistory(); |
|
} |
|
if (marker.collapsed) { |
|
marker.id = ++nextMarkerId; |
|
marker.atomic = true; |
|
} |
|
if (cm) { |
|
// Sync editor state |
|
if (updateMaxLine) cm.curOp.updateMaxLine = true; |
|
if (marker.collapsed) |
|
regChange(cm, from.line, to.line + 1); |
|
else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) |
|
for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); |
|
if (marker.atomic) reCheckSelection(cm.doc); |
|
signalLater(cm, "markerAdded", cm, marker); |
|
} |
|
return marker; |
|
} |
|
|
|
// SHARED TEXTMARKERS |
|
|
|
// A shared marker spans multiple linked documents. It is |
|
// implemented as a meta-marker-object controlling multiple normal |
|
// markers. |
|
var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { |
|
this.markers = markers; |
|
this.primary = primary; |
|
for (var i = 0; i < markers.length; ++i) |
|
markers[i].parent = this; |
|
}; |
|
eventMixin(SharedTextMarker); |
|
|
|
SharedTextMarker.prototype.clear = function() { |
|
if (this.explicitlyCleared) return; |
|
this.explicitlyCleared = true; |
|
for (var i = 0; i < this.markers.length; ++i) |
|
this.markers[i].clear(); |
|
signalLater(this, "clear"); |
|
}; |
|
SharedTextMarker.prototype.find = function(side, lineObj) { |
|
return this.primary.find(side, lineObj); |
|
}; |
|
|
|
function markTextShared(doc, from, to, options, type) { |
|
options = copyObj(options); |
|
options.shared = false; |
|
var markers = [markText(doc, from, to, options, type)], primary = markers[0]; |
|
var widget = options.widgetNode; |
|
linkedDocs(doc, function(doc) { |
|
if (widget) options.widgetNode = widget.cloneNode(true); |
|
markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); |
|
for (var i = 0; i < doc.linked.length; ++i) |
|
if (doc.linked[i].isParent) return; |
|
primary = lst(markers); |
|
}); |
|
return new SharedTextMarker(markers, primary); |
|
} |
|
|
|
function findSharedMarkers(doc) { |
|
return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), |
|
function(m) { return m.parent; }); |
|
} |
|
|
|
function copySharedMarkers(doc, markers) { |
|
for (var i = 0; i < markers.length; i++) { |
|
var marker = markers[i], pos = marker.find(); |
|
var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); |
|
if (cmp(mFrom, mTo)) { |
|
var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); |
|
marker.markers.push(subMark); |
|
subMark.parent = marker; |
|
} |
|
} |
|
} |
|
|
|
function detachSharedMarkers(markers) { |
|
for (var i = 0; i < markers.length; i++) { |
|
var marker = markers[i], linked = [marker.primary.doc];; |
|
linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); |
|
for (var j = 0; j < marker.markers.length; j++) { |
|
var subMarker = marker.markers[j]; |
|
if (indexOf(linked, subMarker.doc) == -1) { |
|
subMarker.parent = null; |
|
marker.markers.splice(j--, 1); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// TEXTMARKER SPANS |
|
|
|
function MarkedSpan(marker, from, to) { |
|
this.marker = marker; |
|
this.from = from; this.to = to; |
|
} |
|
|
|
// Search an array of spans for a span matching the given marker. |
|
function getMarkedSpanFor(spans, marker) { |
|
if (spans) for (var i = 0; i < spans.length; ++i) { |
|
var span = spans[i]; |
|
if (span.marker == marker) return span; |
|
} |
|
} |
|
// Remove a span from an array, returning undefined if no spans are |
|
// left (we don't store arrays for lines without spans). |
|
function removeMarkedSpan(spans, span) { |
|
for (var r, i = 0; i < spans.length; ++i) |
|
if (spans[i] != span) (r || (r = [])).push(spans[i]); |
|
return r; |
|
} |
|
// Add a span to a line. |
|
function addMarkedSpan(line, span) { |
|
line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; |
|
span.marker.attachLine(line); |
|
} |
|
|
|
// Used for the algorithm that adjusts markers for a change in the |
|
// document. These functions cut an array of spans at a given |
|
// character position, returning an array of remaining chunks (or |
|
// undefined if nothing remains). |
|
function markedSpansBefore(old, startCh, isInsert) { |
|
if (old) for (var i = 0, nw; i < old.length; ++i) { |
|
var span = old[i], marker = span.marker; |
|
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); |
|
if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { |
|
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); |
|
(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); |
|
} |
|
} |
|
return nw; |
|
} |
|
function markedSpansAfter(old, endCh, isInsert) { |
|
if (old) for (var i = 0, nw; i < old.length; ++i) { |
|
var span = old[i], marker = span.marker; |
|
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); |
|
if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { |
|
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); |
|
(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, |
|
span.to == null ? null : span.to - endCh)); |
|
} |
|
} |
|
return nw; |
|
} |
|
|
|
// Given a change object, compute the new set of marker spans that |
|
// cover the line in which the change took place. Removes spans |
|
// entirely within the change, reconnects spans belonging to the |
|
// same marker that appear on both sides of the change, and cuts off |
|
// spans partially within the change. Returns an array of span |
|
// arrays with one element for each line in (after) the change. |
|
function stretchSpansOverChange(doc, change) { |
|
if (change.full) return null; |
|
var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; |
|
var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; |
|
if (!oldFirst && !oldLast) return null; |
|
|
|
var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; |
|
// Get the spans that 'stick out' on both sides |
|
var first = markedSpansBefore(oldFirst, startCh, isInsert); |
|
var last = markedSpansAfter(oldLast, endCh, isInsert); |
|
|
|
// Next, merge those two ends |
|
var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); |
|
if (first) { |
|
// Fix up .to properties of first |
|
for (var i = 0; i < first.length; ++i) { |
|
var span = first[i]; |
|
if (span.to == null) { |
|
var found = getMarkedSpanFor(last, span.marker); |
|
if (!found) span.to = startCh; |
|
else if (sameLine) span.to = found.to == null ? null : found.to + offset; |
|
} |
|
} |
|
} |
|
if (last) { |
|
// Fix up .from in last (or move them into first in case of sameLine) |
|
for (var i = 0; i < last.length; ++i) { |
|
var span = last[i]; |
|
if (span.to != null) span.to += offset; |
|
if (span.from == null) { |
|
var found = getMarkedSpanFor(first, span.marker); |
|
if (!found) { |
|
span.from = offset; |
|
if (sameLine) (first || (first = [])).push(span); |
|
} |
|
} else { |
|
span.from += offset; |
|
if (sameLine) (first || (first = [])).push(span); |
|
} |
|
} |
|
} |
|
// Make sure we didn't create any zero-length spans |
|
if (first) first = clearEmptySpans(first); |
|
if (last && last != first) last = clearEmptySpans(last); |
|
|
|
var newMarkers = [first]; |
|
if (!sameLine) { |
|
// Fill gap with whole-line-spans |
|
var gap = change.text.length - 2, gapMarkers; |
|
if (gap > 0 && first) |
|
for (var i = 0; i < first.length; ++i) |
|
if (first[i].to == null) |
|
(gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); |
|
for (var i = 0; i < gap; ++i) |
|
newMarkers.push(gapMarkers); |
|
newMarkers.push(last); |
|
} |
|
return newMarkers; |
|
} |
|
|
|
// Remove spans that are empty and don't have a clearWhenEmpty |
|
// option of false. |
|
function clearEmptySpans(spans) { |
|
for (var i = 0; i < spans.length; ++i) { |
|
var span = spans[i]; |
|
if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) |
|
spans.splice(i--, 1); |
|
} |
|
if (!spans.length) return null; |
|
return spans; |
|
} |
|
|
|
// Used for un/re-doing changes from the history. Combines the |
|
// result of computing the existing spans with the set of spans that |
|
// existed in the history (so that deleting around a span and then |
|
// undoing brings back the span). |
|
function mergeOldSpans(doc, change) { |
|
var old = getOldSpans(doc, change); |
|
var stretched = stretchSpansOverChange(doc, change); |
|
if (!old) return stretched; |
|
if (!stretched) return old; |
|
|
|
for (var i = 0; i < old.length; ++i) { |
|
var oldCur = old[i], stretchCur = stretched[i]; |
|
if (oldCur && stretchCur) { |
|
spans: for (var j = 0; j < stretchCur.length; ++j) { |
|
var span = stretchCur[j]; |
|
for (var k = 0; k < oldCur.length; ++k) |
|
if (oldCur[k].marker == span.marker) continue spans; |
|
oldCur.push(span); |
|
} |
|
} else if (stretchCur) { |
|
old[i] = stretchCur; |
|
} |
|
} |
|
return old; |
|
} |
|
|
|
// Used to 'clip' out readOnly ranges when making a change. |
|
function removeReadOnlyRanges(doc, from, to) { |
|
var markers = null; |
|
doc.iter(from.line, to.line + 1, function(line) { |
|
if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { |
|
var mark = line.markedSpans[i].marker; |
|
if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) |
|
(markers || (markers = [])).push(mark); |
|
} |
|
}); |
|
if (!markers) return null; |
|
var parts = [{from: from, to: to}]; |
|
for (var i = 0; i < markers.length; ++i) { |
|
var mk = markers[i], m = mk.find(0); |
|
for (var j = 0; j < parts.length; ++j) { |
|
var p = parts[j]; |
|
if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; |
|
var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); |
|
if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) |
|
newParts.push({from: p.from, to: m.from}); |
|
if (dto > 0 || !mk.inclusiveRight && !dto) |
|
newParts.push({from: m.to, to: p.to}); |
|
parts.splice.apply(parts, newParts); |
|
j += newParts.length - 1; |
|
} |
|
} |
|
return parts; |
|
} |
|
|
|
// Connect or disconnect spans from a line. |
|
function detachMarkedSpans(line) { |
|
var spans = line.markedSpans; |
|
if (!spans) return; |
|
for (var i = 0; i < spans.length; ++i) |
|
spans[i].marker.detachLine(line); |
|
line.markedSpans = null; |
|
} |
|
function attachMarkedSpans(line, spans) { |
|
if (!spans) return; |
|
for (var i = 0; i < spans.length; ++i) |
|
spans[i].marker.attachLine(line); |
|
line.markedSpans = spans; |
|
} |
|
|
|
// Helpers used when computing which overlapping collapsed span |
|
// counts as the larger one. |
|
function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } |
|
function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } |
|
|
|
// Returns a number indicating which of two overlapping collapsed |
|
// spans is larger (and thus includes the other). Falls back to |
|
// comparing ids when the spans cover exactly the same range. |
|
function compareCollapsedMarkers(a, b) { |
|
var lenDiff = a.lines.length - b.lines.length; |
|
if (lenDiff != 0) return lenDiff; |
|
var aPos = a.find(), bPos = b.find(); |
|
var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); |
|
if (fromCmp) return -fromCmp; |
|
var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); |
|
if (toCmp) return toCmp; |
|
return b.id - a.id; |
|
} |
|
|
|
// Find out whether a line ends or starts in a collapsed span. If |
|
// so, return the marker for that span. |
|
function collapsedSpanAtSide(line, start) { |
|
var sps = sawCollapsedSpans && line.markedSpans, found; |
|
if (sps) for (var sp, i = 0; i < sps.length; ++i) { |
|
sp = sps[i]; |
|
if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && |
|
(!found || compareCollapsedMarkers(found, sp.marker) < 0)) |
|
found = sp.marker; |
|
} |
|
return found; |
|
} |
|
function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } |
|
function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } |
|
|
|
// Test whether there exists a collapsed span that partially |
|
// overlaps (covers the start or end, but not both) of a new span. |
|
// Such overlap is not allowed. |
|
function conflictingCollapsedRange(doc, lineNo, from, to, marker) { |
|
var line = getLine(doc, lineNo); |
|
var sps = sawCollapsedSpans && line.markedSpans; |
|
if (sps) for (var i = 0; i < sps.length; ++i) { |
|
var sp = sps[i]; |
|
if (!sp.marker.collapsed) continue; |
|
var found = sp.marker.find(0); |
|
var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); |
|
var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); |
|
if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; |
|
if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || |
|
fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) |
|
return true; |
|
} |
|
} |
|
|
|
// A visual line is a line as drawn on the screen. Folding, for |
|
// example, can cause multiple logical lines to appear on the same |
|
// visual line. This finds the start of the visual line that the |
|
// given line is part of (usually that is the line itself). |
|
function visualLine(line) { |
|
var merged; |
|
while (merged = collapsedSpanAtStart(line)) |
|
line = merged.find(-1, true).line; |
|
return line; |
|
} |
|
|
|
// Returns an array of logical lines that continue the visual line |
|
// started by the argument, or undefined if there are no such lines. |
|
function visualLineContinued(line) { |
|
var merged, lines; |
|
while (merged = collapsedSpanAtEnd(line)) { |
|
line = merged.find(1, true).line; |
|
(lines || (lines = [])).push(line); |
|
} |
|
return lines; |
|
} |
|
|
|
// Get the line number of the start of the visual line that the |
|
// given line number is part of. |
|
function visualLineNo(doc, lineN) { |
|
var line = getLine(doc, lineN), vis = visualLine(line); |
|
if (line == vis) return lineN; |
|
return lineNo(vis); |
|
} |
|
// Get the line number of the start of the next visual line after |
|
// the given line. |
|
function visualLineEndNo(doc, lineN) { |
|
if (lineN > doc.lastLine()) return lineN; |
|
var line = getLine(doc, lineN), merged; |
|
if (!lineIsHidden(doc, line)) return lineN; |
|
while (merged = collapsedSpanAtEnd(line)) |
|
line = merged.find(1, true).line; |
|
return lineNo(line) + 1; |
|
} |
|
|
|
// Compute whether a line is hidden. Lines count as hidden when they |
|
// are part of a visual line that starts with another line, or when |
|
// they are entirely covered by collapsed, non-widget span. |
|
function lineIsHidden(doc, line) { |
|
var sps = sawCollapsedSpans && line.markedSpans; |
|
if (sps) for (var sp, i = 0; i < sps.length; ++i) { |
|
sp = sps[i]; |
|
if (!sp.marker.collapsed) continue; |
|
if (sp.from == null) return true; |
|
if (sp.marker.widgetNode) continue; |
|
if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) |
|
return true; |
|
} |
|
} |
|
function lineIsHiddenInner(doc, line, span) { |
|
if (span.to == null) { |
|
var end = span.marker.find(1, true); |
|
return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); |
|
} |
|
if (span.marker.inclusiveRight && span.to == line.text.length) |
|
return true; |
|
for (var sp, i = 0; i < line.markedSpans.length; ++i) { |
|
sp = line.markedSpans[i]; |
|
if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && |
|
(sp.to == null || sp.to != span.from) && |
|
(sp.marker.inclusiveLeft || span.marker.inclusiveRight) && |
|
lineIsHiddenInner(doc, line, sp)) return true; |
|
} |
|
} |
|
|
|
// LINE WIDGETS |
|
|
|
// Line widgets are block elements displayed above or below a line. |
|
|
|
var LineWidget = CodeMirror.LineWidget = function(doc, node, options) { |
|
if (options) for (var opt in options) if (options.hasOwnProperty(opt)) |
|
this[opt] = options[opt]; |
|
this.doc = doc; |
|
this.node = node; |
|
}; |
|
eventMixin(LineWidget); |
|
|
|
function adjustScrollWhenAboveVisible(cm, line, diff) { |
|
if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) |
|
addToScrollPos(cm, null, diff); |
|
} |
|
|
|
LineWidget.prototype.clear = function() { |
|
var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); |
|
if (no == null || !ws) return; |
|
for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); |
|
if (!ws.length) line.widgets = null; |
|
var height = widgetHeight(this); |
|
updateLineHeight(line, Math.max(0, line.height - height)); |
|
if (cm) runInOp(cm, function() { |
|
adjustScrollWhenAboveVisible(cm, line, -height); |
|
regLineChange(cm, no, "widget"); |
|
}); |
|
}; |
|
LineWidget.prototype.changed = function() { |
|
var oldH = this.height, cm = this.doc.cm, line = this.line; |
|
this.height = null; |
|
var diff = widgetHeight(this) - oldH; |
|
if (!diff) return; |
|
updateLineHeight(line, line.height + diff); |
|
if (cm) runInOp(cm, function() { |
|
cm.curOp.forceUpdate = true; |
|
adjustScrollWhenAboveVisible(cm, line, diff); |
|
}); |
|
}; |
|
|
|
function widgetHeight(widget) { |
|
if (widget.height != null) return widget.height; |
|
var cm = widget.doc.cm; |
|
if (!cm) return 0; |
|
if (!contains(document.body, widget.node)) { |
|
var parentStyle = "position: relative;"; |
|
if (widget.coverGutter) |
|
parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; |
|
if (widget.noHScroll) |
|
parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; |
|
removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); |
|
} |
|
return widget.height = widget.node.offsetHeight; |
|
} |
|
|
|
function addLineWidget(doc, handle, node, options) { |
|
var widget = new LineWidget(doc, node, options); |
|
var cm = doc.cm; |
|
if (cm && widget.noHScroll) cm.display.alignWidgets = true; |
|
changeLine(doc, handle, "widget", function(line) { |
|
var widgets = line.widgets || (line.widgets = []); |
|
if (widget.insertAt == null) widgets.push(widget); |
|
else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); |
|
widget.line = line; |
|
if (cm && !lineIsHidden(doc, line)) { |
|
var aboveVisible = heightAtLine(line) < doc.scrollTop; |
|
updateLineHeight(line, line.height + widgetHeight(widget)); |
|
if (aboveVisible) addToScrollPos(cm, null, widget.height); |
|
cm.curOp.forceUpdate = true; |
|
} |
|
return true; |
|
}); |
|
return widget; |
|
} |
|
|
|
// LINE DATA STRUCTURE |
|
|
|
// Line objects. These hold state related to a line, including |
|
// highlighting info (the styles array). |
|
var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { |
|
this.text = text; |
|
attachMarkedSpans(this, markedSpans); |
|
this.height = estimateHeight ? estimateHeight(this) : 1; |
|
}; |
|
eventMixin(Line); |
|
Line.prototype.lineNo = function() { return lineNo(this); }; |
|
|
|
// Change the content (text, markers) of a line. Automatically |
|
// invalidates cached information and tries to re-estimate the |
|
// line's height. |
|
function updateLine(line, text, markedSpans, estimateHeight) { |
|
line.text = text; |
|
if (line.stateAfter) line.stateAfter = null; |
|
if (line.styles) line.styles = null; |
|
if (line.order != null) line.order = null; |
|
detachMarkedSpans(line); |
|
attachMarkedSpans(line, markedSpans); |
|
var estHeight = estimateHeight ? estimateHeight(line) : 1; |
|
if (estHeight != line.height) updateLineHeight(line, estHeight); |
|
} |
|
|
|
// Detach a line from the document tree and its markers. |
|
function cleanUpLine(line) { |
|
line.parent = null; |
|
detachMarkedSpans(line); |
|
} |
|
|
|
function extractLineClasses(type, output) { |
|
if (type) for (;;) { |
|
var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); |
|
if (!lineClass) break; |
|
type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); |
|
var prop = lineClass[1] ? "bgClass" : "textClass"; |
|
if (output[prop] == null) |
|
output[prop] = lineClass[2]; |
|
else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) |
|
output[prop] += " " + lineClass[2]; |
|
} |
|
return type; |
|
} |
|
|
|
function callBlankLine(mode, state) { |
|
if (mode.blankLine) return mode.blankLine(state); |
|
if (!mode.innerMode) return; |
|
var inner = CodeMirror.innerMode(mode, state); |
|
if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); |
|
} |
|
|
|
function readToken(mode, stream, state, inner) { |
|
for (var i = 0; i < 10; i++) { |
|
if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode; |
|
var style = mode.token(stream, state); |
|
if (stream.pos > stream.start) return style; |
|
} |
|
throw new Error("Mode " + mode.name + " failed to advance stream."); |
|
} |
|
|
|
// Utility for getTokenAt and getLineTokens |
|
function takeToken(cm, pos, precise, asArray) { |
|
function getObj(copy) { |
|
return {start: stream.start, end: stream.pos, |
|
string: stream.current(), |
|
type: style || null, |
|
state: copy ? copyState(doc.mode, state) : state}; |
|
} |
|
|
|
var doc = cm.doc, mode = doc.mode, style; |
|
pos = clipPos(doc, pos); |
|
var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise); |
|
var stream = new StringStream(line.text, cm.options.tabSize), tokens; |
|
if (asArray) tokens = []; |
|
while ((asArray || stream.pos < pos.ch) && !stream.eol()) { |
|
stream.start = stream.pos; |
|
style = readToken(mode, stream, state); |
|
if (asArray) tokens.push(getObj(true)); |
|
} |
|
return asArray ? tokens : getObj(); |
|
} |
|
|
|
// Run the given mode's parser over a line, calling f for each token. |
|
function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { |
|
var flattenSpans = mode.flattenSpans; |
|
if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; |
|
var curStart = 0, curStyle = null; |
|
var stream = new StringStream(text, cm.options.tabSize), style; |
|
var inner = cm.options.addModeClass && [null]; |
|
if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); |
|
while (!stream.eol()) { |
|
if (stream.pos > cm.options.maxHighlightLength) { |
|
flattenSpans = false; |
|
if (forceToEnd) processLine(cm, text, state, stream.pos); |
|
stream.pos = text.length; |
|
style = null; |
|
} else { |
|
style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses); |
|
} |
|
if (inner) { |
|
var mName = inner[0].name; |
|
if (mName) style = "m-" + (style ? mName + " " + style : mName); |
|
} |
|
if (!flattenSpans || curStyle != style) { |
|
while (curStart < stream.start) { |
|
curStart = Math.min(stream.start, curStart + 50000); |
|
f(curStart, curStyle); |
|
} |
|
curStyle = style; |
|
} |
|
stream.start = stream.pos; |
|
} |
|
while (curStart < stream.pos) { |
|
// Webkit seems to refuse to render text nodes longer than 57444 characters |
|
var pos = Math.min(stream.pos, curStart + 50000); |
|
f(pos, curStyle); |
|
curStart = pos; |
|
} |
|
} |
|
|
|
// Compute a style array (an array starting with a mode generation |
|
// -- for invalidation -- followed by pairs of end positions and |
|
// style strings), which is used to highlight the tokens on the |
|
// line. |
|
function highlightLine(cm, line, state, forceToEnd) { |
|
// A styles array always starts with a number identifying the |
|
// mode/overlays that it is based on (for easy invalidation). |
|
var st = [cm.state.modeGen], lineClasses = {}; |
|
// Compute the base array of styles |
|
runMode(cm, line.text, cm.doc.mode, state, function(end, style) { |
|
st.push(end, style); |
|
}, lineClasses, forceToEnd); |
|
|
|
// Run overlays, adjust style array. |
|
for (var o = 0; o < cm.state.overlays.length; ++o) { |
|
var overlay = cm.state.overlays[o], i = 1, at = 0; |
|
runMode(cm, line.text, overlay.mode, true, function(end, style) { |
|
var start = i; |
|
// Ensure there's a token end at the current position, and that i points at it |
|
while (at < end) { |
|
var i_end = st[i]; |
|
if (i_end > end) |
|
st.splice(i, 1, end, st[i+1], i_end); |
|
i += 2; |
|
at = Math.min(end, i_end); |
|
} |
|
if (!style) return; |
|
if (overlay.opaque) { |
|
st.splice(start, i - start, end, "cm-overlay " + style); |
|
i = start + 2; |
|
} else { |
|
for (; start < i; start += 2) { |
|
var cur = st[start+1]; |
|
st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; |
|
} |
|
} |
|
}, lineClasses); |
|
} |
|
|
|
return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; |
|
} |
|
|
|
function getLineStyles(cm, line, updateFrontier) { |
|
if (!line.styles || line.styles[0] != cm.state.modeGen) { |
|
var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); |
|
line.styles = result.styles; |
|
if (result.classes) line.styleClasses = result.classes; |
|
else if (line.styleClasses) line.styleClasses = null; |
|
if (updateFrontier === cm.doc.frontier) cm.doc.frontier++; |
|
} |
|
return line.styles; |
|
} |
|
|
|
// Lightweight form of highlight -- proceed over this line and |
|
// update state, but don't save a style array. Used for lines that |
|
// aren't currently visible. |
|
function processLine(cm, text, state, startAt) { |
|
var mode = cm.doc.mode; |
|
var stream = new StringStream(text, cm.options.tabSize); |
|
stream.start = stream.pos = startAt || 0; |
|
if (text == "") callBlankLine(mode, state); |
|
while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { |
|
readToken(mode, stream, state); |
|
stream.start = stream.pos; |
|
} |
|
} |
|
|
|
// Convert a style as returned by a mode (either null, or a string |
|
// containing one or more styles) to a CSS style. This is cached, |
|
// and also looks for line-wide styles. |
|
var styleToClassCache = {}, styleToClassCacheWithMode = {}; |
|
function interpretTokenStyle(style, options) { |
|
if (!style || /^\s*$/.test(style)) return null; |
|
var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; |
|
return cache[style] || |
|
(cache[style] = style.replace(/\S+/g, "cm-$&")); |
|
} |
|
|
|
// Render the DOM representation of the text of a line. Also builds |
|
// up a 'line map', which points at the DOM nodes that represent |
|
// specific stretches of text, and is used by the measuring code. |
|
// The returned object contains the DOM node, this map, and |
|
// information about line-wide styles that were set by the mode. |
|
function buildLineContent(cm, lineView) { |
|
// The padding-right forces the element to have a 'border', which |
|
// is needed on Webkit to be able to get line-level bounding |
|
// rectangles for it (in measureChar). |
|
var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); |
|
var builder = {pre: elt("pre", [content], "CodeMirror-line"), content: content, |
|
col: 0, pos: 0, cm: cm, |
|
splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; |
|
lineView.measure = {}; |
|
|
|
// Iterate over the logical lines that make up this visual line. |
|
for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { |
|
var line = i ? lineView.rest[i - 1] : lineView.line, order; |
|
builder.pos = 0; |
|
builder.addToken = buildToken; |
|
// Optionally wire in some hacks into the token-rendering |
|
// algorithm, to deal with browser quirks. |
|
if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) |
|
builder.addToken = buildTokenBadBidi(builder.addToken, order); |
|
builder.map = []; |
|
var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); |
|
insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); |
|
if (line.styleClasses) { |
|
if (line.styleClasses.bgClass) |
|
builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); |
|
if (line.styleClasses.textClass) |
|
builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); |
|
} |
|
|
|
// Ensure at least a single node is present, for measuring. |
|
if (builder.map.length == 0) |
|
builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); |
|
|
|
// Store the map and a cache object for the current logical line |
|
if (i == 0) { |
|
lineView.measure.map = builder.map; |
|
lineView.measure.cache = {}; |
|
} else { |
|
(lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); |
|
(lineView.measure.caches || (lineView.measure.caches = [])).push({}); |
|
} |
|
} |
|
|
|
// See issue #2901 |
|
if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className)) |
|
builder.content.className = "cm-tab-wrap-hack"; |
|
|
|
signal(cm, "renderLine", cm, lineView.line, builder.pre); |
|
if (builder.pre.className) |
|
builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); |
|
|
|
return builder; |
|
} |
|
|
|
function defaultSpecialCharPlaceholder(ch) { |
|
var token = elt("span", "\u2022", "cm-invalidchar"); |
|
token.title = "\\u" + ch.charCodeAt(0).toString(16); |
|
token.setAttribute("aria-label", token.title); |
|
return token; |
|
} |
|
|
|
// Build up the DOM representation for a single token, and add it to |
|
// the line map. Takes care to render special characters separately. |
|
function buildToken(builder, text, style, startStyle, endStyle, title, css) { |
|
if (!text) return; |
|
var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text; |
|
var special = builder.cm.state.specialChars, mustWrap = false; |
|
if (!special.test(text)) { |
|
builder.col += text.length; |
|
var content = document.createTextNode(displayText); |
|
builder.map.push(builder.pos, builder.pos + text.length, content); |
|
if (ie && ie_version < 9) mustWrap = true; |
|
builder.pos += text.length; |
|
} else { |
|
var content = document.createDocumentFragment(), pos = 0; |
|
while (true) { |
|
special.lastIndex = pos; |
|
var m = special.exec(text); |
|
var skipped = m ? m.index - pos : text.length - pos; |
|
if (skipped) { |
|
var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); |
|
if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); |
|
else content.appendChild(txt); |
|
builder.map.push(builder.pos, builder.pos + skipped, txt); |
|
builder.col += skipped; |
|
builder.pos += skipped; |
|
} |
|
if (!m) break; |
|
pos += skipped + 1; |
|
if (m[0] == "\t") { |
|
var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; |
|
var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); |
|
txt.setAttribute("role", "presentation"); |
|
txt.setAttribute("cm-text", "\t"); |
|
builder.col += tabWidth; |
|
} else { |
|
var txt = builder.cm.options.specialCharPlaceholder(m[0]); |
|
txt.setAttribute("cm-text", m[0]); |
|
if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); |
|
else content.appendChild(txt); |
|
builder.col += 1; |
|
} |
|
builder.map.push(builder.pos, builder.pos + 1, txt); |
|
builder.pos++; |
|
} |
|
} |
|
if (style || startStyle || endStyle || mustWrap || css) { |
|
var fullStyle = style || ""; |
|
if (startStyle) fullStyle += startStyle; |
|
if (endStyle) fullStyle += endStyle; |
|
var token = elt("span", [content], fullStyle, css); |
|
if (title) token.title = title; |
|
return builder.content.appendChild(token); |
|
} |
|
builder.content.appendChild(content); |
|
} |
|
|
|
function splitSpaces(old) { |
|
var out = " "; |
|
for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; |
|
out += " "; |
|
return out; |
|
} |
|
|
|
// Work around nonsense dimensions being reported for stretches of |
|
// right-to-left text. |
|
function buildTokenBadBidi(inner, order) { |
|
return function(builder, text, style, startStyle, endStyle, title, css) { |
|
style = style ? style + " cm-force-border" : "cm-force-border"; |
|
var start = builder.pos, end = start + text.length; |
|
for (;;) { |
|
// Find the part that overlaps with the start of this text |
|
for (var i = 0; i < order.length; i++) { |
|
var part = order[i]; |
|
if (part.to > start && part.from <= start) break; |
|
} |
|
if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css); |
|
inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); |
|
startStyle = null; |
|
text = text.slice(part.to - start); |
|
start = part.to; |
|
} |
|
}; |
|
} |
|
|
|
function buildCollapsedSpan(builder, size, marker, ignoreWidget) { |
|
var widget = !ignoreWidget && marker.widgetNode; |
|
if (widget) builder.map.push(builder.pos, builder.pos + size, widget); |
|
if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { |
|
if (!widget) |
|
widget = builder.content.appendChild(document.createElement("span")); |
|
widget.setAttribute("cm-marker", marker.id); |
|
} |
|
if (widget) { |
|
builder.cm.display.input.setUneditable(widget); |
|
builder.content.appendChild(widget); |
|
} |
|
builder.pos += size; |
|
} |
|
|
|
// Outputs a number of spans to make up a line, taking highlighting |
|
// and marked text into account. |
|
function insertLineContent(line, builder, styles) { |
|
var spans = line.markedSpans, allText = line.text, at = 0; |
|
if (!spans) { |
|
for (var i = 1; i < styles.length; i+=2) |
|
builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); |
|
return; |
|
} |
|
|
|
var len = allText.length, pos = 0, i = 1, text = "", style, css; |
|
var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; |
|
for (;;) { |
|
if (nextChange == pos) { // Update current marker set |
|
spanStyle = spanEndStyle = spanStartStyle = title = css = ""; |
|
collapsed = null; nextChange = Infinity; |
|
var foundBookmarks = []; |
|
for (var j = 0; j < spans.length; ++j) { |
|
var sp = spans[j], m = sp.marker; |
|
if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { |
|
foundBookmarks.push(m); |
|
} else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { |
|
if (sp.to != null && sp.to != pos && nextChange > sp.to) { |
|
nextChange = sp.to; |
|
spanEndStyle = ""; |
|
} |
|
if (m.className) spanStyle += " " + m.className; |
|
if (m.css) css = m.css; |
|
if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; |
|
if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; |
|
if (m.title && !title) title = m.title; |
|
if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) |
|
collapsed = sp; |
|
} else if (sp.from > pos && nextChange > sp.from) { |
|
nextChange = sp.from; |
|
} |
|
} |
|
if (collapsed && (collapsed.from || 0) == pos) { |
|
buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, |
|
collapsed.marker, collapsed.from == null); |
|
if (collapsed.to == null) return; |
|
if (collapsed.to == pos) collapsed = false; |
|
} |
|
if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) |
|
buildCollapsedSpan(builder, 0, foundBookmarks[j]); |
|
} |
|
if (pos >= len) break; |
|
|
|
var upto = Math.min(len, nextChange); |
|
while (true) { |
|
if (text) { |
|
var end = pos + text.length; |
|
if (!collapsed) { |
|
var tokenText = end > upto ? text.slice(0, upto - pos) : text; |
|
builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, |
|
spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); |
|
} |
|
if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} |
|
pos = end; |
|
spanStartStyle = ""; |
|
} |
|
text = allText.slice(at, at = styles[i++]); |
|
style = interpretTokenStyle(styles[i++], builder.cm.options); |
|
} |
|
} |
|
} |
|
|
|
// DOCUMENT DATA STRUCTURE |
|
|
|
// By default, updates that start and end at the beginning of a line |
|
// are treated specially, in order to make the association of line |
|
// widgets and marker elements with the text behave more intuitive. |
|
function isWholeLineUpdate(doc, change) { |
|
return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && |
|
(!doc.cm || doc.cm.options.wholeLineUpdateBefore); |
|
} |
|
|
|
// Perform a change on the document data structure. |
|
function updateDoc(doc, change, markedSpans, estimateHeight) { |
|
function spansFor(n) {return markedSpans ? markedSpans[n] : null;} |
|
function update(line, text, spans) { |
|
updateLine(line, text, spans, estimateHeight); |
|
signalLater(line, "change", line, change); |
|
} |
|
function linesFor(start, end) { |
|
for (var i = start, result = []; i < end; ++i) |
|
result.push(new Line(text[i], spansFor(i), estimateHeight)); |
|
return result; |
|
} |
|
|
|
var from = change.from, to = change.to, text = change.text; |
|
var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); |
|
var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; |
|
|
|
// Adjust the line structure |
|
if (change.full) { |
|
doc.insert(0, linesFor(0, text.length)); |
|
doc.remove(text.length, doc.size - text.length); |
|
} else if (isWholeLineUpdate(doc, change)) { |
|
// This is a whole-line replace. Treated specially to make |
|
// sure line objects move the way they are supposed to. |
|
var added = linesFor(0, text.length - 1); |
|
update(lastLine, lastLine.text, lastSpans); |
|
if (nlines) doc.remove(from.line, nlines); |
|
if (added.length) doc.insert(from.line, added); |
|
} else if (firstLine == lastLine) { |
|
if (text.length == 1) { |
|
update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); |
|
} else { |
|
var added = linesFor(1, text.length - 1); |
|
added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); |
|
update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); |
|
doc.insert(from.line + 1, added); |
|
} |
|
} else if (text.length == 1) { |
|
update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); |
|
doc.remove(from.line + 1, nlines); |
|
} else { |
|
update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); |
|
update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); |
|
var added = linesFor(1, text.length - 1); |
|
if (nlines > 1) doc.remove(from.line + 1, nlines - 1); |
|
doc.insert(from.line + 1, added); |
|
} |
|
|
|
signalLater(doc, "change", doc, change); |
|
} |
|
|
|
// The document is represented as a BTree consisting of leaves, with |
|
// chunk of lines in them, and branches, with up to ten leaves or |
|
// other branch nodes below them. The top node is always a branch |
|
// node, and is the document object itself (meaning it has |
|
// additional methods and properties). |
|
// |
|
// All nodes have parent links. The tree is used both to go from |
|
// line numbers to line objects, and to go from objects to numbers. |
|
// It also indexes by height, and is used to convert between height |
|
// and line object, and to find the total height of the document. |
|
// |
|
// See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html |
|
|
|
function LeafChunk(lines) { |
|
this.lines = lines; |
|
this.parent = null; |
|
for (var i = 0, height = 0; i < lines.length; ++i) { |
|
lines[i].parent = this; |
|
height += lines[i].height; |
|
} |
|
this.height = height; |
|
} |
|
|
|
LeafChunk.prototype = { |
|
chunkSize: function() { return this.lines.length; }, |
|
// Remove the n lines at offset 'at'. |
|
removeInner: function(at, n) { |
|
for (var i = at, e = at + n; i < e; ++i) { |
|
var line = this.lines[i]; |
|
this.height -= line.height; |
|
cleanUpLine(line); |
|
signalLater(line, "delete"); |
|
} |
|
this.lines.splice(at, n); |
|
}, |
|
// Helper used to collapse a small branch into a single leaf. |
|
collapse: function(lines) { |
|
lines.push.apply(lines, this.lines); |
|
}, |
|
// Insert the given array of lines at offset 'at', count them as |
|
// having the given height. |
|
insertInner: function(at, lines, height) { |
|
this.height += height; |
|
this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); |
|
for (var i = 0; i < lines.length; ++i) lines[i].parent = this; |
|
}, |
|
// Used to iterate over a part of the tree. |
|
iterN: function(at, n, op) { |
|
for (var e = at + n; at < e; ++at) |
|
if (op(this.lines[at])) return true; |
|
} |
|
}; |
|
|
|
function BranchChunk(children) { |
|
this.children = children; |
|
var size = 0, height = 0; |
|
for (var i = 0; i < children.length; ++i) { |
|
var ch = children[i]; |
|
size += ch.chunkSize(); height += ch.height; |
|
ch.parent = this; |
|
} |
|
this.size = size; |
|
this.height = height; |
|
this.parent = null; |
|
} |
|
|
|
BranchChunk.prototype = { |
|
chunkSize: function() { return this.size; }, |
|
removeInner: function(at, n) { |
|
this.size -= n; |
|
for (var i = 0; i < this.children.length; ++i) { |
|
var child = this.children[i], sz = child.chunkSize(); |
|
if (at < sz) { |
|
var rm = Math.min(n, sz - at), oldHeight = child.height; |
|
child.removeInner(at, rm); |
|
this.height -= oldHeight - child.height; |
|
if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } |
|
if ((n -= rm) == 0) break; |
|
at = 0; |
|
} else at -= sz; |
|
} |
|
// If the result is smaller than 25 lines, ensure that it is a |
|
// single leaf node. |
|
if (this.size - n < 25 && |
|
(this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { |
|
var lines = []; |
|
this.collapse(lines); |
|
this.children = [new LeafChunk(lines)]; |
|
this.children[0].parent = this; |
|
} |
|
}, |
|
collapse: function(lines) { |
|
for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); |
|
}, |
|
insertInner: function(at, lines, height) { |
|
this.size += lines.length; |
|
this.height += height; |
|
for (var i = 0; i < this.children.length; ++i) { |
|
var child = this.children[i], sz = child.chunkSize(); |
|
if (at <= sz) { |
|
child.insertInner(at, lines, height); |
|
if (child.lines && child.lines.length > 50) { |
|
while (child.lines.length > 50) { |
|
var spilled = child.lines.splice(child.lines.length - 25, 25); |
|
var newleaf = new LeafChunk(spilled); |
|
child.height -= newleaf.height; |
|
this.children.splice(i + 1, 0, newleaf); |
|
newleaf.parent = this; |
|
} |
|
this.maybeSpill(); |
|
} |
|
break; |
|
} |
|
at -= sz; |
|
} |
|
}, |
|
// When a node has grown, check whether it should be split. |
|
maybeSpill: function() { |
|
if (this.children.length <= 10) return; |
|
var me = this; |
|
do { |
|
var spilled = me.children.splice(me.children.length - 5, 5); |
|
var sibling = new BranchChunk(spilled); |
|
if (!me.parent) { // Become the parent node |
|
var copy = new BranchChunk(me.children); |
|
copy.parent = me; |
|
me.children = [copy, sibling]; |
|
me = copy; |
|
} else { |
|
me.size -= sibling.size; |
|
me.height -= sibling.height; |
|
var myIndex = indexOf(me.parent.children, me); |
|
me.parent.children.splice(myIndex + 1, 0, sibling); |
|
} |
|
sibling.parent = me.parent; |
|
} while (me.children.length > 10); |
|
me.parent.maybeSpill(); |
|
}, |
|
iterN: function(at, n, op) { |
|
for (var i = 0; i < this.children.length; ++i) { |
|
var child = this.children[i], sz = child.chunkSize(); |
|
if (at < sz) { |
|
var used = Math.min(n, sz - at); |
|
if (child.iterN(at, used, op)) return true; |
|
if ((n -= used) == 0) break; |
|
at = 0; |
|
} else at -= sz; |
|
} |
|
} |
|
}; |
|
|
|
var nextDocId = 0; |
|
var Doc = CodeMirror.Doc = function(text, mode, firstLine) { |
|
if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); |
|
if (firstLine == null) firstLine = 0; |
|
|
|
BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); |
|
this.first = firstLine; |
|
this.scrollTop = this.scrollLeft = 0; |
|
this.cantEdit = false; |
|
this.cleanGeneration = 1; |
|
this.frontier = firstLine; |
|
var start = Pos(firstLine, 0); |
|
this.sel = simpleSelection(start); |
|
this.history = new History(null); |
|
this.id = ++nextDocId; |
|
this.modeOption = mode; |
|
|
|
if (typeof text == "string") text = splitLines(text); |
|
updateDoc(this, {from: start, to: start, text: text}); |
|
setSelection(this, simpleSelection(start), sel_dontScroll); |
|
}; |
|
|
|
Doc.prototype = createObj(BranchChunk.prototype, { |
|
constructor: Doc, |
|
// Iterate over the document. Supports two forms -- with only one |
|
// argument, it calls that for each line in the document. With |
|
// three, it iterates over the range given by the first two (with |
|
// the second being non-inclusive). |
|
iter: function(from, to, op) { |
|
if (op) this.iterN(from - this.first, to - from, op); |
|
else this.iterN(this.first, this.first + this.size, from); |
|
}, |
|
|
|
// Non-public interface for adding and removing lines. |
|
insert: function(at, lines) { |
|
var height = 0; |
|
for (var i = 0; i < lines.length; ++i) height += lines[i].height; |
|
this.insertInner(at - this.first, lines, height); |
|
}, |
|
remove: function(at, n) { this.removeInner(at - this.first, n); }, |
|
|
|
// From here, the methods are part of the public interface. Most |
|
// are also available from CodeMirror (editor) instances. |
|
|
|
getValue: function(lineSep, lineFilter) { |
|
var lines = getLines(this, this.first, this.first + this.size, lineFilter); |
|
if (lineSep === false) return lines; |
|
return lines.join(lineSep || "\n"); |
|
}, |
|
setValue: docMethodOp(function(code) { |
|
var top = Pos(this.first, 0), last = this.first + this.size - 1; |
|
makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), |
|
text: splitLines(code), origin: "setValue", full: true}, true); |
|
setSelection(this, simpleSelection(top)); |
|
}), |
|
replaceRange: function(code, from, to, origin) { |
|
from = clipPos(this, from); |
|
to = to ? clipPos(this, to) : from; |
|
replaceRange(this, code, from, to, origin); |
|
}, |
|
getRange: function(from, to, lineSep) { |
|
var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); |
|
if (lineSep === false) return lines; |
|
return lines.join(lineSep || "\n"); |
|
}, |
|
|
|
getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, |
|
|
|
getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, |
|
getLineNumber: function(line) {return lineNo(line);}, |
|
|
|
getLineHandleVisualStart: function(line) { |
|
if (typeof line == "number") line = getLine(this, line); |
|
return visualLine(line); |
|
}, |
|
|
|
lineCount: function() {return this.size;}, |
|
firstLine: function() {return this.first;}, |
|
lastLine: function() {return this.first + this.size - 1;}, |
|
|
|
clipPos: function(pos) {return clipPos(this, pos);}, |
|
|
|
getCursor: function(start) { |
|
var range = this.sel.primary(), pos; |
|
if (start == null || start == "head") pos = range.head; |
|
else if (start == "anchor") pos = range.anchor; |
|
else if (start == "end" || start == "to" || start === false) pos = range.to(); |
|
else pos = range.from(); |
|
return pos; |
|
}, |
|
listSelections: function() { return this.sel.ranges; }, |
|
somethingSelected: function() {return this.sel.somethingSelected();}, |
|
|
|
setCursor: docMethodOp(function(line, ch, options) { |
|
setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); |
|
}), |
|
setSelection: docMethodOp(function(anchor, head, options) { |
|
setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); |
|
}), |
|
extendSelection: docMethodOp(function(head, other, options) { |
|
extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); |
|
}), |
|
extendSelections: docMethodOp(function(heads, options) { |
|
extendSelections(this, clipPosArray(this, heads, options)); |
|
}), |
|
extendSelectionsBy: docMethodOp(function(f, options) { |
|
extendSelections(this, map(this.sel.ranges, f), options); |
|
}), |
|
setSelections: docMethodOp(function(ranges, primary, options) { |
|
if (!ranges.length) return; |
|
for (var i = 0, out = []; i < ranges.length; i++) |
|
out[i] = new Range(clipPos(this, ranges[i].anchor), |
|
clipPos(this, ranges[i].head)); |
|
if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); |
|
setSelection(this, normalizeSelection(out, primary), options); |
|
}), |
|
addSelection: docMethodOp(function(anchor, head, options) { |
|
var ranges = this.sel.ranges.slice(0); |
|
ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); |
|
setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); |
|
}), |
|
|
|
getSelection: function(lineSep) { |
|
var ranges = this.sel.ranges, lines; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var sel = getBetween(this, ranges[i].from(), ranges[i].to()); |
|
lines = lines ? lines.concat(sel) : sel; |
|
} |
|
if (lineSep === false) return lines; |
|
else return lines.join(lineSep || "\n"); |
|
}, |
|
getSelections: function(lineSep) { |
|
var parts = [], ranges = this.sel.ranges; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var sel = getBetween(this, ranges[i].from(), ranges[i].to()); |
|
if (lineSep !== false) sel = sel.join(lineSep || "\n"); |
|
parts[i] = sel; |
|
} |
|
return parts; |
|
}, |
|
replaceSelection: function(code, collapse, origin) { |
|
var dup = []; |
|
for (var i = 0; i < this.sel.ranges.length; i++) |
|
dup[i] = code; |
|
this.replaceSelections(dup, collapse, origin || "+input"); |
|
}, |
|
replaceSelections: docMethodOp(function(code, collapse, origin) { |
|
var changes = [], sel = this.sel; |
|
for (var i = 0; i < sel.ranges.length; i++) { |
|
var range = sel.ranges[i]; |
|
changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; |
|
} |
|
var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); |
|
for (var i = changes.length - 1; i >= 0; i--) |
|
makeChange(this, changes[i]); |
|
if (newSel) setSelectionReplaceHistory(this, newSel); |
|
else if (this.cm) ensureCursorVisible(this.cm); |
|
}), |
|
undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), |
|
redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), |
|
undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), |
|
redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), |
|
|
|
setExtending: function(val) {this.extend = val;}, |
|
getExtending: function() {return this.extend;}, |
|
|
|
historySize: function() { |
|
var hist = this.history, done = 0, undone = 0; |
|
for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; |
|
for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; |
|
return {undo: done, redo: undone}; |
|
}, |
|
clearHistory: function() {this.history = new History(this.history.maxGeneration);}, |
|
|
|
markClean: function() { |
|
this.cleanGeneration = this.changeGeneration(true); |
|
}, |
|
changeGeneration: function(forceSplit) { |
|
if (forceSplit) |
|
this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; |
|
return this.history.generation; |
|
}, |
|
isClean: function (gen) { |
|
return this.history.generation == (gen || this.cleanGeneration); |
|
}, |
|
|
|
getHistory: function() { |
|
return {done: copyHistoryArray(this.history.done), |
|
undone: copyHistoryArray(this.history.undone)}; |
|
}, |
|
setHistory: function(histData) { |
|
var hist = this.history = new History(this.history.maxGeneration); |
|
hist.done = copyHistoryArray(histData.done.slice(0), null, true); |
|
hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); |
|
}, |
|
|
|
addLineClass: docMethodOp(function(handle, where, cls) { |
|
return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { |
|
var prop = where == "text" ? "textClass" |
|
: where == "background" ? "bgClass" |
|
: where == "gutter" ? "gutterClass" : "wrapClass"; |
|
if (!line[prop]) line[prop] = cls; |
|
else if (classTest(cls).test(line[prop])) return false; |
|
else line[prop] += " " + cls; |
|
return true; |
|
}); |
|
}), |
|
removeLineClass: docMethodOp(function(handle, where, cls) { |
|
return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { |
|
var prop = where == "text" ? "textClass" |
|
: where == "background" ? "bgClass" |
|
: where == "gutter" ? "gutterClass" : "wrapClass"; |
|
var cur = line[prop]; |
|
if (!cur) return false; |
|
else if (cls == null) line[prop] = null; |
|
else { |
|
var found = cur.match(classTest(cls)); |
|
if (!found) return false; |
|
var end = found.index + found[0].length; |
|
line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; |
|
} |
|
return true; |
|
}); |
|
}), |
|
|
|
addLineWidget: docMethodOp(function(handle, node, options) { |
|
return addLineWidget(this, handle, node, options); |
|
}), |
|
removeLineWidget: function(widget) { widget.clear(); }, |
|
|
|
markText: function(from, to, options) { |
|
return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); |
|
}, |
|
setBookmark: function(pos, options) { |
|
var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), |
|
insertLeft: options && options.insertLeft, |
|
clearWhenEmpty: false, shared: options && options.shared, |
|
handleMouseEvents: options && options.handleMouseEvents}; |
|
pos = clipPos(this, pos); |
|
return markText(this, pos, pos, realOpts, "bookmark"); |
|
}, |
|
findMarksAt: function(pos) { |
|
pos = clipPos(this, pos); |
|
var markers = [], spans = getLine(this, pos.line).markedSpans; |
|
if (spans) for (var i = 0; i < spans.length; ++i) { |
|
var span = spans[i]; |
|
if ((span.from == null || span.from <= pos.ch) && |
|
(span.to == null || span.to >= pos.ch)) |
|
markers.push(span.marker.parent || span.marker); |
|
} |
|
return markers; |
|
}, |
|
findMarks: function(from, to, filter) { |
|
from = clipPos(this, from); to = clipPos(this, to); |
|
var found = [], lineNo = from.line; |
|
this.iter(from.line, to.line + 1, function(line) { |
|
var spans = line.markedSpans; |
|
if (spans) for (var i = 0; i < spans.length; i++) { |
|
var span = spans[i]; |
|
if (!(lineNo == from.line && from.ch > span.to || |
|
span.from == null && lineNo != from.line|| |
|
lineNo == to.line && span.from > to.ch) && |
|
(!filter || filter(span.marker))) |
|
found.push(span.marker.parent || span.marker); |
|
} |
|
++lineNo; |
|
}); |
|
return found; |
|
}, |
|
getAllMarks: function() { |
|
var markers = []; |
|
this.iter(function(line) { |
|
var sps = line.markedSpans; |
|
if (sps) for (var i = 0; i < sps.length; ++i) |
|
if (sps[i].from != null) markers.push(sps[i].marker); |
|
}); |
|
return markers; |
|
}, |
|
|
|
posFromIndex: function(off) { |
|
var ch, lineNo = this.first; |
|
this.iter(function(line) { |
|
var sz = line.text.length + 1; |
|
if (sz > off) { ch = off; return true; } |
|
off -= sz; |
|
++lineNo; |
|
}); |
|
return clipPos(this, Pos(lineNo, ch)); |
|
}, |
|
indexFromPos: function (coords) { |
|
coords = clipPos(this, coords); |
|
var index = coords.ch; |
|
if (coords.line < this.first || coords.ch < 0) return 0; |
|
this.iter(this.first, coords.line, function (line) { |
|
index += line.text.length + 1; |
|
}); |
|
return index; |
|
}, |
|
|
|
copy: function(copyHistory) { |
|
var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); |
|
doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; |
|
doc.sel = this.sel; |
|
doc.extend = false; |
|
if (copyHistory) { |
|
doc.history.undoDepth = this.history.undoDepth; |
|
doc.setHistory(this.getHistory()); |
|
} |
|
return doc; |
|
}, |
|
|
|
linkedDoc: function(options) { |
|
if (!options) options = {}; |
|
var from = this.first, to = this.first + this.size; |
|
if (options.from != null && options.from > from) from = options.from; |
|
if (options.to != null && options.to < to) to = options.to; |
|
var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); |
|
if (options.sharedHist) copy.history = this.history; |
|
(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); |
|
copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; |
|
copySharedMarkers(copy, findSharedMarkers(this)); |
|
return copy; |
|
}, |
|
unlinkDoc: function(other) { |
|
if (other instanceof CodeMirror) other = other.doc; |
|
if (this.linked) for (var i = 0; i < this.linked.length; ++i) { |
|
var link = this.linked[i]; |
|
if (link.doc != other) continue; |
|
this.linked.splice(i, 1); |
|
other.unlinkDoc(this); |
|
detachSharedMarkers(findSharedMarkers(this)); |
|
break; |
|
} |
|
// If the histories were shared, split them again |
|
if (other.history == this.history) { |
|
var splitIds = [other.id]; |
|
linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); |
|
other.history = new History(null); |
|
other.history.done = copyHistoryArray(this.history.done, splitIds); |
|
other.history.undone = copyHistoryArray(this.history.undone, splitIds); |
|
} |
|
}, |
|
iterLinkedDocs: function(f) {linkedDocs(this, f);}, |
|
|
|
getMode: function() {return this.mode;}, |
|
getEditor: function() {return this.cm;} |
|
}); |
|
|
|
// Public alias. |
|
Doc.prototype.eachLine = Doc.prototype.iter; |
|
|
|
// Set up methods on CodeMirror's prototype to redirect to the editor's document. |
|
var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); |
|
for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) |
|
CodeMirror.prototype[prop] = (function(method) { |
|
return function() {return method.apply(this.doc, arguments);}; |
|
})(Doc.prototype[prop]); |
|
|
|
eventMixin(Doc); |
|
|
|
// Call f for all linked documents. |
|
function linkedDocs(doc, f, sharedHistOnly) { |
|
function propagate(doc, skip, sharedHist) { |
|
if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { |
|
var rel = doc.linked[i]; |
|
if (rel.doc == skip) continue; |
|
var shared = sharedHist && rel.sharedHist; |
|
if (sharedHistOnly && !shared) continue; |
|
f(rel.doc, shared); |
|
propagate(rel.doc, doc, shared); |
|
} |
|
} |
|
propagate(doc, null, true); |
|
} |
|
|
|
// Attach a document to an editor. |
|
function attachDoc(cm, doc) { |
|
if (doc.cm) throw new Error("This document is already in use."); |
|
cm.doc = doc; |
|
doc.cm = cm; |
|
estimateLineHeights(cm); |
|
loadMode(cm); |
|
if (!cm.options.lineWrapping) findMaxLine(cm); |
|
cm.options.mode = doc.modeOption; |
|
regChange(cm); |
|
} |
|
|
|
// LINE UTILITIES |
|
|
|
// Find the line object corresponding to the given line number. |
|
function getLine(doc, n) { |
|
n -= doc.first; |
|
if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); |
|
for (var chunk = doc; !chunk.lines;) { |
|
for (var i = 0;; ++i) { |
|
var child = chunk.children[i], sz = child.chunkSize(); |
|
if (n < sz) { chunk = child; break; } |
|
n -= sz; |
|
} |
|
} |
|
return chunk.lines[n]; |
|
} |
|
|
|
// Get the part of a document between two positions, as an array of |
|
// strings. |
|
function getBetween(doc, start, end) { |
|
var out = [], n = start.line; |
|
doc.iter(start.line, end.line + 1, function(line) { |
|
var text = line.text; |
|
if (n == end.line) text = text.slice(0, end.ch); |
|
if (n == start.line) text = text.slice(start.ch); |
|
out.push(text); |
|
++n; |
|
}); |
|
return out; |
|
} |
|
// Get the lines between from and to, as array of strings. |
|
function getLines(doc, from, to, lineFilter) { |
|
var out = []; |
|
doc.iter(from, to, function(line) { |
|
var text = line.text; |
|
if(lineFilter){ |
|
text = lineFilter(line); |
|
} |
|
out.push(text); |
|
}); |
|
return out; |
|
} |
|
|
|
// Update the height of a line, propagating the height change |
|
// upwards to parent nodes. |
|
function updateLineHeight(line, height) { |
|
var diff = height - line.height; |
|
if (diff) for (var n = line; n; n = n.parent) n.height += diff; |
|
} |
|
|
|
// Given a line object, find its line number by walking up through |
|
// its parent links. |
|
function lineNo(line) { |
|
if (line.parent == null) return null; |
|
var cur = line.parent, no = indexOf(cur.lines, line); |
|
for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { |
|
for (var i = 0;; ++i) { |
|
if (chunk.children[i] == cur) break; |
|
no += chunk.children[i].chunkSize(); |
|
} |
|
} |
|
return no + cur.first; |
|
} |
|
|
|
// Find the line at the given vertical position, using the height |
|
// information in the document tree. |
|
function lineAtHeight(chunk, h) { |
|
var n = chunk.first; |
|
outer: do { |
|
for (var i = 0; i < chunk.children.length; ++i) { |
|
var child = chunk.children[i], ch = child.height; |
|
if (h < ch) { chunk = child; continue outer; } |
|
h -= ch; |
|
n += child.chunkSize(); |
|
} |
|
return n; |
|
} while (!chunk.lines); |
|
for (var i = 0; i < chunk.lines.length; ++i) { |
|
var line = chunk.lines[i], lh = line.height; |
|
if (h < lh) break; |
|
h -= lh; |
|
} |
|
return n + i; |
|
} |
|
|
|
|
|
// Find the height above the given line. |
|
function heightAtLine(lineObj) { |
|
lineObj = visualLine(lineObj); |
|
|
|
var h = 0, chunk = lineObj.parent; |
|
for (var i = 0; i < chunk.lines.length; ++i) { |
|
var line = chunk.lines[i]; |
|
if (line == lineObj) break; |
|
else h += line.height; |
|
} |
|
for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { |
|
for (var i = 0; i < p.children.length; ++i) { |
|
var cur = p.children[i]; |
|
if (cur == chunk) break; |
|
else h += cur.height; |
|
} |
|
} |
|
return h; |
|
} |
|
|
|
// Get the bidi ordering for the given line (and cache it). Returns |
|
// false for lines that are fully left-to-right, and an array of |
|
// BidiSpan objects otherwise. |
|
function getOrder(line) { |
|
var order = line.order; |
|
if (order == null) order = line.order = bidiOrdering(line.text); |
|
return order; |
|
} |
|
|
|
// HISTORY |
|
|
|
function History(startGen) { |
|
// Arrays of change events and selections. Doing something adds an |
|
// event to done and clears undo. Undoing moves events from done |
|
// to undone, redoing moves them in the other direction. |
|
this.done = []; this.undone = []; |
|
this.undoDepth = Infinity; |
|
// Used to track when changes can be merged into a single undo |
|
// event |
|
this.lastModTime = this.lastSelTime = 0; |
|
this.lastOp = this.lastSelOp = null; |
|
this.lastOrigin = this.lastSelOrigin = null; |
|
// Used by the isClean() method |
|
this.generation = this.maxGeneration = startGen || 1; |
|
} |
|
|
|
// Create a history change event from an updateDoc-style change |
|
// object. |
|
function historyChangeFromChange(doc, change) { |
|
var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; |
|
attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); |
|
linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); |
|
return histChange; |
|
} |
|
|
|
// Pop all selection events off the end of a history array. Stop at |
|
// a change event. |
|
function clearSelectionEvents(array) { |
|
while (array.length) { |
|
var last = lst(array); |
|
if (last.ranges) array.pop(); |
|
else break; |
|
} |
|
} |
|
|
|
// Find the top change event in the history. Pop off selection |
|
// events that are in the way. |
|
function lastChangeEvent(hist, force) { |
|
if (force) { |
|
clearSelectionEvents(hist.done); |
|
return lst(hist.done); |
|
} else if (hist.done.length && !lst(hist.done).ranges) { |
|
return lst(hist.done); |
|
} else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { |
|
hist.done.pop(); |
|
return lst(hist.done); |
|
} |
|
} |
|
|
|
// Register a change in the history. Merges changes that are within |
|
// a single operation, ore are close together with an origin that |
|
// allows merging (starting with "+") into a single event. |
|
function addChangeToHistory(doc, change, selAfter, opId) { |
|
var hist = doc.history; |
|
hist.undone.length = 0; |
|
var time = +new Date, cur; |
|
|
|
if ((hist.lastOp == opId || |
|
hist.lastOrigin == change.origin && change.origin && |
|
((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || |
|
change.origin.charAt(0) == "*")) && |
|
(cur = lastChangeEvent(hist, hist.lastOp == opId))) { |
|
// Merge this change into the last event |
|
var last = lst(cur.changes); |
|
if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { |
|
// Optimized case for simple insertion -- don't want to add |
|
// new changesets for every character typed |
|
last.to = changeEnd(change); |
|
} else { |
|
// Add new sub-event |
|
cur.changes.push(historyChangeFromChange(doc, change)); |
|
} |
|
} else { |
|
// Can not be merged, start a new event. |
|
var before = lst(hist.done); |
|
if (!before || !before.ranges) |
|
pushSelectionToHistory(doc.sel, hist.done); |
|
cur = {changes: [historyChangeFromChange(doc, change)], |
|
generation: hist.generation}; |
|
hist.done.push(cur); |
|
while (hist.done.length > hist.undoDepth) { |
|
hist.done.shift(); |
|
if (!hist.done[0].ranges) hist.done.shift(); |
|
} |
|
} |
|
hist.done.push(selAfter); |
|
hist.generation = ++hist.maxGeneration; |
|
hist.lastModTime = hist.lastSelTime = time; |
|
hist.lastOp = hist.lastSelOp = opId; |
|
hist.lastOrigin = hist.lastSelOrigin = change.origin; |
|
|
|
if (!last) signal(doc, "historyAdded"); |
|
} |
|
|
|
function selectionEventCanBeMerged(doc, origin, prev, sel) { |
|
var ch = origin.charAt(0); |
|
return ch == "*" || |
|
ch == "+" && |
|
prev.ranges.length == sel.ranges.length && |
|
prev.somethingSelected() == sel.somethingSelected() && |
|
new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); |
|
} |
|
|
|
// Called whenever the selection changes, sets the new selection as |
|
// the pending selection in the history, and pushes the old pending |
|
// selection into the 'done' array when it was significantly |
|
// different (in number of selected ranges, emptiness, or time). |
|
function addSelectionToHistory(doc, sel, opId, options) { |
|
var hist = doc.history, origin = options && options.origin; |
|
|
|
// A new event is started when the previous origin does not match |
|
// the current, or the origins don't allow matching. Origins |
|
// starting with * are always merged, those starting with + are |
|
// merged when similar and close together in time. |
|
if (opId == hist.lastSelOp || |
|
(origin && hist.lastSelOrigin == origin && |
|
(hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || |
|
selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) |
|
hist.done[hist.done.length - 1] = sel; |
|
else |
|
pushSelectionToHistory(sel, hist.done); |
|
|
|
hist.lastSelTime = +new Date; |
|
hist.lastSelOrigin = origin; |
|
hist.lastSelOp = opId; |
|
if (options && options.clearRedo !== false) |
|
clearSelectionEvents(hist.undone); |
|
} |
|
|
|
function pushSelectionToHistory(sel, dest) { |
|
var top = lst(dest); |
|
if (!(top && top.ranges && top.equals(sel))) |
|
dest.push(sel); |
|
} |
|
|
|
// Used to store marked span information in the history. |
|
function attachLocalSpans(doc, change, from, to) { |
|
var existing = change["spans_" + doc.id], n = 0; |
|
doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { |
|
if (line.markedSpans) |
|
(existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; |
|
++n; |
|
}); |
|
} |
|
|
|
// When un/re-doing restores text containing marked spans, those |
|
// that have been explicitly cleared should not be restored. |
|
function removeClearedSpans(spans) { |
|
if (!spans) return null; |
|
for (var i = 0, out; i < spans.length; ++i) { |
|
if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } |
|
else if (out) out.push(spans[i]); |
|
} |
|
return !out ? spans : out.length ? out : null; |
|
} |
|
|
|
// Retrieve and filter the old marked spans stored in a change event. |
|
function getOldSpans(doc, change) { |
|
var found = change["spans_" + doc.id]; |
|
if (!found) return null; |
|
for (var i = 0, nw = []; i < change.text.length; ++i) |
|
nw.push(removeClearedSpans(found[i])); |
|
return nw; |
|
} |
|
|
|
// Used both to provide a JSON-safe object in .getHistory, and, when |
|
// detaching a document, to split the history in two |
|
function copyHistoryArray(events, newGroup, instantiateSel) { |
|
for (var i = 0, copy = []; i < events.length; ++i) { |
|
var event = events[i]; |
|
if (event.ranges) { |
|
copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); |
|
continue; |
|
} |
|
var changes = event.changes, newChanges = []; |
|
copy.push({changes: newChanges}); |
|
for (var j = 0; j < changes.length; ++j) { |
|
var change = changes[j], m; |
|
newChanges.push({from: change.from, to: change.to, text: change.text}); |
|
if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { |
|
if (indexOf(newGroup, Number(m[1])) > -1) { |
|
lst(newChanges)[prop] = change[prop]; |
|
delete change[prop]; |
|
} |
|
} |
|
} |
|
} |
|
return copy; |
|
} |
|
|
|
// Rebasing/resetting history to deal with externally-sourced changes |
|
|
|
function rebaseHistSelSingle(pos, from, to, diff) { |
|
if (to < pos.line) { |
|
pos.line += diff; |
|
} else if (from < pos.line) { |
|
pos.line = from; |
|
pos.ch = 0; |
|
} |
|
} |
|
|
|
// Tries to rebase an array of history events given a change in the |
|
// document. If the change touches the same lines as the event, the |
|
// event, and everything 'behind' it, is discarded. If the change is |
|
// before the event, the event's positions are updated. Uses a |
|
// copy-on-write scheme for the positions, to avoid having to |
|
// reallocate them all on every rebase, but also avoid problems with |
|
// shared position objects being unsafely updated. |
|
function rebaseHistArray(array, from, to, diff) { |
|
for (var i = 0; i < array.length; ++i) { |
|
var sub = array[i], ok = true; |
|
if (sub.ranges) { |
|
if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } |
|
for (var j = 0; j < sub.ranges.length; j++) { |
|
rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); |
|
rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); |
|
} |
|
continue; |
|
} |
|
for (var j = 0; j < sub.changes.length; ++j) { |
|
var cur = sub.changes[j]; |
|
if (to < cur.from.line) { |
|
cur.from = Pos(cur.from.line + diff, cur.from.ch); |
|
cur.to = Pos(cur.to.line + diff, cur.to.ch); |
|
} else if (from <= cur.to.line) { |
|
ok = false; |
|
break; |
|
} |
|
} |
|
if (!ok) { |
|
array.splice(0, i + 1); |
|
i = 0; |
|
} |
|
} |
|
} |
|
|
|
function rebaseHist(hist, change) { |
|
var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; |
|
rebaseHistArray(hist.done, from, to, diff); |
|
rebaseHistArray(hist.undone, from, to, diff); |
|
} |
|
|
|
// EVENT UTILITIES |
|
|
|
// Due to the fact that we still support jurassic IE versions, some |
|
// compatibility wrappers are needed. |
|
|
|
var e_preventDefault = CodeMirror.e_preventDefault = function(e) { |
|
if (e.preventDefault) e.preventDefault(); |
|
else e.returnValue = false; |
|
}; |
|
var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { |
|
if (e.stopPropagation) e.stopPropagation(); |
|
else e.cancelBubble = true; |
|
}; |
|
function e_defaultPrevented(e) { |
|
return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; |
|
} |
|
var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; |
|
|
|
function e_target(e) {return e.target || e.srcElement;} |
|
function e_button(e) { |
|
var b = e.which; |
|
if (b == null) { |
|
if (e.button & 1) b = 1; |
|
else if (e.button & 2) b = 3; |
|
else if (e.button & 4) b = 2; |
|
} |
|
if (mac && e.ctrlKey && b == 1) b = 3; |
|
return b; |
|
} |
|
|
|
// EVENT HANDLING |
|
|
|
// Lightweight event framework. on/off also work on DOM nodes, |
|
// registering native DOM handlers. |
|
|
|
var on = CodeMirror.on = function(emitter, type, f) { |
|
if (emitter.addEventListener) |
|
emitter.addEventListener(type, f, false); |
|
else if (emitter.attachEvent) |
|
emitter.attachEvent("on" + type, f); |
|
else { |
|
var map = emitter._handlers || (emitter._handlers = {}); |
|
var arr = map[type] || (map[type] = []); |
|
arr.push(f); |
|
} |
|
}; |
|
|
|
var off = CodeMirror.off = function(emitter, type, f) { |
|
if (emitter.removeEventListener) |
|
emitter.removeEventListener(type, f, false); |
|
else if (emitter.detachEvent) |
|
emitter.detachEvent("on" + type, f); |
|
else { |
|
var arr = emitter._handlers && emitter._handlers[type]; |
|
if (!arr) return; |
|
for (var i = 0; i < arr.length; ++i) |
|
if (arr[i] == f) { arr.splice(i, 1); break; } |
|
} |
|
}; |
|
|
|
var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { |
|
var arr = emitter._handlers && emitter._handlers[type]; |
|
if (!arr) return; |
|
var args = Array.prototype.slice.call(arguments, 2); |
|
for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); |
|
}; |
|
|
|
var orphanDelayedCallbacks = null; |
|
|
|
// Often, we want to signal events at a point where we are in the |
|
// middle of some work, but don't want the handler to start calling |
|
// other methods on the editor, which might be in an inconsistent |
|
// state or simply not expect any other events to happen. |
|
// signalLater looks whether there are any handlers, and schedules |
|
// them to be executed when the last operation ends, or, if no |
|
// operation is active, when a timeout fires. |
|
function signalLater(emitter, type /*, values...*/) { |
|
var arr = emitter._handlers && emitter._handlers[type]; |
|
if (!arr) return; |
|
var args = Array.prototype.slice.call(arguments, 2), list; |
|
if (operationGroup) { |
|
list = operationGroup.delayedCallbacks; |
|
} else if (orphanDelayedCallbacks) { |
|
list = orphanDelayedCallbacks; |
|
} else { |
|
list = orphanDelayedCallbacks = []; |
|
setTimeout(fireOrphanDelayed, 0); |
|
} |
|
function bnd(f) {return function(){f.apply(null, args);};}; |
|
for (var i = 0; i < arr.length; ++i) |
|
list.push(bnd(arr[i])); |
|
} |
|
|
|
function fireOrphanDelayed() { |
|
var delayed = orphanDelayedCallbacks; |
|
orphanDelayedCallbacks = null; |
|
for (var i = 0; i < delayed.length; ++i) delayed[i](); |
|
} |
|
|
|
// The DOM events that CodeMirror handles can be overridden by |
|
// registering a (non-DOM) handler on the editor for the event name, |
|
// and preventDefault-ing the event in that handler. |
|
function signalDOMEvent(cm, e, override) { |
|
if (typeof e == "string") |
|
e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; |
|
signal(cm, override || e.type, cm, e); |
|
return e_defaultPrevented(e) || e.codemirrorIgnore; |
|
} |
|
|
|
function signalCursorActivity(cm) { |
|
var arr = cm._handlers && cm._handlers.cursorActivity; |
|
if (!arr) return; |
|
var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); |
|
for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) |
|
set.push(arr[i]); |
|
} |
|
|
|
function hasHandler(emitter, type) { |
|
var arr = emitter._handlers && emitter._handlers[type]; |
|
return arr && arr.length > 0; |
|
} |
|
|
|
// Add on and off methods to a constructor's prototype, to make |
|
// registering events on such objects more convenient. |
|
function eventMixin(ctor) { |
|
ctor.prototype.on = function(type, f) {on(this, type, f);}; |
|
ctor.prototype.off = function(type, f) {off(this, type, f);}; |
|
} |
|
|
|
// MISC UTILITIES |
|
|
|
// Number of pixels added to scroller and sizer to hide scrollbar |
|
var scrollerGap = 30; |
|
|
|
// Returned or thrown by various protocols to signal 'I'm not |
|
// handling this'. |
|
var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; |
|
|
|
// Reused option objects for setSelection & friends |
|
var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; |
|
|
|
function Delayed() {this.id = null;} |
|
Delayed.prototype.set = function(ms, f) { |
|
clearTimeout(this.id); |
|
this.id = setTimeout(f, ms); |
|
}; |
|
|
|
// Counts the column offset in a string, taking tabs into account. |
|
// Used mostly to find indentation. |
|
var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { |
|
if (end == null) { |
|
end = string.search(/[^\s\u00a0]/); |
|
if (end == -1) end = string.length; |
|
} |
|
for (var i = startIndex || 0, n = startValue || 0;;) { |
|
var nextTab = string.indexOf("\t", i); |
|
if (nextTab < 0 || nextTab >= end) |
|
return n + (end - i); |
|
n += nextTab - i; |
|
n += tabSize - (n % tabSize); |
|
i = nextTab + 1; |
|
} |
|
}; |
|
|
|
// The inverse of countColumn -- find the offset that corresponds to |
|
// a particular column. |
|
function findColumn(string, goal, tabSize) { |
|
for (var pos = 0, col = 0;;) { |
|
var nextTab = string.indexOf("\t", pos); |
|
if (nextTab == -1) nextTab = string.length; |
|
var skipped = nextTab - pos; |
|
if (nextTab == string.length || col + skipped >= goal) |
|
return pos + Math.min(skipped, goal - col); |
|
col += nextTab - pos; |
|
col += tabSize - (col % tabSize); |
|
pos = nextTab + 1; |
|
if (col >= goal) return pos; |
|
} |
|
} |
|
|
|
var spaceStrs = [""]; |
|
function spaceStr(n) { |
|
while (spaceStrs.length <= n) |
|
spaceStrs.push(lst(spaceStrs) + " "); |
|
return spaceStrs[n]; |
|
} |
|
|
|
function lst(arr) { return arr[arr.length-1]; } |
|
|
|
var selectInput = function(node) { node.select(); }; |
|
if (ios) // Mobile Safari apparently has a bug where select() is broken. |
|
selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; |
|
else if (ie) // Suppress mysterious IE10 errors |
|
selectInput = function(node) { try { node.select(); } catch(_e) {} }; |
|
|
|
function indexOf(array, elt) { |
|
for (var i = 0; i < array.length; ++i) |
|
if (array[i] == elt) return i; |
|
return -1; |
|
} |
|
function map(array, f) { |
|
var out = []; |
|
for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); |
|
return out; |
|
} |
|
|
|
function nothing() {} |
|
|
|
function createObj(base, props) { |
|
var inst; |
|
if (Object.create) { |
|
inst = Object.create(base); |
|
} else { |
|
nothing.prototype = base; |
|
inst = new nothing(); |
|
} |
|
if (props) copyObj(props, inst); |
|
return inst; |
|
}; |
|
|
|
function copyObj(obj, target, overwrite) { |
|
if (!target) target = {}; |
|
for (var prop in obj) |
|
if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) |
|
target[prop] = obj[prop]; |
|
return target; |
|
} |
|
|
|
function bind(f) { |
|
var args = Array.prototype.slice.call(arguments, 1); |
|
return function(){return f.apply(null, args);}; |
|
} |
|
|
|
var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; |
|
var isWordCharBasic = CodeMirror.isWordChar = function(ch) { |
|
return /\w/.test(ch) || ch > "\x80" && |
|
(ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); |
|
}; |
|
function isWordChar(ch, helper) { |
|
if (!helper) return isWordCharBasic(ch); |
|
if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; |
|
return helper.test(ch); |
|
} |
|
|
|
function isEmpty(obj) { |
|
for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; |
|
return true; |
|
} |
|
|
|
// Extending unicode characters. A series of a non-extending char + |
|
// any number of extending chars is treated as a single unit as far |
|
// as editing and measuring is concerned. This is not fully correct, |
|
// since some scripts/font/browsers also treat other configurations |
|
// of code points as a group. |
|
var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; |
|
function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } |
|
|
|
// DOM UTILITIES |
|
|
|
function elt(tag, content, className, style) { |
|
var e = document.createElement(tag); |
|
if (className) e.className = className; |
|
if (style) e.style.cssText = style; |
|
if (typeof content == "string") e.appendChild(document.createTextNode(content)); |
|
else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); |
|
return e; |
|
} |
|
|
|
var range; |
|
if (document.createRange) range = function(node, start, end, endNode) { |
|
var r = document.createRange(); |
|
r.setEnd(endNode || node, end); |
|
r.setStart(node, start); |
|
return r; |
|
}; |
|
else range = function(node, start, end) { |
|
var r = document.body.createTextRange(); |
|
try { r.moveToElementText(node.parentNode); } |
|
catch(e) { return r; } |
|
r.collapse(true); |
|
r.moveEnd("character", end); |
|
r.moveStart("character", start); |
|
return r; |
|
}; |
|
|
|
function removeChildren(e) { |
|
for (var count = e.childNodes.length; count > 0; --count) |
|
e.removeChild(e.firstChild); |
|
return e; |
|
} |
|
|
|
function removeChildrenAndAdd(parent, e) { |
|
return removeChildren(parent).appendChild(e); |
|
} |
|
|
|
var contains = CodeMirror.contains = function(parent, child) { |
|
if (child.nodeType == 3) // Android browser always returns false when child is a textnode |
|
child = child.parentNode; |
|
if (parent.contains) |
|
return parent.contains(child); |
|
do { |
|
if (child.nodeType == 11) child = child.host; |
|
if (child == parent) return true; |
|
} while (child = child.parentNode); |
|
}; |
|
|
|
function activeElt() { return document.activeElement; } |
|
// Older versions of IE throws unspecified error when touching |
|
// document.activeElement in some cases (during loading, in iframe) |
|
if (ie && ie_version < 11) activeElt = function() { |
|
try { return document.activeElement; } |
|
catch(e) { return document.body; } |
|
}; |
|
|
|
function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); } |
|
var rmClass = CodeMirror.rmClass = function(node, cls) { |
|
var current = node.className; |
|
var match = classTest(cls).exec(current); |
|
if (match) { |
|
var after = current.slice(match.index + match[0].length); |
|
node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); |
|
} |
|
}; |
|
var addClass = CodeMirror.addClass = function(node, cls) { |
|
var current = node.className; |
|
if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls; |
|
}; |
|
function joinClasses(a, b) { |
|
var as = a.split(" "); |
|
for (var i = 0; i < as.length; i++) |
|
if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; |
|
return b; |
|
} |
|
|
|
// WINDOW-WIDE EVENTS |
|
|
|
// These must be handled carefully, because naively registering a |
|
// handler for each editor will cause the editors to never be |
|
// garbage collected. |
|
|
|
function forEachCodeMirror(f) { |
|
if (!document.body.getElementsByClassName) return; |
|
var byClass = document.body.getElementsByClassName("CodeMirror"); |
|
for (var i = 0; i < byClass.length; i++) { |
|
var cm = byClass[i].CodeMirror; |
|
if (cm) f(cm); |
|
} |
|
} |
|
|
|
var globalsRegistered = false; |
|
function ensureGlobalHandlers() { |
|
if (globalsRegistered) return; |
|
registerGlobalHandlers(); |
|
globalsRegistered = true; |
|
} |
|
function registerGlobalHandlers() { |
|
// When the window resizes, we need to refresh active editors. |
|
var resizeTimer; |
|
on(window, "resize", function() { |
|
if (resizeTimer == null) resizeTimer = setTimeout(function() { |
|
resizeTimer = null; |
|
forEachCodeMirror(onResize); |
|
}, 100); |
|
}); |
|
// When the window loses focus, we want to show the editor as blurred |
|
on(window, "blur", function() { |
|
forEachCodeMirror(onBlur); |
|
}); |
|
} |
|
|
|
// FEATURE DETECTION |
|
|
|
// Detect drag-and-drop |
|
var dragAndDrop = function() { |
|
// There is *some* kind of drag-and-drop support in IE6-8, but I |
|
// couldn't get it to work yet. |
|
if (ie && ie_version < 9) return false; |
|
var div = elt('div'); |
|
return "draggable" in div || "dragDrop" in div; |
|
}(); |
|
|
|
var zwspSupported; |
|
function zeroWidthElement(measure) { |
|
if (zwspSupported == null) { |
|
var test = elt("span", "\u200b"); |
|
removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); |
|
if (measure.firstChild.offsetHeight != 0) |
|
zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); |
|
} |
|
var node = zwspSupported ? elt("span", "\u200b") : |
|
elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); |
|
node.setAttribute("cm-text", ""); |
|
return node; |
|
} |
|
|
|
// Feature-detect IE's crummy client rect reporting for bidi text |
|
var badBidiRects; |
|
function hasBadBidiRects(measure) { |
|
if (badBidiRects != null) return badBidiRects; |
|
var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); |
|
var r0 = range(txt, 0, 1).getBoundingClientRect(); |
|
if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) |
|
var r1 = range(txt, 1, 2).getBoundingClientRect(); |
|
return badBidiRects = (r1.right - r0.right < 3); |
|
} |
|
|
|
// See if "".split is the broken IE version, if so, provide an |
|
// alternative way to split lines. |
|
var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { |
|
var pos = 0, result = [], l = string.length; |
|
while (pos <= l) { |
|
var nl = string.indexOf("\n", pos); |
|
if (nl == -1) nl = string.length; |
|
var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); |
|
var rt = line.indexOf("\r"); |
|
if (rt != -1) { |
|
result.push(line.slice(0, rt)); |
|
pos += rt + 1; |
|
} else { |
|
result.push(line); |
|
pos = nl + 1; |
|
} |
|
} |
|
return result; |
|
} : function(string){return string.split(/\r\n?|\n/);}; |
|
|
|
var hasSelection = window.getSelection ? function(te) { |
|
try { return te.selectionStart != te.selectionEnd; } |
|
catch(e) { return false; } |
|
} : function(te) { |
|
try {var range = te.ownerDocument.selection.createRange();} |
|
catch(e) {} |
|
if (!range || range.parentElement() != te) return false; |
|
return range.compareEndPoints("StartToEnd", range) != 0; |
|
}; |
|
|
|
var hasCopyEvent = (function() { |
|
var e = elt("div"); |
|
if ("oncopy" in e) return true; |
|
e.setAttribute("oncopy", "return;"); |
|
return typeof e.oncopy == "function"; |
|
})(); |
|
|
|
var badZoomedRects = null; |
|
function hasBadZoomedRects(measure) { |
|
if (badZoomedRects != null) return badZoomedRects; |
|
var node = removeChildrenAndAdd(measure, elt("span", "x")); |
|
var normal = node.getBoundingClientRect(); |
|
var fromRange = range(node, 0, 1).getBoundingClientRect(); |
|
return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; |
|
} |
|
|
|
// KEY NAMES |
|
|
|
var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", |
|
19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", |
|
36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", |
|
46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", |
|
173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", |
|
221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", |
|
63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; |
|
CodeMirror.keyNames = keyNames; |
|
(function() { |
|
// Number keys |
|
for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); |
|
// Alphabetic keys |
|
for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); |
|
// Function keys |
|
for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; |
|
})(); |
|
|
|
// BIDI HELPERS |
|
|
|
function iterateBidiSections(order, from, to, f) { |
|
if (!order) return f(from, to, "ltr"); |
|
var found = false; |
|
for (var i = 0; i < order.length; ++i) { |
|
var part = order[i]; |
|
if (part.from < to && part.to > from || from == to && part.to == from) { |
|
f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); |
|
found = true; |
|
} |
|
} |
|
if (!found) f(from, to, "ltr"); |
|
} |
|
|
|
function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } |
|
function bidiRight(part) { return part.level % 2 ? part.from : part.to; } |
|
|
|
function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } |
|
function lineRight(line) { |
|
var order = getOrder(line); |
|
if (!order) return line.text.length; |
|
return bidiRight(lst(order)); |
|
} |
|
|
|
function lineStart(cm, lineN) { |
|
var line = getLine(cm.doc, lineN); |
|
var visual = visualLine(line); |
|
if (visual != line) lineN = lineNo(visual); |
|
var order = getOrder(visual); |
|
var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); |
|
return Pos(lineN, ch); |
|
} |
|
function lineEnd(cm, lineN) { |
|
var merged, line = getLine(cm.doc, lineN); |
|
while (merged = collapsedSpanAtEnd(line)) { |
|
line = merged.find(1, true).line; |
|
lineN = null; |
|
} |
|
var order = getOrder(line); |
|
var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); |
|
return Pos(lineN == null ? lineNo(line) : lineN, ch); |
|
} |
|
function lineStartSmart(cm, pos) { |
|
var start = lineStart(cm, pos.line); |
|
var line = getLine(cm.doc, start.line); |
|
var order = getOrder(line); |
|
if (!order || order[0].level == 0) { |
|
var firstNonWS = Math.max(0, line.text.search(/\S/)); |
|
var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; |
|
return Pos(start.line, inWS ? 0 : firstNonWS); |
|
} |
|
return start; |
|
} |
|
|
|
function compareBidiLevel(order, a, b) { |
|
var linedir = order[0].level; |
|
if (a == linedir) return true; |
|
if (b == linedir) return false; |
|
return a < b; |
|
} |
|
var bidiOther; |
|
function getBidiPartAt(order, pos) { |
|
bidiOther = null; |
|
for (var i = 0, found; i < order.length; ++i) { |
|
var cur = order[i]; |
|
if (cur.from < pos && cur.to > pos) return i; |
|
if ((cur.from == pos || cur.to == pos)) { |
|
if (found == null) { |
|
found = i; |
|
} else if (compareBidiLevel(order, cur.level, order[found].level)) { |
|
if (cur.from != cur.to) bidiOther = found; |
|
return i; |
|
} else { |
|
if (cur.from != cur.to) bidiOther = i; |
|
return found; |
|
} |
|
} |
|
} |
|
return found; |
|
} |
|
|
|
function moveInLine(line, pos, dir, byUnit) { |
|
if (!byUnit) return pos + dir; |
|
do pos += dir; |
|
while (pos > 0 && isExtendingChar(line.text.charAt(pos))); |
|
return pos; |
|
} |
|
|
|
// This is needed in order to move 'visually' through bi-directional |
|
// text -- i.e., pressing left should make the cursor go left, even |
|
// when in RTL text. The tricky part is the 'jumps', where RTL and |
|
// LTR text touch each other. This often requires the cursor offset |
|
// to move more than one unit, in order to visually move one unit. |
|
function moveVisually(line, start, dir, byUnit) { |
|
var bidi = getOrder(line); |
|
if (!bidi) return moveLogically(line, start, dir, byUnit); |
|
var pos = getBidiPartAt(bidi, start), part = bidi[pos]; |
|
var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); |
|
|
|
for (;;) { |
|
if (target > part.from && target < part.to) return target; |
|
if (target == part.from || target == part.to) { |
|
if (getBidiPartAt(bidi, target) == pos) return target; |
|
part = bidi[pos += dir]; |
|
return (dir > 0) == part.level % 2 ? part.to : part.from; |
|
} else { |
|
part = bidi[pos += dir]; |
|
if (!part) return null; |
|
if ((dir > 0) == part.level % 2) |
|
target = moveInLine(line, part.to, -1, byUnit); |
|
else |
|
target = moveInLine(line, part.from, 1, byUnit); |
|
} |
|
} |
|
} |
|
|
|
function moveLogically(line, start, dir, byUnit) { |
|
var target = start + dir; |
|
if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; |
|
return target < 0 || target > line.text.length ? null : target; |
|
} |
|
|
|
// Bidirectional ordering algorithm |
|
// See http://unicode.org/reports/tr9/tr9-13.html for the algorithm |
|
// that this (partially) implements. |
|
|
|
// One-char codes used for character types: |
|
// L (L): Left-to-Right |
|
// R (R): Right-to-Left |
|
// r (AL): Right-to-Left Arabic |
|
// 1 (EN): European Number |
|
// + (ES): European Number Separator |
|
// % (ET): European Number Terminator |
|
// n (AN): Arabic Number |
|
// , (CS): Common Number Separator |
|
// m (NSM): Non-Spacing Mark |
|
// b (BN): Boundary Neutral |
|
// s (B): Paragraph Separator |
|
// t (S): Segment Separator |
|
// w (WS): Whitespace |
|
// N (ON): Other Neutrals |
|
|
|
// Returns null if characters are ordered as they appear |
|
// (left-to-right), or an array of sections ({from, to, level} |
|
// objects) in the order in which they occur visually. |
|
var bidiOrdering = (function() { |
|
// Character types for codepoints 0 to 0xff |
|
var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; |
|
// Character types for codepoints 0x600 to 0x6ff |
|
var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; |
|
function charType(code) { |
|
if (code <= 0xf7) return lowTypes.charAt(code); |
|
else if (0x590 <= code && code <= 0x5f4) return "R"; |
|
else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); |
|
else if (0x6ee <= code && code <= 0x8ac) return "r"; |
|
else if (0x2000 <= code && code <= 0x200b) return "w"; |
|
else if (code == 0x200c) return "b"; |
|
else return "L"; |
|
} |
|
|
|
var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; |
|
var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; |
|
// Browsers seem to always treat the boundaries of block elements as being L. |
|
var outerType = "L"; |
|
|
|
function BidiSpan(level, from, to) { |
|
this.level = level; |
|
this.from = from; this.to = to; |
|
} |
|
|
|
return function(str) { |
|
if (!bidiRE.test(str)) return false; |
|
var len = str.length, types = []; |
|
for (var i = 0, type; i < len; ++i) |
|
types.push(type = charType(str.charCodeAt(i))); |
|
|
|
// W1. Examine each non-spacing mark (NSM) in the level run, and |
|
// change the type of the NSM to the type of the previous |
|
// character. If the NSM is at the start of the level run, it will |
|
// get the type of sor. |
|
for (var i = 0, prev = outerType; i < len; ++i) { |
|
var type = types[i]; |
|
if (type == "m") types[i] = prev; |
|
else prev = type; |
|
} |
|
|
|
// W2. Search backwards from each instance of a European number |
|
// until the first strong type (R, L, AL, or sor) is found. If an |
|
// AL is found, change the type of the European number to Arabic |
|
// number. |
|
// W3. Change all ALs to R. |
|
for (var i = 0, cur = outerType; i < len; ++i) { |
|
var type = types[i]; |
|
if (type == "1" && cur == "r") types[i] = "n"; |
|
else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } |
|
} |
|
|
|
// W4. A single European separator between two European numbers |
|
// changes to a European number. A single common separator between |
|
// two numbers of the same type changes to that type. |
|
for (var i = 1, prev = types[0]; i < len - 1; ++i) { |
|
var type = types[i]; |
|
if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; |
|
else if (type == "," && prev == types[i+1] && |
|
(prev == "1" || prev == "n")) types[i] = prev; |
|
prev = type; |
|
} |
|
|
|
// W5. A sequence of European terminators adjacent to European |
|
// numbers changes to all European numbers. |
|
// W6. Otherwise, separators and terminators change to Other |
|
// Neutral. |
|
for (var i = 0; i < len; ++i) { |
|
var type = types[i]; |
|
if (type == ",") types[i] = "N"; |
|
else if (type == "%") { |
|
for (var end = i + 1; end < len && types[end] == "%"; ++end) {} |
|
var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; |
|
for (var j = i; j < end; ++j) types[j] = replace; |
|
i = end - 1; |
|
} |
|
} |
|
|
|
// W7. Search backwards from each instance of a European number |
|
// until the first strong type (R, L, or sor) is found. If an L is |
|
// found, then change the type of the European number to L. |
|
for (var i = 0, cur = outerType; i < len; ++i) { |
|
var type = types[i]; |
|
if (cur == "L" && type == "1") types[i] = "L"; |
|
else if (isStrong.test(type)) cur = type; |
|
} |
|
|
|
// N1. A sequence of neutrals takes the direction of the |
|
// surrounding strong text if the text on both sides has the same |
|
// direction. European and Arabic numbers act as if they were R in |
|
// terms of their influence on neutrals. Start-of-level-run (sor) |
|
// and end-of-level-run (eor) are used at level run boundaries. |
|
// N2. Any remaining neutrals take the embedding direction. |
|
for (var i = 0; i < len; ++i) { |
|
if (isNeutral.test(types[i])) { |
|
for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} |
|
var before = (i ? types[i-1] : outerType) == "L"; |
|
var after = (end < len ? types[end] : outerType) == "L"; |
|
var replace = before || after ? "L" : "R"; |
|
for (var j = i; j < end; ++j) types[j] = replace; |
|
i = end - 1; |
|
} |
|
} |
|
|
|
// Here we depart from the documented algorithm, in order to avoid |
|
// building up an actual levels array. Since there are only three |
|
// levels (0, 1, 2) in an implementation that doesn't take |
|
// explicit embedding into account, we can build up the order on |
|
// the fly, without following the level-based algorithm. |
|
var order = [], m; |
|
for (var i = 0; i < len;) { |
|
if (countsAsLeft.test(types[i])) { |
|
var start = i; |
|
for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} |
|
order.push(new BidiSpan(0, start, i)); |
|
} else { |
|
var pos = i, at = order.length; |
|
for (++i; i < len && types[i] != "L"; ++i) {} |
|
for (var j = pos; j < i;) { |
|
if (countsAsNum.test(types[j])) { |
|
if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); |
|
var nstart = j; |
|
for (++j; j < i && countsAsNum.test(types[j]); ++j) {} |
|
order.splice(at, 0, new BidiSpan(2, nstart, j)); |
|
pos = j; |
|
} else ++j; |
|
} |
|
if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); |
|
} |
|
} |
|
if (order[0].level == 1 && (m = str.match(/^\s+/))) { |
|
order[0].from = m[0].length; |
|
order.unshift(new BidiSpan(0, 0, m[0].length)); |
|
} |
|
if (lst(order).level == 1 && (m = str.match(/\s+$/))) { |
|
lst(order).to -= m[0].length; |
|
order.push(new BidiSpan(0, len - m[0].length, len)); |
|
} |
|
if (order[0].level == 2) |
|
order.unshift(new BidiSpan(1, order[0].to, order[0].to)); |
|
if (order[0].level != lst(order).level) |
|
order.push(new BidiSpan(order[0].level, len, len)); |
|
|
|
return order; |
|
}; |
|
})(); |
|
|
|
// THE END |
|
|
|
CodeMirror.version = "5.4.1"; |
|
|
|
return CodeMirror; |
|
});(function (mod) { |
|
mod(CodeMirror); |
|
})(function (CodeMirror) { |
|
var Pos = CodeMirror.Pos; |
|
|
|
function forEach(arr, f) { |
|
for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); |
|
} |
|
|
|
function arrayContains(arr, item) { |
|
if (!Array.prototype.indexOf) { |
|
var i = arr.length; |
|
while (i--) { |
|
if (arr[i] === item) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
return arr.indexOf(item) != -1; |
|
} |
|
|
|
function scriptHint(editor, keywords, getToken, options) { |
|
// Find the token at the cursor |
|
var cur = editor.getCursor(), token = getToken(editor, cur); |
|
if (/\b(?:string)\b/.test(token.type)) { |
|
return; |
|
} |
|
token.state = CodeMirror.innerMode(editor.getMode(), token.state).state; |
|
|
|
if (!/^[\w$_]*$/.test(token.string)) { |
|
token = { |
|
start: cur.ch, end: cur.ch, string: "", state: token.state, |
|
type: token.string == "." ? "property" : null |
|
}; |
|
} else if (token.end > cur.ch) { |
|
token.end = cur.ch; |
|
token.string = token.string.slice(0, cur.ch - token.start); |
|
} |
|
|
|
var tprop = token; |
|
// If it is a property, find out what it is a property of. |
|
while (tprop.type == "property") { |
|
tprop = getToken(editor, Pos(cur.line, tprop.start)); |
|
if (tprop.string != ".") return; |
|
tprop = getToken(editor, Pos(cur.line, tprop.start)); |
|
if (!context) var context = []; |
|
context.push(tprop); |
|
} |
|
return { |
|
list: getCompletions(token, context, keywords, options), |
|
from: Pos(cur.line, token.start), |
|
to: Pos(cur.line, token.end) |
|
}; |
|
} |
|
|
|
function getFormulaKeywords() { |
|
return FormulaCollections; |
|
} |
|
|
|
function formulaHint(editor, options) { |
|
return scriptHint(editor, getFormulaKeywords(), |
|
function (e, cur) { |
|
return e.getTokenAt(cur); |
|
}, |
|
options); |
|
}; |
|
CodeMirror.registerHelper("hint", "formula", formulaHint); |
|
|
|
function getCompletions(token, context, keywords, options) { |
|
var found = [], start = token.string; |
|
if (!start) { |
|
return found; |
|
} |
|
function maybeAdd(str) { |
|
if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) { |
|
found.push(str); |
|
} |
|
} |
|
|
|
if (context && context.length) { |
|
context.pop(); |
|
} else { |
|
forEach(keywords, maybeAdd); |
|
} |
|
return found; |
|
} |
|
});(function (mod) { |
|
mod(CodeMirror); |
|
})(function (CodeMirror) { |
|
"use strict"; |
|
|
|
CodeMirror.defineMode('formula', function () { |
|
function wordObj(words) { |
|
var o = {}; |
|
for (var i = 0, e = words.length; i < e; ++i) o[words[i]] = true; |
|
return o; |
|
} |
|
|
|
var atoms = wordObj(['false', 'true']); |
|
var keywords = wordObj(FormulaCollections); |
|
|
|
function tokenBase(stream, state) { |
|
if (stream.eatSpace()) { |
|
return null; |
|
} |
|
var ch = stream.next(); |
|
|
|
if (ch === '"' || ch === '\'') { |
|
nextUntilUnescaped(stream, ch); |
|
return "string"; |
|
} |
|
if (/[\[\],\(\)]/.test(ch)) { |
|
return 'bracket'; |
|
} |
|
|
|
// richie:暂时不需要解析操作符号 |
|
//if (/[+\-*\/=<>!&|]/.test(ch)) { |
|
// return 'operator'; |
|
//} |
|
//if (/\d|\d./.test(ch)) { |
|
// stream.eatWhile(/\d|\./); |
|
// if (stream.eol() || !/\w/.test(stream.peek())) { |
|
// return 'number'; |
|
// } |
|
//} |
|
|
|
|
|
|
|
stream.eatWhile(/[\w-]/); |
|
var word = stream.current(); |
|
if (atoms.hasOwnProperty(word)) { |
|
return "atom"; |
|
} |
|
if (keywords.hasOwnProperty(word)) { |
|
return "keyword"; |
|
} |
|
return null; |
|
} |
|
|
|
function nextUntilUnescaped(stream, end) { |
|
var escaped = false, next; |
|
while ((next = stream.next()) != null) { |
|
if (next === end && !escaped) { |
|
return false; |
|
} |
|
escaped = !escaped && next === "\\"; |
|
} |
|
return escaped; |
|
} |
|
|
|
function tokenize(stream, state) { |
|
return (state.tokens[0] || tokenBase)(stream, state); |
|
} |
|
|
|
return { |
|
startState: function () { |
|
return {tokens: []}; |
|
}, |
|
token: function (stream, state) { |
|
return tokenize(stream, state); |
|
}, |
|
fold: "brace" |
|
}; |
|
}); |
|
CodeMirror.defineMIME("text/fx-formula", "formula"); |
|
});// CodeMirror, copyright (c) by Marijn Haverbeke and others |
|
// Distributed under an MIT license: http://codemirror.net/LICENSE |
|
|
|
(function (mod) { |
|
mod(CodeMirror); |
|
})(function (CodeMirror) { |
|
"use strict"; |
|
|
|
var HINT_ELEMENT_CLASS = "CodeMirror-hint"; |
|
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; |
|
|
|
// This is the old interface, kept around for now to stay |
|
// backwards-compatible. |
|
CodeMirror.showHint = function (cm, getHints, options) { |
|
if (!getHints) return cm.showHint(options); |
|
if (options && options.async) getHints.async = true; |
|
var newOpts = {hint: getHints}; |
|
if (options) for (var prop in options) newOpts[prop] = options[prop]; |
|
return cm.showHint(newOpts); |
|
}; |
|
|
|
CodeMirror.defineExtension("showHint", function (options) { |
|
// We want a single cursor position. |
|
if (this.listSelections().length > 1 || this.somethingSelected()) return; |
|
|
|
if (this.state.completionActive) this.state.completionActive.close(); |
|
var completion = this.state.completionActive = new Completion(this, options); |
|
if (!completion.options.hint) return; |
|
|
|
CodeMirror.signal(this, "startCompletion", this); |
|
completion.update(true); |
|
}); |
|
|
|
function Completion(cm, options) { |
|
this.cm = cm; |
|
this.options = this.buildOptions(options); |
|
this.widget = null; |
|
this.debounce = 0; |
|
this.tick = 0; |
|
this.startPos = this.cm.getCursor(); |
|
this.startLen = this.cm.getLine(this.startPos.line).length; |
|
|
|
var self = this; |
|
cm.on("cursorActivity", this.activityFunc = function () { |
|
self.cursorActivity(); |
|
}); |
|
} |
|
|
|
var requestAnimationFrame = window.requestAnimationFrame || function (fn) { |
|
return setTimeout(fn, 1000 / 60); |
|
}; |
|
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; |
|
|
|
Completion.prototype = { |
|
close: function () { |
|
if (!this.active()) return; |
|
this.cm.state.completionActive = null; |
|
this.tick = null; |
|
this.cm.off("cursorActivity", this.activityFunc); |
|
|
|
if (this.widget && this.data) CodeMirror.signal(this.data, "close"); |
|
if (this.widget) this.widget.close(); |
|
CodeMirror.signal(this.cm, "endCompletion", this.cm); |
|
}, |
|
|
|
active: function () { |
|
return this.cm.state.completionActive == this; |
|
}, |
|
|
|
pick: function (data, i) { |
|
var completion = data.list[i]; |
|
if (completion.hint) completion.hint(this.cm, data, completion); |
|
else { |
|
this.cm.replaceRange(getText(completion), completion.from || data.from, |
|
completion.to || data.to, "complete"); |
|
var to = this.cm.getCursor(); |
|
this.cm.markText(completion.from || data.from, to, {className: "#function", atomic: true}); |
|
this.cm.replaceSelection("() "); |
|
to = this.cm.getCursor(); |
|
to.ch = to.ch - 2; |
|
this.cm.setCursor(to); |
|
this.cm.focus(); |
|
} |
|
CodeMirror.signal(data, "pick", completion); |
|
this.close(); |
|
}, |
|
|
|
cursorActivity: function () { |
|
if (this.debounce) { |
|
cancelAnimationFrame(this.debounce); |
|
this.debounce = 0; |
|
} |
|
|
|
var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line); |
|
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || |
|
pos.ch < this.startPos.ch || this.cm.somethingSelected() || |
|
(pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { |
|
this.close(); |
|
} else { |
|
var self = this; |
|
this.debounce = requestAnimationFrame(function () { |
|
self.update(); |
|
}); |
|
if (this.widget) this.widget.disable(); |
|
} |
|
}, |
|
|
|
update: function (first) { |
|
if (this.tick == null) return; |
|
if (this.data) CodeMirror.signal(this.data, "update"); |
|
if (!this.options.hint.async) { |
|
this.finishUpdate(this.options.hint(this.cm, this.options), first); |
|
} else { |
|
var myTick = ++this.tick, self = this; |
|
this.options.hint(this.cm, function (data) { |
|
if (self.tick == myTick) self.finishUpdate(data, first); |
|
}, this.options); |
|
} |
|
}, |
|
|
|
finishUpdate: function (data, first) { |
|
this.data = data; |
|
|
|
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); |
|
if (this.widget) this.widget.close(); |
|
if (data && data.list.length) { |
|
if (picked && data.list.length == 1) { |
|
this.pick(data, 0); |
|
} else { |
|
this.widget = new Widget(this, data); |
|
CodeMirror.signal(data, "shown"); |
|
} |
|
} |
|
}, |
|
|
|
buildOptions: function (options) { |
|
var editor = this.cm.options.hintOptions; |
|
var out = {}; |
|
for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; |
|
if (editor) for (var prop in editor) |
|
if (editor[prop] !== undefined) out[prop] = editor[prop]; |
|
if (options) for (var prop in options) |
|
if (options[prop] !== undefined) out[prop] = options[prop]; |
|
return out; |
|
} |
|
}; |
|
|
|
function getText(completion) { |
|
if (typeof completion == "string") return completion; |
|
else return completion.text; |
|
} |
|
|
|
function buildKeyMap(completion, handle) { |
|
var baseMap = { |
|
Up: function () { |
|
handle.moveFocus(-1); |
|
}, |
|
Down: function () { |
|
handle.moveFocus(1); |
|
}, |
|
PageUp: function () { |
|
handle.moveFocus(-handle.menuSize() + 1, true); |
|
}, |
|
PageDown: function () { |
|
handle.moveFocus(handle.menuSize() - 1, true); |
|
}, |
|
Home: function () { |
|
handle.setFocus(0); |
|
}, |
|
End: function () { |
|
handle.setFocus(handle.length - 1); |
|
}, |
|
Enter: handle.pick, |
|
Tab: handle.pick, |
|
Esc: handle.close |
|
}; |
|
var custom = completion.options.customKeys; |
|
var ourMap = custom ? {} : baseMap; |
|
|
|
function addBinding(key, val) { |
|
var bound; |
|
if (typeof val != "string") |
|
bound = function (cm) { |
|
return val(cm, handle); |
|
}; |
|
// This mechanism is deprecated |
|
else if (baseMap.hasOwnProperty(val)) |
|
bound = baseMap[val]; |
|
else |
|
bound = val; |
|
ourMap[key] = bound; |
|
} |
|
|
|
if (custom) |
|
for (var key in custom) if (custom.hasOwnProperty(key)) |
|
addBinding(key, custom[key]); |
|
var extra = completion.options.extraKeys; |
|
if (extra) |
|
for (var key in extra) if (extra.hasOwnProperty(key)) |
|
addBinding(key, extra[key]); |
|
return ourMap; |
|
} |
|
|
|
function getHintElement(hintsElement, el) { |
|
while (el && el != hintsElement) { |
|
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; |
|
el = el.parentNode; |
|
} |
|
} |
|
|
|
function Widget(completion, data) { |
|
this.completion = completion; |
|
this.data = data; |
|
this.picked = false; |
|
var widget = this, cm = completion.cm; |
|
|
|
var hints = this.hints = document.createElement("ul"); |
|
hints.className = "CodeMirror-hints"; |
|
this.selectedHint = data.selectedHint || 0; |
|
|
|
var completions = data.list; |
|
for (var i = 0; i < completions.length; ++i) { |
|
var elt = hints.appendChild(document.createElement("li")), cur = completions[i]; |
|
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); |
|
if (cur.className != null) className = cur.className + " " + className; |
|
elt.className = className; |
|
if (cur.render) cur.render(elt, data, cur); |
|
else elt.appendChild(document.createTextNode(cur.displayText || getText(cur))); |
|
elt.hintId = i; |
|
} |
|
|
|
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); |
|
var left = pos.left, top = pos.bottom, below = true; |
|
hints.style.left = left + "px"; |
|
hints.style.top = top + "px"; |
|
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. |
|
var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); |
|
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); |
|
(completion.options.container || document.body).appendChild(hints); |
|
var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; |
|
if (overlapY > 0) { |
|
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); |
|
if (curTop - height > 0) { // Fits above cursor |
|
hints.style.top = (top = pos.top - height) + "px"; |
|
below = false; |
|
} else if (height > winH) { |
|
hints.style.height = (winH - 5) + "px"; |
|
hints.style.top = (top = pos.bottom - box.top) + "px"; |
|
var cursor = cm.getCursor(); |
|
if (data.from.ch != cursor.ch) { |
|
pos = cm.cursorCoords(cursor); |
|
hints.style.left = (left = pos.left) + "px"; |
|
box = hints.getBoundingClientRect(); |
|
} |
|
} |
|
} |
|
var overlapX = box.right - winW; |
|
if (overlapX > 0) { |
|
if (box.right - box.left > winW) { |
|
hints.style.width = (winW - 5) + "px"; |
|
overlapX -= (box.right - box.left) - winW; |
|
} |
|
hints.style.left = (left = pos.left - overlapX) + "px"; |
|
} |
|
|
|
cm.addKeyMap(this.keyMap = buildKeyMap(completion, { |
|
moveFocus: function (n, avoidWrap) { |
|
widget.changeActive(widget.selectedHint + n, avoidWrap); |
|
}, |
|
setFocus: function (n) { |
|
widget.changeActive(n); |
|
}, |
|
menuSize: function () { |
|
return widget.screenAmount(); |
|
}, |
|
length: completions.length, |
|
close: function () { |
|
completion.close(); |
|
}, |
|
pick: function () { |
|
widget.pick(); |
|
}, |
|
data: data |
|
})); |
|
|
|
if (completion.options.closeOnUnfocus) { |
|
var closingOnBlur; |
|
cm.on("blur", this.onBlur = function () { |
|
closingOnBlur = setTimeout(function () { |
|
completion.close(); |
|
}, 100); |
|
}); |
|
cm.on("focus", this.onFocus = function () { |
|
clearTimeout(closingOnBlur); |
|
}); |
|
} |
|
|
|
var startScroll = cm.getScrollInfo(); |
|
cm.on("scroll", this.onScroll = function () { |
|
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); |
|
var newTop = top + startScroll.top - curScroll.top; |
|
var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop); |
|
if (!below) point += hints.offsetHeight; |
|
if (point <= editor.top || point >= editor.bottom) return completion.close(); |
|
hints.style.top = newTop + "px"; |
|
hints.style.left = (left + startScroll.left - curScroll.left) + "px"; |
|
}); |
|
|
|
CodeMirror.on(hints, "dblclick", function (e) { |
|
var t = getHintElement(hints, e.target || e.srcElement); |
|
if (t && t.hintId != null) { |
|
widget.changeActive(t.hintId); |
|
widget.pick(); |
|
} |
|
}); |
|
|
|
CodeMirror.on(hints, "click", function (e) { |
|
var t = getHintElement(hints, e.target || e.srcElement); |
|
if (t && t.hintId != null) { |
|
widget.changeActive(t.hintId); |
|
if (completion.options.completeOnSingleClick) widget.pick(); |
|
} |
|
}); |
|
|
|
CodeMirror.on(hints, "mousedown", function () { |
|
setTimeout(function () { |
|
cm.focus(); |
|
}, 20); |
|
}); |
|
|
|
CodeMirror.signal(data, "select", completions[0], hints.firstChild); |
|
return true; |
|
} |
|
|
|
Widget.prototype = { |
|
close: function () { |
|
if (this.completion.widget != this) return; |
|
this.completion.widget = null; |
|
this.hints.parentNode.removeChild(this.hints); |
|
this.completion.cm.removeKeyMap(this.keyMap); |
|
|
|
var cm = this.completion.cm; |
|
if (this.completion.options.closeOnUnfocus) { |
|
cm.off("blur", this.onBlur); |
|
cm.off("focus", this.onFocus); |
|
} |
|
cm.off("scroll", this.onScroll); |
|
}, |
|
|
|
disable: function () { |
|
this.completion.cm.removeKeyMap(this.keyMap); |
|
var widget = this; |
|
this.keyMap = { |
|
Enter: function () { |
|
widget.picked = true; |
|
} |
|
}; |
|
this.completion.cm.addKeyMap(this.keyMap); |
|
}, |
|
|
|
pick: function () { |
|
this.completion.pick(this.data, this.selectedHint); |
|
}, |
|
|
|
changeActive: function (i, avoidWrap) { |
|
if (i >= this.data.list.length) |
|
i = avoidWrap ? this.data.list.length - 1 : 0; |
|
else if (i < 0) |
|
i = avoidWrap ? 0 : this.data.list.length - 1; |
|
if (this.selectedHint == i) return; |
|
var node = this.hints.childNodes[this.selectedHint]; |
|
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); |
|
node = this.hints.childNodes[this.selectedHint = i]; |
|
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; |
|
if (node.offsetTop < this.hints.scrollTop) |
|
this.hints.scrollTop = node.offsetTop - 3; |
|
else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) |
|
this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; |
|
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); |
|
}, |
|
|
|
screenAmount: function () { |
|
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; |
|
} |
|
}; |
|
|
|
CodeMirror.registerHelper("hint", "auto", function (cm, options) { |
|
var helpers = cm.getHelpers(cm.getCursor(), "hint"), words; |
|
if (helpers.length) { |
|
for (var i = 0; i < helpers.length; i++) { |
|
var cur = helpers[i](cm, options); |
|
if (cur && cur.list.length) return cur; |
|
} |
|
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { |
|
if (words) return CodeMirror.hint.fromList(cm, {words: words}); |
|
} else if (CodeMirror.hint.anyword) { |
|
return CodeMirror.hint.anyword(cm, options); |
|
} |
|
}); |
|
|
|
CodeMirror.registerHelper("hint", "fromList", function (cm, options) { |
|
var cur = cm.getCursor(), token = cm.getTokenAt(cur); |
|
var found = []; |
|
for (var i = 0; i < options.words.length; i++) { |
|
var word = options.words[i]; |
|
if (word.slice(0, token.string.length) == token.string) |
|
found.push(word); |
|
} |
|
|
|
if (found.length) return { |
|
list: found, |
|
from: CodeMirror.Pos(cur.line, token.start), |
|
to: CodeMirror.Pos(cur.line, token.end) |
|
}; |
|
}); |
|
|
|
CodeMirror.commands.autocomplete = CodeMirror.showHint; |
|
|
|
var defaultOptions = { |
|
hint: CodeMirror.hint.auto, |
|
completeSingle: true, |
|
alignWithWord: true, |
|
closeCharacters: /[\s()\[\]{};:>,]/, |
|
closeOnUnfocus: true, |
|
completeOnSingleClick: true, |
|
container: null, |
|
customKeys: null, |
|
extraKeys: null |
|
}; |
|
|
|
CodeMirror.defineOption("hintOptions", null); |
|
});/** |
|
* Farbtastic Color Picker 1.2 |
|
* © 2008 Steven Wittens |
|
* |
|
* This program is free software; you can redistribute it and/or modify |
|
* it under the terms of the GNU General Public License as published by |
|
* the Free Software Foundation; either version 2 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU General Public License |
|
* along with this program; if not, write to the Free Software |
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
|
*/ |
|
|
|
jQuery.fn.farbtastic = function (callback) { |
|
$.farbtastic(this, callback); |
|
return this; |
|
}; |
|
|
|
jQuery.farbtastic = function (container, callback) { |
|
var container = $(container).get(0); |
|
return container.farbtastic || (container.farbtastic = new jQuery._farbtastic(container, callback)); |
|
} |
|
|
|
jQuery._farbtastic = function (container, callback) { |
|
// Store farbtastic object |
|
var fb = this; |
|
|
|
// Insert markup |
|
$(container).html('<div class="farbtastic"><div class="color"></div><div class="wheel"></div><div class="overlay"></div><div class="h-marker marker"></div><div class="sl-marker marker"></div></div>'); |
|
var e = $('.farbtastic', container); |
|
fb.wheel = $('.wheel', container).get(0); |
|
// Dimensions |
|
fb.radius = 84; |
|
fb.square = 100; |
|
fb.width = 194; |
|
|
|
// Fix background PNGs in IE6 |
|
if (navigator.appVersion.match(/MSIE [0-6]\./)) { |
|
$('*', e).each(function () { |
|
if (this.currentStyle.backgroundImage != 'none') { |
|
var image = this.currentStyle.backgroundImage; |
|
image = this.currentStyle.backgroundImage.substring(5, image.length - 2); |
|
$(this).css({ |
|
'backgroundImage': 'none', |
|
'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')" |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* Link to the given element(s) or callback. |
|
*/ |
|
fb.linkTo = function (callback) { |
|
// Unbind previous nodes |
|
if (typeof fb.callback == 'object') { |
|
$(fb.callback).unbind('keyup', fb.updateValue); |
|
} |
|
|
|
// Reset color |
|
fb.color = null; |
|
|
|
// Bind callback or elements |
|
if (typeof callback == 'function') { |
|
fb.callback = callback; |
|
} |
|
else if (typeof callback == 'object' || typeof callback == 'string') { |
|
fb.callback = $(callback); |
|
fb.callback.bind('keyup', fb.updateValue); |
|
if (fb.callback.get(0).value) { |
|
fb.setColor(fb.callback.get(0).value); |
|
} |
|
} |
|
return this; |
|
} |
|
fb.updateValue = function (event) { |
|
if (this.value && this.value != fb.color) { |
|
fb.setColor(this.value); |
|
} |
|
} |
|
|
|
/** |
|
* Change color with HTML syntax #123456 |
|
*/ |
|
fb.setColor = function (color) { |
|
var unpack = fb.unpack(color); |
|
if (fb.color != color && unpack) { |
|
fb.color = color; |
|
fb.rgb = unpack; |
|
fb.hsl = fb.RGBToHSL(fb.rgb); |
|
fb.updateDisplay(); |
|
} |
|
return this; |
|
} |
|
|
|
/** |
|
* Change color with HSL triplet [0..1, 0..1, 0..1] |
|
*/ |
|
fb.setHSL = function (hsl) { |
|
fb.hsl = hsl; |
|
fb.rgb = fb.HSLToRGB(hsl); |
|
fb.color = fb.pack(fb.rgb); |
|
fb.updateDisplay(); |
|
return this; |
|
} |
|
|
|
///////////////////////////////////////////////////// |
|
|
|
/** |
|
* Retrieve the coordinates of the given event relative to the center |
|
* of the widget. |
|
*/ |
|
fb.widgetCoords = function (event) { |
|
var x, y; |
|
var el = event.target || event.srcElement; |
|
var reference = fb.wheel; |
|
|
|
if (typeof event.offsetX != 'undefined') { |
|
// Use offset coordinates and find common offsetParent |
|
var pos = { x: event.offsetX, y: event.offsetY }; |
|
|
|
// Send the coordinates upwards through the offsetParent chain. |
|
var e = el; |
|
while (e) { |
|
e.mouseX = pos.x; |
|
e.mouseY = pos.y; |
|
pos.x += e.offsetLeft; |
|
pos.y += e.offsetTop; |
|
e = e.offsetParent; |
|
} |
|
|
|
// Look for the coordinates starting from the wheel widget. |
|
var e = reference; |
|
var offset = { x: 0, y: 0 } |
|
while (e) { |
|
if (typeof e.mouseX != 'undefined') { |
|
x = e.mouseX - offset.x; |
|
y = e.mouseY - offset.y; |
|
break; |
|
} |
|
offset.x += e.offsetLeft; |
|
offset.y += e.offsetTop; |
|
e = e.offsetParent; |
|
} |
|
|
|
// Reset stored coordinates |
|
e = el; |
|
while (e) { |
|
e.mouseX = undefined; |
|
e.mouseY = undefined; |
|
e = e.offsetParent; |
|
} |
|
} |
|
else { |
|
// Use absolute coordinates |
|
var pos = fb.absolutePosition(reference); |
|
x = (event.pageX || 0*(event.clientX + $('html').get(0).scrollLeft)) - pos.x; |
|
y = (event.pageY || 0*(event.clientY + $('html').get(0).scrollTop)) - pos.y; |
|
} |
|
// Subtract distance to middle |
|
return { x: x - fb.width / 2, y: y - fb.width / 2 }; |
|
} |
|
|
|
/** |
|
* Mousedown handler |
|
*/ |
|
fb.mousedown = function (event) { |
|
// Capture mouse |
|
if (!document.dragging) { |
|
$(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup); |
|
document.dragging = true; |
|
} |
|
|
|
// Check which area is being dragged |
|
var pos = fb.widgetCoords(event); |
|
fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square; |
|
|
|
// Process |
|
fb.mousemove(event); |
|
return false; |
|
} |
|
|
|
/** |
|
* Mousemove handler |
|
*/ |
|
fb.mousemove = function (event) { |
|
// Get coordinates relative to color picker center |
|
var pos = fb.widgetCoords(event); |
|
|
|
// Set new HSL parameters |
|
if (fb.circleDrag) { |
|
var hue = Math.atan2(pos.x, -pos.y) / 6.28; |
|
if (hue < 0) hue += 1; |
|
fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]); |
|
} |
|
else { |
|
var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5)); |
|
var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5)); |
|
fb.setHSL([fb.hsl[0], sat, lum]); |
|
} |
|
return false; |
|
} |
|
|
|
/** |
|
* Mouseup handler |
|
*/ |
|
fb.mouseup = function () { |
|
// Uncapture mouse |
|
$(document).unbind('mousemove', fb.mousemove); |
|
$(document).unbind('mouseup', fb.mouseup); |
|
document.dragging = false; |
|
} |
|
|
|
/** |
|
* Update the markers and styles |
|
*/ |
|
fb.updateDisplay = function () { |
|
// Markers |
|
var angle = fb.hsl[0] * 6.28; |
|
$('.h-marker', e).css({ |
|
left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px', |
|
top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px' |
|
}); |
|
|
|
$('.sl-marker', e).css({ |
|
left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px', |
|
top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px' |
|
}); |
|
|
|
// Saturation/Luminance gradient |
|
$('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5]))); |
|
|
|
// Linked elements or callback |
|
if (typeof fb.callback == 'object') { |
|
// Set background/foreground color |
|
$(fb.callback).css({ |
|
backgroundColor: fb.color, |
|
color: fb.hsl[2] > 0.5 ? '#000' : '#fff' |
|
}); |
|
|
|
// Change linked value |
|
$(fb.callback).each(function() { |
|
if (this.value && this.value != fb.color) { |
|
this.value = fb.color; |
|
} |
|
}); |
|
} |
|
else if (typeof fb.callback == 'function') { |
|
fb.callback.call(fb, fb.color); |
|
} |
|
} |
|
|
|
/** |
|
* Get absolute position of element |
|
*/ |
|
fb.absolutePosition = function (el) { |
|
var r = { x: el.offsetLeft, y: el.offsetTop }; |
|
// Resolve relative to offsetParent |
|
if (el.offsetParent) { |
|
var tmp = fb.absolutePosition(el.offsetParent); |
|
r.x += tmp.x; |
|
r.y += tmp.y; |
|
} |
|
return r; |
|
}; |
|
|
|
/* Various color utility functions */ |
|
fb.pack = function (rgb) { |
|
var r = Math.round(rgb[0] * 255); |
|
var g = Math.round(rgb[1] * 255); |
|
var b = Math.round(rgb[2] * 255); |
|
return '#' + (r < 16 ? '0' : '') + r.toString(16) + |
|
(g < 16 ? '0' : '') + g.toString(16) + |
|
(b < 16 ? '0' : '') + b.toString(16); |
|
} |
|
|
|
fb.unpack = function (color) { |
|
if (color.length == 7) { |
|
return [parseInt('0x' + color.substring(1, 3)) / 255, |
|
parseInt('0x' + color.substring(3, 5)) / 255, |
|
parseInt('0x' + color.substring(5, 7)) / 255]; |
|
} |
|
else if (color.length == 4) { |
|
return [parseInt('0x' + color.substring(1, 2)) / 15, |
|
parseInt('0x' + color.substring(2, 3)) / 15, |
|
parseInt('0x' + color.substring(3, 4)) / 15]; |
|
} |
|
} |
|
|
|
fb.HSLToRGB = function (hsl) { |
|
var m1, m2, r, g, b; |
|
var h = hsl[0], s = hsl[1], l = hsl[2]; |
|
m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s; |
|
m1 = l * 2 - m2; |
|
return [this.hueToRGB(m1, m2, h+0.33333), |
|
this.hueToRGB(m1, m2, h), |
|
this.hueToRGB(m1, m2, h-0.33333)]; |
|
} |
|
|
|
fb.hueToRGB = function (m1, m2, h) { |
|
h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h); |
|
if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; |
|
if (h * 2 < 1) return m2; |
|
if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6; |
|
return m1; |
|
} |
|
|
|
fb.RGBToHSL = function (rgb) { |
|
var min, max, delta, h, s, l; |
|
var r = rgb[0], g = rgb[1], b = rgb[2]; |
|
min = Math.min(r, Math.min(g, b)); |
|
max = Math.max(r, Math.max(g, b)); |
|
delta = max - min; |
|
l = (min + max) / 2; |
|
s = 0; |
|
if (l > 0 && l < 1) { |
|
s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l)); |
|
} |
|
h = 0; |
|
if (delta > 0) { |
|
if (max == r && max != g) h += (g - b) / delta; |
|
if (max == g && max != b) h += (2 + (b - r) / delta); |
|
if (max == b && max != r) h += (4 + (r - g) / delta); |
|
h /= 6; |
|
} |
|
return [h, s, l]; |
|
} |
|
|
|
// Install mousedown handler (the others are set on the document on-demand) |
|
$('*', e).mousedown(fb.mousedown); |
|
|
|
// Init color |
|
fb.setColor('#000000'); |
|
|
|
// Set linked elements/callback |
|
if (callback) { |
|
fb.linkTo(callback); |
|
} |
|
}/*! |
|
* jQuery Mousewheel 3.1.13 |
|
* |
|
* Copyright jQuery Foundation and other contributors |
|
* Released under the MIT license |
|
* http://jquery.org/license |
|
*/ |
|
|
|
(function (factory) { |
|
if ( typeof define === 'function' && define.amd ) { |
|
// AMD. Register as an anonymous module. |
|
define(['../core/jquery'], factory); |
|
} else if (typeof exports === 'object') { |
|
// Node/CommonJS style for Browserify |
|
module.exports = factory; |
|
} else { |
|
// Browser globals |
|
factory(jQuery); |
|
} |
|
}(function ($) { |
|
|
|
var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'], |
|
toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ? |
|
['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'], |
|
slice = Array.prototype.slice, |
|
nullLowestDeltaTimeout, lowestDelta; |
|
|
|
if ( $.event.fixHooks ) { |
|
for ( var i = toFix.length; i; ) { |
|
$.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks; |
|
} |
|
} |
|
|
|
var special = $.event.special.mousewheel = { |
|
version: '3.1.12', |
|
|
|
setup: function() { |
|
if ( this.addEventListener ) { |
|
for ( var i = toBind.length; i; ) { |
|
this.addEventListener( toBind[--i], handler, false ); |
|
} |
|
} else { |
|
this.onmousewheel = handler; |
|
} |
|
// Store the line height and page height for this particular element |
|
$.data(this, 'mousewheel-line-height', special.getLineHeight(this)); |
|
$.data(this, 'mousewheel-page-height', special.getPageHeight(this)); |
|
}, |
|
|
|
teardown: function() { |
|
if ( this.removeEventListener ) { |
|
for ( var i = toBind.length; i; ) { |
|
this.removeEventListener( toBind[--i], handler, false ); |
|
} |
|
} else { |
|
this.onmousewheel = null; |
|
} |
|
// Clean up the data we added to the element |
|
$.removeData(this, 'mousewheel-line-height'); |
|
$.removeData(this, 'mousewheel-page-height'); |
|
}, |
|
|
|
getLineHeight: function(elem) { |
|
var $elem = $(elem), |
|
$parent = $elem['offsetParent' in $.fn ? 'offsetParent' : 'parent'](); |
|
if (!$parent.length) { |
|
$parent = $('body'); |
|
} |
|
return parseInt($parent.css('fontSize'), 10) || parseInt($elem.css('fontSize'), 10) || 16; |
|
}, |
|
|
|
getPageHeight: function(elem) { |
|
return $(elem).height(); |
|
}, |
|
|
|
settings: { |
|
adjustOldDeltas: true, // see shouldAdjustOldDeltas() below |
|
normalizeOffset: true // calls getBoundingClientRect for each event |
|
} |
|
}; |
|
|
|
$.fn.extend({ |
|
mousewheel: function(fn) { |
|
return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel'); |
|
}, |
|
|
|
unmousewheel: function(fn) { |
|
return this.unbind('mousewheel', fn); |
|
} |
|
}); |
|
|
|
|
|
function handler(event) { |
|
var orgEvent = event || window.event, |
|
args = slice.call(arguments, 1), |
|
delta = 0, |
|
deltaX = 0, |
|
deltaY = 0, |
|
absDelta = 0, |
|
offsetX = 0, |
|
offsetY = 0; |
|
event = $.event.fix(orgEvent); |
|
event.type = 'mousewheel'; |
|
|
|
// Old school scrollwheel delta |
|
if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } |
|
if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } |
|
if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } |
|
if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; } |
|
|
|
// Firefox < 17 horizontal scrolling related to DOMMouseScroll event |
|
if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { |
|
deltaX = deltaY * -1; |
|
deltaY = 0; |
|
} |
|
|
|
// Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy |
|
delta = deltaY === 0 ? deltaX : deltaY; |
|
|
|
// New school wheel delta (wheel event) |
|
if ( 'deltaY' in orgEvent ) { |
|
deltaY = orgEvent.deltaY * -1; |
|
delta = deltaY; |
|
} |
|
if ( 'deltaX' in orgEvent ) { |
|
deltaX = orgEvent.deltaX; |
|
if ( deltaY === 0 ) { delta = deltaX * -1; } |
|
} |
|
|
|
// No change actually happened, no reason to go any further |
|
if ( deltaY === 0 && deltaX === 0 ) { return; } |
|
|
|
// Need to convert lines and pages to pixels if we aren't already in pixels |
|
// There are three delta modes: |
|
// * deltaMode 0 is by pixels, nothing to do |
|
// * deltaMode 1 is by lines |
|
// * deltaMode 2 is by pages |
|
if ( orgEvent.deltaMode === 1 ) { |
|
var lineHeight = $.data(this, 'mousewheel-line-height'); |
|
delta *= lineHeight; |
|
deltaY *= lineHeight; |
|
deltaX *= lineHeight; |
|
} else if ( orgEvent.deltaMode === 2 ) { |
|
var pageHeight = $.data(this, 'mousewheel-page-height'); |
|
delta *= pageHeight; |
|
deltaY *= pageHeight; |
|
deltaX *= pageHeight; |
|
} |
|
|
|
// Store lowest absolute delta to normalize the delta values |
|
absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) ); |
|
|
|
if ( !lowestDelta || absDelta < lowestDelta ) { |
|
lowestDelta = absDelta; |
|
|
|
// Adjust older deltas if necessary |
|
if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { |
|
lowestDelta /= 40; |
|
} |
|
} |
|
|
|
// Adjust older deltas if necessary |
|
if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { |
|
// Divide all the things by 40! |
|
delta /= 40; |
|
deltaX /= 40; |
|
deltaY /= 40; |
|
} |
|
|
|
// Get a whole, normalized value for the deltas |
|
delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta); |
|
deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta); |
|
deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta); |
|
|
|
// Normalise offsetX and offsetY properties |
|
if ( special.settings.normalizeOffset && this.getBoundingClientRect ) { |
|
var boundingRect = this.getBoundingClientRect(); |
|
offsetX = event.clientX - boundingRect.left; |
|
offsetY = event.clientY - boundingRect.top; |
|
} |
|
|
|
// Add information to the event object |
|
event.deltaX = deltaX; |
|
event.deltaY = deltaY; |
|
event.deltaFactor = lowestDelta; |
|
event.offsetX = offsetX; |
|
event.offsetY = offsetY; |
|
// Go ahead and set deltaMode to 0 since we converted to pixels |
|
// Although this is a little odd since we overwrite the deltaX/Y |
|
// properties with normalized deltas. |
|
event.deltaMode = 0; |
|
|
|
// Add event and delta to the front of the arguments |
|
args.unshift(event, delta, deltaX, deltaY); |
|
|
|
// Clearout lowestDelta after sometime to better |
|
// handle multiple device types that give different |
|
// a different lowestDelta |
|
// Ex: trackpad = 3 and mouse wheel = 120 |
|
if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); } |
|
nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200); |
|
|
|
return ($.event.dispatch || $.event.handle).apply(this, args); |
|
} |
|
|
|
function nullLowestDelta() { |
|
lowestDelta = null; |
|
} |
|
|
|
function shouldAdjustOldDeltas(orgEvent, absDelta) { |
|
// If this is an older event and the delta is divisable by 120, |
|
// then we are assuming that the browser is treating this as an |
|
// older mouse wheel event and that we should divide the deltas |
|
// by 40 to try and get a more usable deltaFactor. |
|
// Side note, this actually impacts the reported scroll distance |
|
// in older browsers and can cause scrolling to be slower than native. |
|
// Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false. |
|
return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0; |
|
} |
|
|
|
}));// Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. |
|
// |
|
// Licensed under the Apache License, Version 2.0 (the "License"); |
|
// you may not use this file except in compliance with the License. |
|
// You may obtain a copy of the License at |
|
// |
|
// http://www.apache.org/licenses/LICENSE-2.0 |
|
// |
|
// Unless required by applicable law or agreed to in writing, software |
|
// distributed under the License is distributed on an "AS IS" BASIS, |
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
// See the License for the specific language governing permissions and |
|
// limitations under the License. |
|
// ┌────────────────────────────────────────────────────────────┐ \\ |
|
// │ Eve 0.4.2 - JavaScript Events Library │ \\ |
|
// ├────────────────────────────────────────────────────────────┤ \\ |
|
// │ Author Dmitry Baranovskiy (http://dmitry.baranovskiy.com/) │ \\ |
|
// └────────────────────────────────────────────────────────────┘ \\ |
|
(function (glob, factory) { |
|
if (typeof define === "function" && define.amd) { |
|
define("eve", function() { |
|
return factory(); |
|
}); |
|
} else if (typeof exports === "object") { |
|
module.exports = factory(); |
|
} else { |
|
glob.eve = factory(); |
|
} |
|
}(this, function(){ |
|
var version = "0.4.2", |
|
has = "hasOwnProperty", |
|
separator = /[\.\/]/, |
|
wildcard = "*", |
|
fun = function () {}, |
|
numsort = function (a, b) { |
|
return a - b; |
|
}, |
|
current_event, |
|
stop, |
|
events = {n: {}}, |
|
/*\ |
|
* eve |
|
[ method ] |
|
|
|
* Fires event with given `name`, given scope and other parameters. |
|
|
|
> Arguments |
|
|
|
- name (string) name of the *event*, dot (`.`) or slash (`/`) separated |
|
- scope (object) context for the event handlers |
|
- varargs (...) the rest of arguments will be sent to event handlers |
|
|
|
= (object) array of returned values from the listeners |
|
\*/ |
|
eve = function (name, scope) { |
|
name = String(name); |
|
var e = events, |
|
oldstop = stop, |
|
args = Array.prototype.slice.call(arguments, 2), |
|
listeners = eve.listeners(name), |
|
z = 0, |
|
f = false, |
|
l, |
|
indexed = [], |
|
queue = {}, |
|
out = [], |
|
ce = current_event, |
|
errors = []; |
|
current_event = name; |
|
stop = 0; |
|
for (var i = 0, ii = listeners.length; i < ii; i++) if ("zIndex" in listeners[i]) { |
|
indexed.push(listeners[i].zIndex); |
|
if (listeners[i].zIndex < 0) { |
|
queue[listeners[i].zIndex] = listeners[i]; |
|
} |
|
} |
|
indexed.sort(numsort); |
|
while (indexed[z] < 0) { |
|
l = queue[indexed[z++]]; |
|
out.push(l.apply(scope, args)); |
|
if (stop) { |
|
stop = oldstop; |
|
return out; |
|
} |
|
} |
|
for (i = 0; i < ii; i++) { |
|
l = listeners[i]; |
|
if ("zIndex" in l) { |
|
if (l.zIndex == indexed[z]) { |
|
out.push(l.apply(scope, args)); |
|
if (stop) { |
|
break; |
|
} |
|
do { |
|
z++; |
|
l = queue[indexed[z]]; |
|
l && out.push(l.apply(scope, args)); |
|
if (stop) { |
|
break; |
|
} |
|
} while (l) |
|
} else { |
|
queue[l.zIndex] = l; |
|
} |
|
} else { |
|
out.push(l.apply(scope, args)); |
|
if (stop) { |
|
break; |
|
} |
|
} |
|
} |
|
stop = oldstop; |
|
current_event = ce; |
|
return out.length ? out : null; |
|
}; |
|
// Undocumented. Debug only. |
|
eve._events = events; |
|
/*\ |
|
* eve.listeners |
|
[ method ] |
|
|
|
* Internal method which gives you array of all event handlers that will be triggered by the given `name`. |
|
|
|
> Arguments |
|
|
|
- name (string) name of the event, dot (`.`) or slash (`/`) separated |
|
|
|
= (array) array of event handlers |
|
\*/ |
|
eve.listeners = function (name) { |
|
var names = name.split(separator), |
|
e = events, |
|
item, |
|
items, |
|
k, |
|
i, |
|
ii, |
|
j, |
|
jj, |
|
nes, |
|
es = [e], |
|
out = []; |
|
for (i = 0, ii = names.length; i < ii; i++) { |
|
nes = []; |
|
for (j = 0, jj = es.length; j < jj; j++) { |
|
e = es[j].n; |
|
items = [e[names[i]], e[wildcard]]; |
|
k = 2; |
|
while (k--) { |
|
item = items[k]; |
|
if (item) { |
|
nes.push(item); |
|
out = out.concat(item.f || []); |
|
} |
|
} |
|
} |
|
es = nes; |
|
} |
|
return out; |
|
}; |
|
|
|
/*\ |
|
* eve.on |
|
[ method ] |
|
** |
|
* Binds given event handler with a given name. You can use wildcards “`*`” for the names: |
|
| eve.on("*.under.*", f); |
|
| eve("mouse.under.floor"); // triggers f |
|
* Use @eve to trigger the listener. |
|
** |
|
> Arguments |
|
** |
|
- name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards |
|
- f (function) event handler function |
|
** |
|
= (function) returned function accepts a single numeric parameter that represents z-index of the handler. It is an optional feature and only used when you need to ensure that some subset of handlers will be invoked in a given order, despite of the order of assignment. |
|
> Example: |
|
| eve.on("mouse", eatIt)(2); |
|
| eve.on("mouse", scream); |
|
| eve.on("mouse", catchIt)(1); |
|
* This will ensure that `catchIt()` function will be called before `eatIt()`. |
|
* |
|
* If you want to put your handler before non-indexed handlers, specify a negative value. |
|
* Note: I assume most of the time you don’t need to worry about z-index, but it’s nice to have this feature “just in case”. |
|
\*/ |
|
eve.on = function (name, f) { |
|
name = String(name); |
|
if (typeof f != "function") { |
|
return function () {}; |
|
} |
|
var names = name.split(separator), |
|
e = events; |
|
for (var i = 0, ii = names.length; i < ii; i++) { |
|
e = e.n; |
|
e = e.hasOwnProperty(names[i]) && e[names[i]] || (e[names[i]] = {n: {}}); |
|
} |
|
e.f = e.f || []; |
|
for (i = 0, ii = e.f.length; i < ii; i++) if (e.f[i] == f) { |
|
return fun; |
|
} |
|
e.f.push(f); |
|
return function (zIndex) { |
|
if (+zIndex == +zIndex) { |
|
f.zIndex = +zIndex; |
|
} |
|
}; |
|
}; |
|
/*\ |
|
* eve.f |
|
[ method ] |
|
** |
|
* Returns function that will fire given event with optional arguments. |
|
* Arguments that will be passed to the result function will be also |
|
* concated to the list of final arguments. |
|
| el.onclick = eve.f("click", 1, 2); |
|
| eve.on("click", function (a, b, c) { |
|
| console.log(a, b, c); // 1, 2, [event object] |
|
| }); |
|
> Arguments |
|
- event (string) event name |
|
- varargs (…) and any other arguments |
|
= (function) possible event handler function |
|
\*/ |
|
eve.f = function (event) { |
|
var attrs = [].slice.call(arguments, 1); |
|
return function () { |
|
eve.apply(null, [event, null].concat(attrs).concat([].slice.call(arguments, 0))); |
|
}; |
|
}; |
|
/*\ |
|
* eve.stop |
|
[ method ] |
|
** |
|
* Is used inside an event handler to stop the event, preventing any subsequent listeners from firing. |
|
\*/ |
|
eve.stop = function () { |
|
stop = 1; |
|
}; |
|
/*\ |
|
* eve.nt |
|
[ method ] |
|
** |
|
* Could be used inside event handler to figure out actual name of the event. |
|
** |
|
> Arguments |
|
** |
|
- subname (string) #optional subname of the event |
|
** |
|
= (string) name of the event, if `subname` is not specified |
|
* or |
|
= (boolean) `true`, if current event’s name contains `subname` |
|
\*/ |
|
eve.nt = function (subname) { |
|
if (subname) { |
|
return new RegExp("(?:\\.|\\/|^)" + subname + "(?:\\.|\\/|$)").test(current_event); |
|
} |
|
return current_event; |
|
}; |
|
/*\ |
|
* eve.nts |
|
[ method ] |
|
** |
|
* Could be used inside event handler to figure out actual name of the event. |
|
** |
|
** |
|
= (array) names of the event |
|
\*/ |
|
eve.nts = function () { |
|
return current_event.split(separator); |
|
}; |
|
/*\ |
|
* eve.off |
|
[ method ] |
|
** |
|
* Removes given function from the list of event listeners assigned to given name. |
|
* If no arguments specified all the events will be cleared. |
|
** |
|
> Arguments |
|
** |
|
- name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards |
|
- f (function) event handler function |
|
\*/ |
|
/*\ |
|
* eve.unbind |
|
[ method ] |
|
** |
|
* See @eve.off |
|
\*/ |
|
eve.off = eve.unbind = function (name, f) { |
|
if (!name) { |
|
eve._events = events = {n: {}}; |
|
return; |
|
} |
|
var names = name.split(separator), |
|
e, |
|
key, |
|
splice, |
|
i, ii, j, jj, |
|
cur = [events]; |
|
for (i = 0, ii = names.length; i < ii; i++) { |
|
for (j = 0; j < cur.length; j += splice.length - 2) { |
|
splice = [j, 1]; |
|
e = cur[j].n; |
|
if (names[i] != wildcard) { |
|
if (e[names[i]]) { |
|
splice.push(e[names[i]]); |
|
} |
|
} else { |
|
for (key in e) if (e[has](key)) { |
|
splice.push(e[key]); |
|
} |
|
} |
|
cur.splice.apply(cur, splice); |
|
} |
|
} |
|
for (i = 0, ii = cur.length; i < ii; i++) { |
|
e = cur[i]; |
|
while (e.n) { |
|
if (f) { |
|
if (e.f) { |
|
for (j = 0, jj = e.f.length; j < jj; j++) if (e.f[j] == f) { |
|
e.f.splice(j, 1); |
|
break; |
|
} |
|
!e.f.length && delete e.f; |
|
} |
|
for (key in e.n) if (e.n[has](key) && e.n[key].f) { |
|
var funcs = e.n[key].f; |
|
for (j = 0, jj = funcs.length; j < jj; j++) if (funcs[j] == f) { |
|
funcs.splice(j, 1); |
|
break; |
|
} |
|
!funcs.length && delete e.n[key].f; |
|
} |
|
} else { |
|
delete e.f; |
|
for (key in e.n) if (e.n[has](key) && e.n[key].f) { |
|
delete e.n[key].f; |
|
} |
|
} |
|
e = e.n; |
|
} |
|
} |
|
}; |
|
/*\ |
|
* eve.once |
|
[ method ] |
|
** |
|
* Binds given event handler with a given name to only run once then unbind itself. |
|
| eve.once("login", f); |
|
| eve("login"); // triggers f |
|
| eve("login"); // no listeners |
|
* Use @eve to trigger the listener. |
|
** |
|
> Arguments |
|
** |
|
- name (string) name of the event, dot (`.`) or slash (`/`) separated, with optional wildcards |
|
- f (function) event handler function |
|
** |
|
= (function) same return function as @eve.on |
|
\*/ |
|
eve.once = function (name, f) { |
|
var f2 = function () { |
|
eve.unbind(name, f2); |
|
return f.apply(this, arguments); |
|
}; |
|
return eve.on(name, f2); |
|
}; |
|
/*\ |
|
* eve.version |
|
[ property (string) ] |
|
** |
|
* Current version of the library. |
|
\*/ |
|
eve.version = version; |
|
eve.toString = function () { |
|
return "You are running Eve " + version; |
|
}; |
|
|
|
return eve; |
|
})); |
|
|
|
// ┌────────────────────────────────────────────────────────────────────┐ \\ |
|
// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\ |
|
// ├────────────────────────────────────────────────────────────────────┤ \\ |
|
// │ Core Module │ \\ |
|
// ├────────────────────────────────────────────────────────────────────┤ \\ |
|
// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\ |
|
// └────────────────────────────────────────────────────────────────────┘ \\ |
|
|
|
(function (glob, factory) { |
|
if (typeof define === "function" && define.amd) { |
|
define("raphael.core", ["eve"], function(eve) { |
|
return factory(eve); |
|
}); |
|
} else if (typeof exports === "object") { |
|
module.exports = factory(require("eve")); |
|
} else { |
|
glob.Raphael = factory(glob.eve); |
|
} |
|
}(this, function (eve) { |
|
/*\ |
|
* Raphael |
|
[ method ] |
|
** |
|
* Creates a canvas object on which to draw. |
|
* You must do this first, as all future calls to drawing methods |
|
* from this instance will be bound to this canvas. |
|
> Parameters |
|
** |
|
- container (HTMLElement|string) DOM element or its ID which is going to be a parent for drawing surface |
|
- width (number) |
|
- height (number) |
|
- callback (function) #optional callback function which is going to be executed in the context of newly created paper |
|
* or |
|
- x (number) |
|
- y (number) |
|
- width (number) |
|
- height (number) |
|
- callback (function) #optional callback function which is going to be executed in the context of newly created paper |
|
* or |
|
- all (array) (first 3 or 4 elements in the array are equal to [containerID, width, height] or [x, y, width, height]. The rest are element descriptions in format {type: type, <attributes>}). See @Paper.add. |
|
- callback (function) #optional callback function which is going to be executed in the context of newly created paper |
|
* or |
|
- onReadyCallback (function) function that is going to be called on DOM ready event. You can also subscribe to this event via Eve’s “DOMLoad” event. In this case method returns `undefined`. |
|
= (object) @Paper |
|
> Usage |
|
| // Each of the following examples create a canvas |
|
| // that is 320px wide by 200px high. |
|
| // Canvas is created at the viewport’s 10,50 coordinate. |
|
| var paper = Raphael(10, 50, 320, 200); |
|
| // Canvas is created at the top left corner of the #notepad element |
|
| // (or its top right corner in dir="rtl" elements) |
|
| var paper = Raphael(document.getElementById("notepad"), 320, 200); |
|
| // Same as above |
|
| var paper = Raphael("notepad", 320, 200); |
|
| // Image dump |
|
| var set = Raphael(["notepad", 320, 200, { |
|
| type: "rect", |
|
| x: 10, |
|
| y: 10, |
|
| width: 25, |
|
| height: 25, |
|
| stroke: "#f00" |
|
| }, { |
|
| type: "text", |
|
| x: 30, |
|
| y: 40, |
|
| text: "Dump" |
|
| }]); |
|
\*/ |
|
function R(first) { |
|
if (R.is(first, "function")) { |
|
return loaded ? first() : eve.on("raphael.DOMload", first); |
|
} else if (R.is(first, array)) { |
|
return R._engine.create[apply](R, first.splice(0, 3 + R.is(first[0], nu))).add(first); |
|
} else { |
|
var args = Array.prototype.slice.call(arguments, 0); |
|
if (R.is(args[args.length - 1], "function")) { |
|
var f = args.pop(); |
|
return loaded ? f.call(R._engine.create[apply](R, args)) : eve.on("raphael.DOMload", function () { |
|
f.call(R._engine.create[apply](R, args)); |
|
}); |
|
} else { |
|
return R._engine.create[apply](R, arguments); |
|
} |
|
} |
|
} |
|
R.version = "2.1.4"; |
|
R.eve = eve; |
|
var loaded, |
|
separator = /[, ]+/, |
|
elements = {circle: 1, rect: 1, path: 1, ellipse: 1, text: 1, image: 1}, |
|
formatrg = /\{(\d+)\}/g, |
|
proto = "prototype", |
|
has = "hasOwnProperty", |
|
g = { |
|
doc: document, |
|
win: window |
|
}, |
|
oldRaphael = { |
|
was: Object.prototype[has].call(g.win, "Raphael"), |
|
is: g.win.Raphael |
|
}, |
|
Paper = function () { |
|
/*\ |
|
* Paper.ca |
|
[ property (object) ] |
|
** |
|
* Shortcut for @Paper.customAttributes |
|
\*/ |
|
/*\ |
|
* Paper.customAttributes |
|
[ property (object) ] |
|
** |
|
* If you have a set of attributes that you would like to represent |
|
* as a function of some number you can do it easily with custom attributes: |
|
> Usage |
|
| paper.customAttributes.hue = function (num) { |
|
| num = num % 1; |
|
| return {fill: "hsb(" + num + ", 0.75, 1)"}; |
|
| }; |
|
| // Custom attribute “hue” will change fill |
|
| // to be given hue with fixed saturation and brightness. |
|
| // Now you can use it like this: |
|
| var c = paper.circle(10, 10, 10).attr({hue: .45}); |
|
| // or even like this: |
|
| c.animate({hue: 1}, 1e3); |
|
| |
|
| // You could also create custom attribute |
|
| // with multiple parameters: |
|
| paper.customAttributes.hsb = function (h, s, b) { |
|
| return {fill: "hsb(" + [h, s, b].join(",") + ")"}; |
|
| }; |
|
| c.attr({hsb: "0.5 .8 1"}); |
|
| c.animate({hsb: [1, 0, 0.5]}, 1e3); |
|
\*/ |
|
this.ca = this.customAttributes = {}; |
|
}, |
|
paperproto, |
|
appendChild = "appendChild", |
|
apply = "apply", |
|
concat = "concat", |
|
supportsTouch = ('ontouchstart' in g.win) || g.win.DocumentTouch && g.doc instanceof DocumentTouch, //taken from Modernizr touch test |
|
E = "", |
|
S = " ", |
|
Str = String, |
|
split = "split", |
|
events = "click dblclick mousedown mousemove mouseout mouseover mouseup touchstart touchmove touchend touchcancel"[split](S), |
|
touchMap = { |
|
mousedown: "touchstart", |
|
mousemove: "touchmove", |
|
mouseup: "touchend" |
|
}, |
|
lowerCase = Str.prototype.toLowerCase, |
|
math = Math, |
|
mmax = math.max, |
|
mmin = math.min, |
|
abs = math.abs, |
|
pow = math.pow, |
|
PI = math.PI, |
|
nu = "number", |
|
string = "string", |
|
array = "array", |
|
toString = "toString", |
|
fillString = "fill", |
|
objectToString = Object.prototype.toString, |
|
paper = {}, |
|
push = "push", |
|
ISURL = R._ISURL = /^url\(['"]?(.+?)['"]?\)$/i, |
|
colourRegExp = /^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?)%?\s*\))\s*$/i, |
|
isnan = {"NaN": 1, "Infinity": 1, "-Infinity": 1}, |
|
bezierrg = /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/, |
|
round = math.round, |
|
setAttribute = "setAttribute", |
|
toFloat = parseFloat, |
|
toInt = parseInt, |
|
upperCase = Str.prototype.toUpperCase, |
|
availableAttrs = R._availableAttrs = { |
|
"arrow-end": "none", |
|
"arrow-start": "none", |
|
blur: 0, |
|
"clip-rect": "0 0 1e9 1e9", |
|
cursor: "default", |
|
cx: 0, |
|
cy: 0, |
|
fill: "#fff", |
|
"fill-opacity": 1, |
|
font: '10px "Arial"', |
|
"font-family": '"Arial"', |
|
"font-size": "10", |
|
"font-style": "normal", |
|
"font-weight": 400, |
|
gradient: 0, |
|
height: 0, |
|
href: "http://raphaeljs.com/", |
|
"letter-spacing": 0, |
|
opacity: 1, |
|
path: "M0,0", |
|
r: 0, |
|
rx: 0, |
|
ry: 0, |
|
src: "", |
|
stroke: "#000", |
|
"stroke-dasharray": "", |
|
"stroke-linecap": "butt", |
|
"stroke-linejoin": "butt", |
|
"stroke-miterlimit": 0, |
|
"stroke-opacity": 1, |
|
"stroke-width": 1, |
|
target: "_blank", |
|
"text-anchor": "middle", |
|
title: "Raphael", |
|
transform: "", |
|
width: 0, |
|
x: 0, |
|
y: 0 |
|
}, |
|
availableAnimAttrs = R._availableAnimAttrs = { |
|
blur: nu, |
|
"clip-rect": "csv", |
|
cx: nu, |
|
cy: nu, |
|
fill: "colour", |
|
"fill-opacity": nu, |
|
"font-size": nu, |
|
height: nu, |
|
opacity: nu, |
|
path: "path", |
|
r: nu, |
|
rx: nu, |
|
ry: nu, |
|
stroke: "colour", |
|
"stroke-opacity": nu, |
|
"stroke-width": nu, |
|
transform: "transform", |
|
width: nu, |
|
x: nu, |
|
y: nu |
|
}, |
|
whitespace = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]/g, |
|
commaSpaces = /[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/, |
|
hsrg = {hs: 1, rg: 1}, |
|
p2s = /,?([achlmqrstvxz]),?/gi, |
|
pathCommand = /([achlmrqstvz])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig, |
|
tCommand = /([rstm])[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*)+)/ig, |
|
pathValues = /(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,?[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*/ig, |
|
radial_gradient = R._radial_gradient = /^r(?:\(([^,]+?)[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*,[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029]*([^\)]+?)\))?/, |
|
eldata = {}, |
|
sortByKey = function (a, b) { |
|
return a.key - b.key; |
|
}, |
|
sortByNumber = function (a, b) { |
|
return toFloat(a) - toFloat(b); |
|
}, |
|
fun = function () {}, |
|
pipe = function (x) { |
|
return x; |
|
}, |
|
rectPath = R._rectPath = function (x, y, w, h, r) { |
|
if (r) { |
|
return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]]; |
|
} |
|
return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]]; |
|
}, |
|
ellipsePath = function (x, y, rx, ry) { |
|
if (ry == null) { |
|
ry = rx; |
|
} |
|
return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]]; |
|
}, |
|
getPath = R._getPath = { |
|
path: function (el) { |
|
return el.attr("path"); |
|
}, |
|
circle: function (el) { |
|
var a = el.attrs; |
|
return ellipsePath(a.cx, a.cy, a.r); |
|
}, |
|
ellipse: function (el) { |
|
var a = el.attrs; |
|
return ellipsePath(a.cx, a.cy, a.rx, a.ry); |
|
}, |
|
rect: function (el) { |
|
var a = el.attrs; |
|
return rectPath(a.x, a.y, a.width, a.height, a.r); |
|
}, |
|
image: function (el) { |
|
var a = el.attrs; |
|
return rectPath(a.x, a.y, a.width, a.height); |
|
}, |
|
text: function (el) { |
|
var bbox = el._getBBox(); |
|
return rectPath(bbox.x, bbox.y, bbox.width, bbox.height); |
|
}, |
|
set : function(el) { |
|
var bbox = el._getBBox(); |
|
return rectPath(bbox.x, bbox.y, bbox.width, bbox.height); |
|
} |
|
}, |
|
/*\ |
|
* Raphael.mapPath |
|
[ method ] |
|
** |
|
* Transform the path string with given matrix. |
|
> Parameters |
|
- path (string) path string |
|
- matrix (object) see @Matrix |
|
= (string) transformed path string |
|
\*/ |
|
mapPath = R.mapPath = function (path, matrix) { |
|
if (!matrix) { |
|
return path; |
|
} |
|
var x, y, i, j, ii, jj, pathi; |
|
path = path2curve(path); |
|
for (i = 0, ii = path.length; i < ii; i++) { |
|
pathi = path[i]; |
|
for (j = 1, jj = pathi.length; j < jj; j += 2) { |
|
x = matrix.x(pathi[j], pathi[j + 1]); |
|
y = matrix.y(pathi[j], pathi[j + 1]); |
|
pathi[j] = x; |
|
pathi[j + 1] = y; |
|
} |
|
} |
|
return path; |
|
}; |
|
|
|
R._g = g; |
|
/*\ |
|
* Raphael.type |
|
[ property (string) ] |
|
** |
|
* Can be “SVG”, “VML” or empty, depending on browser support. |
|
\*/ |
|
R.type = (g.win.SVGAngle || g.doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML"); |
|
if (R.type == "VML") { |
|
var d = g.doc.createElement("div"), |
|
b; |
|
d.innerHTML = '<v:shape adj="1"/>'; |
|
b = d.firstChild; |
|
b.style.behavior = "url(#default#VML)"; |
|
if (!(b && typeof b.adj == "object")) { |
|
return (R.type = E); |
|
} |
|
d = null; |
|
} |
|
/*\ |
|
* Raphael.svg |
|
[ property (boolean) ] |
|
** |
|
* `true` if browser supports SVG. |
|
\*/ |
|
/*\ |
|
* Raphael.vml |
|
[ property (boolean) ] |
|
** |
|
* `true` if browser supports VML. |
|
\*/ |
|
R.svg = !(R.vml = R.type == "VML"); |
|
R._Paper = Paper; |
|
/*\ |
|
* Raphael.fn |
|
[ property (object) ] |
|
** |
|
* You can add your own method to the canvas. For example if you want to draw a pie chart, |
|
* you can create your own pie chart function and ship it as a Raphaël plugin. To do this |
|
* you need to extend the `Raphael.fn` object. You should modify the `fn` object before a |
|
* Raphaël instance is created, otherwise it will take no effect. Please note that the |
|
* ability for namespaced plugins was removed in Raphael 2.0. It is up to the plugin to |
|
* ensure any namespacing ensures proper context. |
|
> Usage |
|
| Raphael.fn.arrow = function (x1, y1, x2, y2, size) { |
|
| return this.path( ... ); |
|
| }; |
|
| // or create namespace |
|
| Raphael.fn.mystuff = { |
|
| arrow: function () {…}, |
|
| star: function () {…}, |
|
| // etc… |
|
| }; |
|
| var paper = Raphael(10, 10, 630, 480); |
|
| // then use it |
|
| paper.arrow(10, 10, 30, 30, 5).attr({fill: "#f00"}); |
|
| paper.mystuff.arrow(); |
|
| paper.mystuff.star(); |
|
\*/ |
|
R.fn = paperproto = Paper.prototype = R.prototype; |
|
R._id = 0; |
|
R._oid = 0; |
|
/*\ |
|
* Raphael.is |
|
[ method ] |
|
** |
|
* Handful of replacements for `typeof` operator. |
|
> Parameters |
|
- o (…) any object or primitive |
|
- type (string) name of the type, i.e. “string”, “function”, “number”, etc. |
|
= (boolean) is given value is of given type |
|
\*/ |
|
R.is = function (o, type) { |
|
type = lowerCase.call(type); |
|
if (type == "finite") { |
|
return !isnan[has](+o); |
|
} |
|
if (type == "array") { |
|
return o instanceof Array; |
|
} |
|
return (type == "null" && o === null) || |
|
(type == typeof o && o !== null) || |
|
(type == "object" && o === Object(o)) || |
|
(type == "array" && Array.isArray && Array.isArray(o)) || |
|
objectToString.call(o).slice(8, -1).toLowerCase() == type; |
|
}; |
|
|
|
function clone(obj) { |
|
if (typeof obj == "function" || Object(obj) !== obj) { |
|
return obj; |
|
} |
|
var res = new obj.constructor; |
|
for (var key in obj) if (obj[has](key)) { |
|
res[key] = clone(obj[key]); |
|
} |
|
return res; |
|
} |
|
|
|
/*\ |
|
* Raphael.angle |
|
[ method ] |
|
** |
|
* Returns angle between two or three points |
|
> Parameters |
|
- x1 (number) x coord of first point |
|
- y1 (number) y coord of first point |
|
- x2 (number) x coord of second point |
|
- y2 (number) y coord of second point |
|
- x3 (number) #optional x coord of third point |
|
- y3 (number) #optional y coord of third point |
|
= (number) angle in degrees. |
|
\*/ |
|
R.angle = function (x1, y1, x2, y2, x3, y3) { |
|
if (x3 == null) { |
|
var x = x1 - x2, |
|
y = y1 - y2; |
|
if (!x && !y) { |
|
return 0; |
|
} |
|
return (180 + math.atan2(-y, -x) * 180 / PI + 360) % 360; |
|
} else { |
|
return R.angle(x1, y1, x3, y3) - R.angle(x2, y2, x3, y3); |
|
} |
|
}; |
|
/*\ |
|
* Raphael.rad |
|
[ method ] |
|
** |
|
* Transform angle to radians |
|
> Parameters |
|
- deg (number) angle in degrees |
|
= (number) angle in radians. |
|
\*/ |
|
R.rad = function (deg) { |
|
return deg % 360 * PI / 180; |
|
}; |
|
/*\ |
|
* Raphael.deg |
|
[ method ] |
|
** |
|
* Transform angle to degrees |
|
> Parameters |
|
- rad (number) angle in radians |
|
= (number) angle in degrees. |
|
\*/ |
|
R.deg = function (rad) { |
|
return Math.round ((rad * 180 / PI% 360)* 1000) / 1000; |
|
}; |
|
/*\ |
|
* Raphael.snapTo |
|
[ method ] |
|
** |
|
* Snaps given value to given grid. |
|
> Parameters |
|
- values (array|number) given array of values or step of the grid |
|
- value (number) value to adjust |
|
- tolerance (number) #optional tolerance for snapping. Default is `10`. |
|
= (number) adjusted value. |
|
\*/ |
|
R.snapTo = function (values, value, tolerance) { |
|
tolerance = R.is(tolerance, "finite") ? tolerance : 10; |
|
if (R.is(values, array)) { |
|
var i = values.length; |
|
while (i--) if (abs(values[i] - value) <= tolerance) { |
|
return values[i]; |
|
} |
|
} else { |
|
values = +values; |
|
var rem = value % values; |
|
if (rem < tolerance) { |
|
return value - rem; |
|
} |
|
if (rem > values - tolerance) { |
|
return value - rem + values; |
|
} |
|
} |
|
return value; |
|
}; |
|
|
|
/*\ |
|
* Raphael.createUUID |
|
[ method ] |
|
** |
|
* Returns RFC4122, version 4 ID |
|
\*/ |
|
var createUUID = R.createUUID = (function (uuidRegEx, uuidReplacer) { |
|
return function () { |
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(uuidRegEx, uuidReplacer).toUpperCase(); |
|
}; |
|
})(/[xy]/g, function (c) { |
|
var r = math.random() * 16 | 0, |
|
v = c == "x" ? r : (r & 3 | 8); |
|
return v.toString(16); |
|
}); |
|
|
|
/*\ |
|
* Raphael.setWindow |
|
[ method ] |
|
** |
|
* Used when you need to draw in `<iframe>`. Switched window to the iframe one. |
|
> Parameters |
|
- newwin (window) new window object |
|
\*/ |
|
R.setWindow = function (newwin) { |
|
eve("raphael.setWindow", R, g.win, newwin); |
|
g.win = newwin; |
|
g.doc = g.win.document; |
|
if (R._engine.initWin) { |
|
R._engine.initWin(g.win); |
|
} |
|
}; |
|
var toHex = function (color) { |
|
if (R.vml) { |
|
// http://dean.edwards.name/weblog/2009/10/convert-any-colour-value-to-hex-in-msie/ |
|
var trim = /^\s+|\s+$/g; |
|
var bod; |
|
try { |
|
var docum = new ActiveXObject("htmlfile"); |
|
docum.write("<body>"); |
|
docum.close(); |
|
bod = docum.body; |
|
} catch(e) { |
|
bod = createPopup().document.body; |
|
} |
|
var range = bod.createTextRange(); |
|
toHex = cacher(function (color) { |
|
try { |
|
bod.style.color = Str(color).replace(trim, E); |
|
var value = range.queryCommandValue("ForeColor"); |
|
value = ((value & 255) << 16) | (value & 65280) | ((value & 16711680) >>> 16); |
|
return "#" + ("000000" + value.toString(16)).slice(-6); |
|
} catch(e) { |
|
return "none"; |
|
} |
|
}); |
|
} else { |
|
var i = g.doc.createElement("i"); |
|
i.title = "Rapha\xebl Colour Picker"; |
|
i.style.display = "none"; |
|
g.doc.body.appendChild(i); |
|
toHex = cacher(function (color) { |
|
i.style.color = color; |
|
return g.doc.defaultView.getComputedStyle(i, E).getPropertyValue("color"); |
|
}); |
|
} |
|
return toHex(color); |
|
}, |
|
hsbtoString = function () { |
|
return "hsb(" + [this.h, this.s, this.b] + ")"; |
|
}, |
|
hsltoString = function () { |
|
return "hsl(" + [this.h, this.s, this.l] + ")"; |
|
}, |
|
rgbtoString = function () { |
|
return this.hex; |
|
}, |
|
prepareRGB = function (r, g, b) { |
|
if (g == null && R.is(r, "object") && "r" in r && "g" in r && "b" in r) { |
|
b = r.b; |
|
g = r.g; |
|
r = r.r; |
|
} |
|
if (g == null && R.is(r, string)) { |
|
var clr = R.getRGB(r); |
|
r = clr.r; |
|
g = clr.g; |
|
b = clr.b; |
|
} |
|
if (r > 1 || g > 1 || b > 1) { |
|
r /= 255; |
|
g /= 255; |
|
b /= 255; |
|
} |
|
|
|
return [r, g, b]; |
|
}, |
|
packageRGB = function (r, g, b, o) { |
|
r *= 255; |
|
g *= 255; |
|
b *= 255; |
|
var rgb = { |
|
r: r, |
|
g: g, |
|
b: b, |
|
hex: R.rgb(r, g, b), |
|
toString: rgbtoString |
|
}; |
|
R.is(o, "finite") && (rgb.opacity = o); |
|
return rgb; |
|
}; |
|
|
|
/*\ |
|
* Raphael.color |
|
[ method ] |
|
** |
|
* Parses the color string and returns object with all values for the given color. |
|
> Parameters |
|
- clr (string) color string in one of the supported formats (see @Raphael.getRGB) |
|
= (object) Combined RGB & HSB object in format: |
|
o { |
|
o r (number) red, |
|
o g (number) green, |
|
o b (number) blue, |
|
o hex (string) color in HTML/CSS format: #••••••, |
|
o error (boolean) `true` if string can’t be parsed, |
|
o h (number) hue, |
|
o s (number) saturation, |
|
o v (number) value (brightness), |
|
o l (number) lightness |
|
o } |
|
\*/ |
|
R.color = function (clr) { |
|
var rgb; |
|
if (R.is(clr, "object") && "h" in clr && "s" in clr && "b" in clr) { |
|
rgb = R.hsb2rgb(clr); |
|
clr.r = rgb.r; |
|
clr.g = rgb.g; |
|
clr.b = rgb.b; |
|
clr.hex = rgb.hex; |
|
} else if (R.is(clr, "object") && "h" in clr && "s" in clr && "l" in clr) { |
|
rgb = R.hsl2rgb(clr); |
|
clr.r = rgb.r; |
|
clr.g = rgb.g; |
|
clr.b = rgb.b; |
|
clr.hex = rgb.hex; |
|
} else { |
|
if (R.is(clr, "string")) { |
|
clr = R.getRGB(clr); |
|
} |
|
if (R.is(clr, "object") && "r" in clr && "g" in clr && "b" in clr) { |
|
rgb = R.rgb2hsl(clr); |
|
clr.h = rgb.h; |
|
clr.s = rgb.s; |
|
clr.l = rgb.l; |
|
rgb = R.rgb2hsb(clr); |
|
clr.v = rgb.b; |
|
} else { |
|
clr = {hex: "none"}; |
|
clr.r = clr.g = clr.b = clr.h = clr.s = clr.v = clr.l = -1; |
|
} |
|
} |
|
clr.toString = rgbtoString; |
|
return clr; |
|
}; |
|
/*\ |
|
* Raphael.hsb2rgb |
|
[ method ] |
|
** |
|
* Converts HSB values to RGB object. |
|
> Parameters |
|
- h (number) hue |
|
- s (number) saturation |
|
- v (number) value or brightness |
|
= (object) RGB object in format: |
|
o { |
|
o r (number) red, |
|
o g (number) green, |
|
o b (number) blue, |
|
o hex (string) color in HTML/CSS format: #•••••• |
|
o } |
|
\*/ |
|
R.hsb2rgb = function (h, s, v, o) { |
|
if (this.is(h, "object") && "h" in h && "s" in h && "b" in h) { |
|
v = h.b; |
|
s = h.s; |
|
o = h.o; |
|
h = h.h; |
|
} |
|
h *= 360; |
|
var R, G, B, X, C; |
|
h = (h % 360) / 60; |
|
C = v * s; |
|
X = C * (1 - abs(h % 2 - 1)); |
|
R = G = B = v - C; |
|
|
|
h = ~~h; |
|
R += [C, X, 0, 0, X, C][h]; |
|
G += [X, C, C, X, 0, 0][h]; |
|
B += [0, 0, X, C, C, X][h]; |
|
return packageRGB(R, G, B, o); |
|
}; |
|
/*\ |
|
* Raphael.hsl2rgb |
|
[ method ] |
|
** |
|
* Converts HSL values to RGB object. |
|
> Parameters |
|
- h (number) hue |
|
- s (number) saturation |
|
- l (number) luminosity |
|
= (object) RGB object in format: |
|
o { |
|
o r (number) red, |
|
o g (number) green, |
|
o b (number) blue, |
|
o hex (string) color in HTML/CSS format: #•••••• |
|
o } |
|
\*/ |
|
R.hsl2rgb = function (h, s, l, o) { |
|
if (this.is(h, "object") && "h" in h && "s" in h && "l" in h) { |
|
l = h.l; |
|
s = h.s; |
|
h = h.h; |
|
} |
|
if (h > 1 || s > 1 || l > 1) { |
|
h /= 360; |
|
s /= 100; |
|
l /= 100; |
|
} |
|
h *= 360; |
|
var R, G, B, X, C; |
|
h = (h % 360) / 60; |
|
C = 2 * s * (l < .5 ? l : 1 - l); |
|
X = C * (1 - abs(h % 2 - 1)); |
|
R = G = B = l - C / 2; |
|
|
|
h = ~~h; |
|
R += [C, X, 0, 0, X, C][h]; |
|
G += [X, C, C, X, 0, 0][h]; |
|
B += [0, 0, X, C, C, X][h]; |
|
return packageRGB(R, G, B, o); |
|
}; |
|
/*\ |
|
* Raphael.rgb2hsb |
|
[ method ] |
|
** |
|
* Converts RGB values to HSB object. |
|
> Parameters |
|
- r (number) red |
|
- g (number) green |
|
- b (number) blue |
|
= (object) HSB object in format: |
|
o { |
|
o h (number) hue |
|
o s (number) saturation |
|
o b (number) brightness |
|
o } |
|
\*/ |
|
R.rgb2hsb = function (r, g, b) { |
|
b = prepareRGB(r, g, b); |
|
r = b[0]; |
|
g = b[1]; |
|
b = b[2]; |
|
|
|
var H, S, V, C; |
|
V = mmax(r, g, b); |
|
C = V - mmin(r, g, b); |
|
H = (C == 0 ? null : |
|
V == r ? (g - b) / C : |
|
V == g ? (b - r) / C + 2 : |
|
(r - g) / C + 4 |
|
); |
|
H = ((H + 360) % 6) * 60 / 360; |
|
S = C == 0 ? 0 : C / V; |
|
return {h: H, s: S, b: V, toString: hsbtoString}; |
|
}; |
|
/*\ |
|
* Raphael.rgb2hsl |
|
[ method ] |
|
** |
|
* Converts RGB values to HSL object. |
|
> Parameters |
|
- r (number) red |
|
- g (number) green |
|
- b (number) blue |
|
= (object) HSL object in format: |
|
o { |
|
o h (number) hue |
|
o s (number) saturation |
|
o l (number) luminosity |
|
o } |
|
\*/ |
|
R.rgb2hsl = function (r, g, b) { |
|
b = prepareRGB(r, g, b); |
|
r = b[0]; |
|
g = b[1]; |
|
b = b[2]; |
|
|
|
var H, S, L, M, m, C; |
|
M = mmax(r, g, b); |
|
m = mmin(r, g, b); |
|
C = M - m; |
|
H = (C == 0 ? null : |
|
M == r ? (g - b) / C : |
|
M == g ? (b - r) / C + 2 : |
|
(r - g) / C + 4); |
|
H = ((H + 360) % 6) * 60 / 360; |
|
L = (M + m) / 2; |
|
S = (C == 0 ? 0 : |
|
L < .5 ? C / (2 * L) : |
|
C / (2 - 2 * L)); |
|
return {h: H, s: S, l: L, toString: hsltoString}; |
|
}; |
|
R._path2string = function () { |
|
return this.join(",").replace(p2s, "$1"); |
|
}; |
|
function repush(array, item) { |
|
for (var i = 0, ii = array.length; i < ii; i++) if (array[i] === item) { |
|
return array.push(array.splice(i, 1)[0]); |
|
} |
|
} |
|
function cacher(f, scope, postprocessor) { |
|
function newf() { |
|
var arg = Array.prototype.slice.call(arguments, 0), |
|
args = arg.join("\u2400"), |
|
cache = newf.cache = newf.cache || {}, |
|
count = newf.count = newf.count || []; |
|
if (cache[has](args)) { |
|
repush(count, args); |
|
return postprocessor ? postprocessor(cache[args]) : cache[args]; |
|
} |
|
count.length >= 1e3 && delete cache[count.shift()]; |
|
count.push(args); |
|
cache[args] = f[apply](scope, arg); |
|
return postprocessor ? postprocessor(cache[args]) : cache[args]; |
|
} |
|
return newf; |
|
} |
|
|
|
var preload = R._preload = function (src, f) { |
|
var img = g.doc.createElement("img"); |
|
img.style.cssText = "position:absolute;left:-9999em;top:-9999em"; |
|
img.onload = function () { |
|
f.call(this); |
|
this.onload = null; |
|
g.doc.body.removeChild(this); |
|
}; |
|
img.onerror = function () { |
|
g.doc.body.removeChild(this); |
|
}; |
|
g.doc.body.appendChild(img); |
|
img.src = src; |
|
}; |
|
|
|
function clrToString() { |
|
return this.hex; |
|
} |
|
|
|
/*\ |
|
* Raphael.getRGB |
|
[ method ] |
|
** |
|
* Parses colour string as RGB object |
|
> Parameters |
|
- colour (string) colour string in one of formats: |
|
# <ul> |
|
# <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li> |
|
# <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li> |
|
# <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li> |
|
# <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200, 100, 0)</code>”)</li> |
|
# <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%, 175%, 0%)</code>”)</li> |
|
# <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5, 0.25, 1)</code>”)</li> |
|
# <li>hsb(•••%, •••%, •••%) — same as above, but in %</li> |
|
# <li>hsl(•••, •••, •••) — same as hsb</li> |
|
# <li>hsl(•••%, •••%, •••%) — same as hsb</li> |
|
# </ul> |
|
= (object) RGB object in format: |
|
o { |
|
o r (number) red, |
|
o g (number) green, |
|
o b (number) blue |
|
o hex (string) color in HTML/CSS format: #••••••, |
|
o error (boolean) true if string can’t be parsed |
|
o } |
|
\*/ |
|
R.getRGB = cacher(function (colour) { |
|
if (!colour || !!((colour = Str(colour)).indexOf("-") + 1)) { |
|
return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString}; |
|
} |
|
if (colour == "none") { |
|
return {r: -1, g: -1, b: -1, hex: "none", toString: clrToString}; |
|
} |
|
!(hsrg[has](colour.toLowerCase().substring(0, 2)) || colour.charAt() == "#") && (colour = toHex(colour)); |
|
var res, |
|
red, |
|
green, |
|
blue, |
|
opacity, |
|
t, |
|
values, |
|
rgb = colour.match(colourRegExp); |
|
if (rgb) { |
|
if (rgb[2]) { |
|
blue = toInt(rgb[2].substring(5), 16); |
|
green = toInt(rgb[2].substring(3, 5), 16); |
|
red = toInt(rgb[2].substring(1, 3), 16); |
|
} |
|
if (rgb[3]) { |
|
blue = toInt((t = rgb[3].charAt(3)) + t, 16); |
|
green = toInt((t = rgb[3].charAt(2)) + t, 16); |
|
red = toInt((t = rgb[3].charAt(1)) + t, 16); |
|
} |
|
if (rgb[4]) { |
|
values = rgb[4][split](commaSpaces); |
|
red = toFloat(values[0]); |
|
values[0].slice(-1) == "%" && (red *= 2.55); |
|
green = toFloat(values[1]); |
|
values[1].slice(-1) == "%" && (green *= 2.55); |
|
blue = toFloat(values[2]); |
|
values[2].slice(-1) == "%" && (blue *= 2.55); |
|
rgb[1].toLowerCase().slice(0, 4) == "rgba" && (opacity = toFloat(values[3])); |
|
values[3] && values[3].slice(-1) == "%" && (opacity /= 100); |
|
} |
|
if (rgb[5]) { |
|
values = rgb[5][split](commaSpaces); |
|
red = toFloat(values[0]); |
|
values[0].slice(-1) == "%" && (red *= 2.55); |
|
green = toFloat(values[1]); |
|
values[1].slice(-1) == "%" && (green *= 2.55); |
|
blue = toFloat(values[2]); |
|
values[2].slice(-1) == "%" && (blue *= 2.55); |
|
(values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360); |
|
rgb[1].toLowerCase().slice(0, 4) == "hsba" && (opacity = toFloat(values[3])); |
|
values[3] && values[3].slice(-1) == "%" && (opacity /= 100); |
|
return R.hsb2rgb(red, green, blue, opacity); |
|
} |
|
if (rgb[6]) { |
|
values = rgb[6][split](commaSpaces); |
|
red = toFloat(values[0]); |
|
values[0].slice(-1) == "%" && (red *= 2.55); |
|
green = toFloat(values[1]); |
|
values[1].slice(-1) == "%" && (green *= 2.55); |
|
blue = toFloat(values[2]); |
|
values[2].slice(-1) == "%" && (blue *= 2.55); |
|
(values[0].slice(-3) == "deg" || values[0].slice(-1) == "\xb0") && (red /= 360); |
|
rgb[1].toLowerCase().slice(0, 4) == "hsla" && (opacity = toFloat(values[3])); |
|
values[3] && values[3].slice(-1) == "%" && (opacity /= 100); |
|
return R.hsl2rgb(red, green, blue, opacity); |
|
} |
|
rgb = {r: red, g: green, b: blue, toString: clrToString}; |
|
rgb.hex = "#" + (16777216 | blue | (green << 8) | (red << 16)).toString(16).slice(1); |
|
R.is(opacity, "finite") && (rgb.opacity = opacity); |
|
return rgb; |
|
} |
|
return {r: -1, g: -1, b: -1, hex: "none", error: 1, toString: clrToString}; |
|
}, R); |
|
/*\ |
|
* Raphael.hsb |
|
[ method ] |
|
** |
|
* Converts HSB values to hex representation of the colour. |
|
> Parameters |
|
- h (number) hue |
|
- s (number) saturation |
|
- b (number) value or brightness |
|
= (string) hex representation of the colour. |
|
\*/ |
|
R.hsb = cacher(function (h, s, b) { |
|
return R.hsb2rgb(h, s, b).hex; |
|
}); |
|
/*\ |
|
* Raphael.hsl |
|
[ method ] |
|
** |
|
* Converts HSL values to hex representation of the colour. |
|
> Parameters |
|
- h (number) hue |
|
- s (number) saturation |
|
- l (number) luminosity |
|
= (string) hex representation of the colour. |
|
\*/ |
|
R.hsl = cacher(function (h, s, l) { |
|
return R.hsl2rgb(h, s, l).hex; |
|
}); |
|
/*\ |
|
* Raphael.rgb |
|
[ method ] |
|
** |
|
* Converts RGB values to hex representation of the colour. |
|
> Parameters |
|
- r (number) red |
|
- g (number) green |
|
- b (number) blue |
|
= (string) hex representation of the colour. |
|
\*/ |
|
R.rgb = cacher(function (r, g, b) { |
|
function round(x) { return (x + 0.5) | 0; } |
|
return "#" + (16777216 | round(b) | (round(g) << 8) | (round(r) << 16)).toString(16).slice(1); |
|
}); |
|
/*\ |
|
* Raphael.getColor |
|
[ method ] |
|
** |
|
* On each call returns next colour in the spectrum. To reset it back to red call @Raphael.getColor.reset |
|
> Parameters |
|
- value (number) #optional brightness, default is `0.75` |
|
= (string) hex representation of the colour. |
|
\*/ |
|
R.getColor = function (value) { |
|
var start = this.getColor.start = this.getColor.start || {h: 0, s: 1, b: value || .75}, |
|
rgb = this.hsb2rgb(start.h, start.s, start.b); |
|
start.h += .075; |
|
if (start.h > 1) { |
|
start.h = 0; |
|
start.s -= .2; |
|
start.s <= 0 && (this.getColor.start = {h: 0, s: 1, b: start.b}); |
|
} |
|
return rgb.hex; |
|
}; |
|
/*\ |
|
* Raphael.getColor.reset |
|
[ method ] |
|
** |
|
* Resets spectrum position for @Raphael.getColor back to red. |
|
\*/ |
|
R.getColor.reset = function () { |
|
delete this.start; |
|
}; |
|
|
|
// http://schepers.cc/getting-to-the-point |
|
function catmullRom2bezier(crp, z) { |
|
var d = []; |
|
for (var i = 0, iLen = crp.length; iLen - 2 * !z > i; i += 2) { |
|
var p = [ |
|
{x: +crp[i - 2], y: +crp[i - 1]}, |
|
{x: +crp[i], y: +crp[i + 1]}, |
|
{x: +crp[i + 2], y: +crp[i + 3]}, |
|
{x: +crp[i + 4], y: +crp[i + 5]} |
|
]; |
|
if (z) { |
|
if (!i) { |
|
p[0] = {x: +crp[iLen - 2], y: +crp[iLen - 1]}; |
|
} else if (iLen - 4 == i) { |
|
p[3] = {x: +crp[0], y: +crp[1]}; |
|
} else if (iLen - 2 == i) { |
|
p[2] = {x: +crp[0], y: +crp[1]}; |
|
p[3] = {x: +crp[2], y: +crp[3]}; |
|
} |
|
} else { |
|
if (iLen - 4 == i) { |
|
p[3] = p[2]; |
|
} else if (!i) { |
|
p[0] = {x: +crp[i], y: +crp[i + 1]}; |
|
} |
|
} |
|
d.push(["C", |
|
(-p[0].x + 6 * p[1].x + p[2].x) / 6, |
|
(-p[0].y + 6 * p[1].y + p[2].y) / 6, |
|
(p[1].x + 6 * p[2].x - p[3].x) / 6, |
|
(p[1].y + 6*p[2].y - p[3].y) / 6, |
|
p[2].x, |
|
p[2].y |
|
]); |
|
} |
|
|
|
return d; |
|
} |
|
/*\ |
|
* Raphael.parsePathString |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Parses given path string into an array of arrays of path segments. |
|
> Parameters |
|
- pathString (string|array) path string or array of segments (in the last case it will be returned straight away) |
|
= (array) array of segments. |
|
\*/ |
|
R.parsePathString = function (pathString) { |
|
if (!pathString) { |
|
return null; |
|
} |
|
var pth = paths(pathString); |
|
if (pth.arr) { |
|
return pathClone(pth.arr); |
|
} |
|
|
|
var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, r: 4, q: 4, s: 4, t: 2, v: 1, z: 0}, |
|
data = []; |
|
if (R.is(pathString, array) && R.is(pathString[0], array)) { // rough assumption |
|
data = pathClone(pathString); |
|
} |
|
if (!data.length) { |
|
Str(pathString).replace(pathCommand, function (a, b, c) { |
|
var params = [], |
|
name = b.toLowerCase(); |
|
c.replace(pathValues, function (a, b) { |
|
b && params.push(+b); |
|
}); |
|
if (name == "m" && params.length > 2) { |
|
data.push([b][concat](params.splice(0, 2))); |
|
name = "l"; |
|
b = b == "m" ? "l" : "L"; |
|
} |
|
if (name == "r") { |
|
data.push([b][concat](params)); |
|
} else while (params.length >= paramCounts[name]) { |
|
data.push([b][concat](params.splice(0, paramCounts[name]))); |
|
if (!paramCounts[name]) { |
|
break; |
|
} |
|
} |
|
}); |
|
} |
|
data.toString = R._path2string; |
|
pth.arr = pathClone(data); |
|
return data; |
|
}; |
|
/*\ |
|
* Raphael.parseTransformString |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Parses given path string into an array of transformations. |
|
> Parameters |
|
- TString (string|array) transform string or array of transformations (in the last case it will be returned straight away) |
|
= (array) array of transformations. |
|
\*/ |
|
R.parseTransformString = cacher(function (TString) { |
|
if (!TString) { |
|
return null; |
|
} |
|
var paramCounts = {r: 3, s: 4, t: 2, m: 6}, |
|
data = []; |
|
if (R.is(TString, array) && R.is(TString[0], array)) { // rough assumption |
|
data = pathClone(TString); |
|
} |
|
if (!data.length) { |
|
Str(TString).replace(tCommand, function (a, b, c) { |
|
var params = [], |
|
name = lowerCase.call(b); |
|
c.replace(pathValues, function (a, b) { |
|
b && params.push(+b); |
|
}); |
|
data.push([b][concat](params)); |
|
}); |
|
} |
|
data.toString = R._path2string; |
|
return data; |
|
}); |
|
// PATHS |
|
var paths = function (ps) { |
|
var p = paths.ps = paths.ps || {}; |
|
if (p[ps]) { |
|
p[ps].sleep = 100; |
|
} else { |
|
p[ps] = { |
|
sleep: 100 |
|
}; |
|
} |
|
setTimeout(function () { |
|
for (var key in p) if (p[has](key) && key != ps) { |
|
p[key].sleep--; |
|
!p[key].sleep && delete p[key]; |
|
} |
|
}); |
|
return p[ps]; |
|
}; |
|
/*\ |
|
* Raphael.findDotsAtSegment |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Find dot coordinates on the given cubic bezier curve at the given t. |
|
> Parameters |
|
- p1x (number) x of the first point of the curve |
|
- p1y (number) y of the first point of the curve |
|
- c1x (number) x of the first anchor of the curve |
|
- c1y (number) y of the first anchor of the curve |
|
- c2x (number) x of the second anchor of the curve |
|
- c2y (number) y of the second anchor of the curve |
|
- p2x (number) x of the second point of the curve |
|
- p2y (number) y of the second point of the curve |
|
- t (number) position on the curve (0..1) |
|
= (object) point information in format: |
|
o { |
|
o x: (number) x coordinate of the point |
|
o y: (number) y coordinate of the point |
|
o m: { |
|
o x: (number) x coordinate of the left anchor |
|
o y: (number) y coordinate of the left anchor |
|
o } |
|
o n: { |
|
o x: (number) x coordinate of the right anchor |
|
o y: (number) y coordinate of the right anchor |
|
o } |
|
o start: { |
|
o x: (number) x coordinate of the start of the curve |
|
o y: (number) y coordinate of the start of the curve |
|
o } |
|
o end: { |
|
o x: (number) x coordinate of the end of the curve |
|
o y: (number) y coordinate of the end of the curve |
|
o } |
|
o alpha: (number) angle of the curve derivative at the point |
|
o } |
|
\*/ |
|
R.findDotsAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) { |
|
var t1 = 1 - t, |
|
t13 = pow(t1, 3), |
|
t12 = pow(t1, 2), |
|
t2 = t * t, |
|
t3 = t2 * t, |
|
x = t13 * p1x + t12 * 3 * t * c1x + t1 * 3 * t * t * c2x + t3 * p2x, |
|
y = t13 * p1y + t12 * 3 * t * c1y + t1 * 3 * t * t * c2y + t3 * p2y, |
|
mx = p1x + 2 * t * (c1x - p1x) + t2 * (c2x - 2 * c1x + p1x), |
|
my = p1y + 2 * t * (c1y - p1y) + t2 * (c2y - 2 * c1y + p1y), |
|
nx = c1x + 2 * t * (c2x - c1x) + t2 * (p2x - 2 * c2x + c1x), |
|
ny = c1y + 2 * t * (c2y - c1y) + t2 * (p2y - 2 * c2y + c1y), |
|
ax = t1 * p1x + t * c1x, |
|
ay = t1 * p1y + t * c1y, |
|
cx = t1 * c2x + t * p2x, |
|
cy = t1 * c2y + t * p2y, |
|
alpha = (90 - math.atan2(mx - nx, my - ny) * 180 / PI); |
|
(mx > nx || my < ny) && (alpha += 180); |
|
return { |
|
x: x, |
|
y: y, |
|
m: {x: mx, y: my}, |
|
n: {x: nx, y: ny}, |
|
start: {x: ax, y: ay}, |
|
end: {x: cx, y: cy}, |
|
alpha: alpha |
|
}; |
|
}; |
|
/*\ |
|
* Raphael.bezierBBox |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Return bounding box of a given cubic bezier curve |
|
> Parameters |
|
- p1x (number) x of the first point of the curve |
|
- p1y (number) y of the first point of the curve |
|
- c1x (number) x of the first anchor of the curve |
|
- c1y (number) y of the first anchor of the curve |
|
- c2x (number) x of the second anchor of the curve |
|
- c2y (number) y of the second anchor of the curve |
|
- p2x (number) x of the second point of the curve |
|
- p2y (number) y of the second point of the curve |
|
* or |
|
- bez (array) array of six points for bezier curve |
|
= (object) point information in format: |
|
o { |
|
o min: { |
|
o x: (number) x coordinate of the left point |
|
o y: (number) y coordinate of the top point |
|
o } |
|
o max: { |
|
o x: (number) x coordinate of the right point |
|
o y: (number) y coordinate of the bottom point |
|
o } |
|
o } |
|
\*/ |
|
R.bezierBBox = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) { |
|
if (!R.is(p1x, "array")) { |
|
p1x = [p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y]; |
|
} |
|
var bbox = curveDim.apply(null, p1x); |
|
return { |
|
x: bbox.min.x, |
|
y: bbox.min.y, |
|
x2: bbox.max.x, |
|
y2: bbox.max.y, |
|
width: bbox.max.x - bbox.min.x, |
|
height: bbox.max.y - bbox.min.y |
|
}; |
|
}; |
|
/*\ |
|
* Raphael.isPointInsideBBox |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Returns `true` if given point is inside bounding boxes. |
|
> Parameters |
|
- bbox (string) bounding box |
|
- x (string) x coordinate of the point |
|
- y (string) y coordinate of the point |
|
= (boolean) `true` if point inside |
|
\*/ |
|
R.isPointInsideBBox = function (bbox, x, y) { |
|
return x >= bbox.x && x <= bbox.x2 && y >= bbox.y && y <= bbox.y2; |
|
}; |
|
/*\ |
|
* Raphael.isBBoxIntersect |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Returns `true` if two bounding boxes intersect |
|
> Parameters |
|
- bbox1 (string) first bounding box |
|
- bbox2 (string) second bounding box |
|
= (boolean) `true` if they intersect |
|
\*/ |
|
R.isBBoxIntersect = function (bbox1, bbox2) { |
|
var i = R.isPointInsideBBox; |
|
return i(bbox2, bbox1.x, bbox1.y) |
|
|| i(bbox2, bbox1.x2, bbox1.y) |
|
|| i(bbox2, bbox1.x, bbox1.y2) |
|
|| i(bbox2, bbox1.x2, bbox1.y2) |
|
|| i(bbox1, bbox2.x, bbox2.y) |
|
|| i(bbox1, bbox2.x2, bbox2.y) |
|
|| i(bbox1, bbox2.x, bbox2.y2) |
|
|| i(bbox1, bbox2.x2, bbox2.y2) |
|
|| (bbox1.x < bbox2.x2 && bbox1.x > bbox2.x || bbox2.x < bbox1.x2 && bbox2.x > bbox1.x) |
|
&& (bbox1.y < bbox2.y2 && bbox1.y > bbox2.y || bbox2.y < bbox1.y2 && bbox2.y > bbox1.y); |
|
}; |
|
function base3(t, p1, p2, p3, p4) { |
|
var t1 = -3 * p1 + 9 * p2 - 9 * p3 + 3 * p4, |
|
t2 = t * t1 + 6 * p1 - 12 * p2 + 6 * p3; |
|
return t * t2 - 3 * p1 + 3 * p2; |
|
} |
|
function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) { |
|
if (z == null) { |
|
z = 1; |
|
} |
|
z = z > 1 ? 1 : z < 0 ? 0 : z; |
|
var z2 = z / 2, |
|
n = 12, |
|
Tvalues = [-0.1252,0.1252,-0.3678,0.3678,-0.5873,0.5873,-0.7699,0.7699,-0.9041,0.9041,-0.9816,0.9816], |
|
Cvalues = [0.2491,0.2491,0.2335,0.2335,0.2032,0.2032,0.1601,0.1601,0.1069,0.1069,0.0472,0.0472], |
|
sum = 0; |
|
for (var i = 0; i < n; i++) { |
|
var ct = z2 * Tvalues[i] + z2, |
|
xbase = base3(ct, x1, x2, x3, x4), |
|
ybase = base3(ct, y1, y2, y3, y4), |
|
comb = xbase * xbase + ybase * ybase; |
|
sum += Cvalues[i] * math.sqrt(comb); |
|
} |
|
return z2 * sum; |
|
} |
|
function getTatLen(x1, y1, x2, y2, x3, y3, x4, y4, ll) { |
|
if (ll < 0 || bezlen(x1, y1, x2, y2, x3, y3, x4, y4) < ll) { |
|
return; |
|
} |
|
var t = 1, |
|
step = t / 2, |
|
t2 = t - step, |
|
l, |
|
e = .01; |
|
l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2); |
|
while (abs(l - ll) > e) { |
|
step /= 2; |
|
t2 += (l < ll ? 1 : -1) * step; |
|
l = bezlen(x1, y1, x2, y2, x3, y3, x4, y4, t2); |
|
} |
|
return t2; |
|
} |
|
function intersect(x1, y1, x2, y2, x3, y3, x4, y4) { |
|
if ( |
|
mmax(x1, x2) < mmin(x3, x4) || |
|
mmin(x1, x2) > mmax(x3, x4) || |
|
mmax(y1, y2) < mmin(y3, y4) || |
|
mmin(y1, y2) > mmax(y3, y4) |
|
) { |
|
return; |
|
} |
|
var nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), |
|
ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), |
|
denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); |
|
|
|
if (!denominator) { |
|
return; |
|
} |
|
var px = nx / denominator, |
|
py = ny / denominator, |
|
px2 = +px.toFixed(2), |
|
py2 = +py.toFixed(2); |
|
if ( |
|
px2 < +mmin(x1, x2).toFixed(2) || |
|
px2 > +mmax(x1, x2).toFixed(2) || |
|
px2 < +mmin(x3, x4).toFixed(2) || |
|
px2 > +mmax(x3, x4).toFixed(2) || |
|
py2 < +mmin(y1, y2).toFixed(2) || |
|
py2 > +mmax(y1, y2).toFixed(2) || |
|
py2 < +mmin(y3, y4).toFixed(2) || |
|
py2 > +mmax(y3, y4).toFixed(2) |
|
) { |
|
return; |
|
} |
|
return {x: px, y: py}; |
|
} |
|
function inter(bez1, bez2) { |
|
return interHelper(bez1, bez2); |
|
} |
|
function interCount(bez1, bez2) { |
|
return interHelper(bez1, bez2, 1); |
|
} |
|
function interHelper(bez1, bez2, justCount) { |
|
var bbox1 = R.bezierBBox(bez1), |
|
bbox2 = R.bezierBBox(bez2); |
|
if (!R.isBBoxIntersect(bbox1, bbox2)) { |
|
return justCount ? 0 : []; |
|
} |
|
var l1 = bezlen.apply(0, bez1), |
|
l2 = bezlen.apply(0, bez2), |
|
n1 = mmax(~~(l1 / 5), 1), |
|
n2 = mmax(~~(l2 / 5), 1), |
|
dots1 = [], |
|
dots2 = [], |
|
xy = {}, |
|
res = justCount ? 0 : []; |
|
for (var i = 0; i < n1 + 1; i++) { |
|
var p = R.findDotsAtSegment.apply(R, bez1.concat(i / n1)); |
|
dots1.push({x: p.x, y: p.y, t: i / n1}); |
|
} |
|
for (i = 0; i < n2 + 1; i++) { |
|
p = R.findDotsAtSegment.apply(R, bez2.concat(i / n2)); |
|
dots2.push({x: p.x, y: p.y, t: i / n2}); |
|
} |
|
for (i = 0; i < n1; i++) { |
|
for (var j = 0; j < n2; j++) { |
|
var di = dots1[i], |
|
di1 = dots1[i + 1], |
|
dj = dots2[j], |
|
dj1 = dots2[j + 1], |
|
ci = abs(di1.x - di.x) < .001 ? "y" : "x", |
|
cj = abs(dj1.x - dj.x) < .001 ? "y" : "x", |
|
is = intersect(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y); |
|
if (is) { |
|
if (xy[is.x.toFixed(4)] == is.y.toFixed(4)) { |
|
continue; |
|
} |
|
xy[is.x.toFixed(4)] = is.y.toFixed(4); |
|
var t1 = di.t + abs((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t), |
|
t2 = dj.t + abs((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t); |
|
if (t1 >= 0 && t1 <= 1.001 && t2 >= 0 && t2 <= 1.001) { |
|
if (justCount) { |
|
res++; |
|
} else { |
|
res.push({ |
|
x: is.x, |
|
y: is.y, |
|
t1: mmin(t1, 1), |
|
t2: mmin(t2, 1) |
|
}); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
return res; |
|
} |
|
/*\ |
|
* Raphael.pathIntersection |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Finds intersections of two paths |
|
> Parameters |
|
- path1 (string) path string |
|
- path2 (string) path string |
|
= (array) dots of intersection |
|
o [ |
|
o { |
|
o x: (number) x coordinate of the point |
|
o y: (number) y coordinate of the point |
|
o t1: (number) t value for segment of path1 |
|
o t2: (number) t value for segment of path2 |
|
o segment1: (number) order number for segment of path1 |
|
o segment2: (number) order number for segment of path2 |
|
o bez1: (array) eight coordinates representing beziér curve for the segment of path1 |
|
o bez2: (array) eight coordinates representing beziér curve for the segment of path2 |
|
o } |
|
o ] |
|
\*/ |
|
R.pathIntersection = function (path1, path2) { |
|
return interPathHelper(path1, path2); |
|
}; |
|
R.pathIntersectionNumber = function (path1, path2) { |
|
return interPathHelper(path1, path2, 1); |
|
}; |
|
function interPathHelper(path1, path2, justCount) { |
|
path1 = R._path2curve(path1); |
|
path2 = R._path2curve(path2); |
|
var x1, y1, x2, y2, x1m, y1m, x2m, y2m, bez1, bez2, |
|
res = justCount ? 0 : []; |
|
for (var i = 0, ii = path1.length; i < ii; i++) { |
|
var pi = path1[i]; |
|
if (pi[0] == "M") { |
|
x1 = x1m = pi[1]; |
|
y1 = y1m = pi[2]; |
|
} else { |
|
if (pi[0] == "C") { |
|
bez1 = [x1, y1].concat(pi.slice(1)); |
|
x1 = bez1[6]; |
|
y1 = bez1[7]; |
|
} else { |
|
bez1 = [x1, y1, x1, y1, x1m, y1m, x1m, y1m]; |
|
x1 = x1m; |
|
y1 = y1m; |
|
} |
|
for (var j = 0, jj = path2.length; j < jj; j++) { |
|
var pj = path2[j]; |
|
if (pj[0] == "M") { |
|
x2 = x2m = pj[1]; |
|
y2 = y2m = pj[2]; |
|
} else { |
|
if (pj[0] == "C") { |
|
bez2 = [x2, y2].concat(pj.slice(1)); |
|
x2 = bez2[6]; |
|
y2 = bez2[7]; |
|
} else { |
|
bez2 = [x2, y2, x2, y2, x2m, y2m, x2m, y2m]; |
|
x2 = x2m; |
|
y2 = y2m; |
|
} |
|
var intr = interHelper(bez1, bez2, justCount); |
|
if (justCount) { |
|
res += intr; |
|
} else { |
|
for (var k = 0, kk = intr.length; k < kk; k++) { |
|
intr[k].segment1 = i; |
|
intr[k].segment2 = j; |
|
intr[k].bez1 = bez1; |
|
intr[k].bez2 = bez2; |
|
} |
|
res = res.concat(intr); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
return res; |
|
} |
|
/*\ |
|
* Raphael.isPointInsidePath |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Returns `true` if given point is inside a given closed path. |
|
> Parameters |
|
- path (string) path string |
|
- x (number) x of the point |
|
- y (number) y of the point |
|
= (boolean) true, if point is inside the path |
|
\*/ |
|
R.isPointInsidePath = function (path, x, y) { |
|
var bbox = R.pathBBox(path); |
|
return R.isPointInsideBBox(bbox, x, y) && |
|
interPathHelper(path, [["M", x, y], ["H", bbox.x2 + 10]], 1) % 2 == 1; |
|
}; |
|
R._removedFactory = function (methodname) { |
|
return function () { |
|
eve("raphael.log", null, "Rapha\xebl: you are calling to method \u201c" + methodname + "\u201d of removed object", methodname); |
|
}; |
|
}; |
|
/*\ |
|
* Raphael.pathBBox |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Return bounding box of a given path |
|
> Parameters |
|
- path (string) path string |
|
= (object) bounding box |
|
o { |
|
o x: (number) x coordinate of the left top point of the box |
|
o y: (number) y coordinate of the left top point of the box |
|
o x2: (number) x coordinate of the right bottom point of the box |
|
o y2: (number) y coordinate of the right bottom point of the box |
|
o width: (number) width of the box |
|
o height: (number) height of the box |
|
o cx: (number) x coordinate of the center of the box |
|
o cy: (number) y coordinate of the center of the box |
|
o } |
|
\*/ |
|
var pathDimensions = R.pathBBox = function (path) { |
|
var pth = paths(path); |
|
if (pth.bbox) { |
|
return clone(pth.bbox); |
|
} |
|
if (!path) { |
|
return {x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0}; |
|
} |
|
path = path2curve(path); |
|
var x = 0, |
|
y = 0, |
|
X = [], |
|
Y = [], |
|
p; |
|
for (var i = 0, ii = path.length; i < ii; i++) { |
|
p = path[i]; |
|
if (p[0] == "M") { |
|
x = p[1]; |
|
y = p[2]; |
|
X.push(x); |
|
Y.push(y); |
|
} else { |
|
var dim = curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]); |
|
X = X[concat](dim.min.x, dim.max.x); |
|
Y = Y[concat](dim.min.y, dim.max.y); |
|
x = p[5]; |
|
y = p[6]; |
|
} |
|
} |
|
var xmin = mmin[apply](0, X), |
|
ymin = mmin[apply](0, Y), |
|
xmax = mmax[apply](0, X), |
|
ymax = mmax[apply](0, Y), |
|
width = xmax - xmin, |
|
height = ymax - ymin, |
|
bb = { |
|
x: xmin, |
|
y: ymin, |
|
x2: xmax, |
|
y2: ymax, |
|
width: width, |
|
height: height, |
|
cx: xmin + width / 2, |
|
cy: ymin + height / 2 |
|
}; |
|
pth.bbox = clone(bb); |
|
return bb; |
|
}, |
|
pathClone = function (pathArray) { |
|
var res = clone(pathArray); |
|
res.toString = R._path2string; |
|
return res; |
|
}, |
|
pathToRelative = R._pathToRelative = function (pathArray) { |
|
var pth = paths(pathArray); |
|
if (pth.rel) { |
|
return pathClone(pth.rel); |
|
} |
|
if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption |
|
pathArray = R.parsePathString(pathArray); |
|
} |
|
var res = [], |
|
x = 0, |
|
y = 0, |
|
mx = 0, |
|
my = 0, |
|
start = 0; |
|
if (pathArray[0][0] == "M") { |
|
x = pathArray[0][1]; |
|
y = pathArray[0][2]; |
|
mx = x; |
|
my = y; |
|
start++; |
|
res.push(["M", x, y]); |
|
} |
|
for (var i = start, ii = pathArray.length; i < ii; i++) { |
|
var r = res[i] = [], |
|
pa = pathArray[i]; |
|
if (pa[0] != lowerCase.call(pa[0])) { |
|
r[0] = lowerCase.call(pa[0]); |
|
switch (r[0]) { |
|
case "a": |
|
r[1] = pa[1]; |
|
r[2] = pa[2]; |
|
r[3] = pa[3]; |
|
r[4] = pa[4]; |
|
r[5] = pa[5]; |
|
r[6] = +(pa[6] - x).toFixed(3); |
|
r[7] = +(pa[7] - y).toFixed(3); |
|
break; |
|
case "v": |
|
r[1] = +(pa[1] - y).toFixed(3); |
|
break; |
|
case "m": |
|
mx = pa[1]; |
|
my = pa[2]; |
|
default: |
|
for (var j = 1, jj = pa.length; j < jj; j++) { |
|
r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3); |
|
} |
|
} |
|
} else { |
|
r = res[i] = []; |
|
if (pa[0] == "m") { |
|
mx = pa[1] + x; |
|
my = pa[2] + y; |
|
} |
|
for (var k = 0, kk = pa.length; k < kk; k++) { |
|
res[i][k] = pa[k]; |
|
} |
|
} |
|
var len = res[i].length; |
|
switch (res[i][0]) { |
|
case "z": |
|
x = mx; |
|
y = my; |
|
break; |
|
case "h": |
|
x += +res[i][len - 1]; |
|
break; |
|
case "v": |
|
y += +res[i][len - 1]; |
|
break; |
|
default: |
|
x += +res[i][len - 2]; |
|
y += +res[i][len - 1]; |
|
} |
|
} |
|
res.toString = R._path2string; |
|
pth.rel = pathClone(res); |
|
return res; |
|
}, |
|
pathToAbsolute = R._pathToAbsolute = function (pathArray) { |
|
var pth = paths(pathArray); |
|
if (pth.abs) { |
|
return pathClone(pth.abs); |
|
} |
|
if (!R.is(pathArray, array) || !R.is(pathArray && pathArray[0], array)) { // rough assumption |
|
pathArray = R.parsePathString(pathArray); |
|
} |
|
if (!pathArray || !pathArray.length) { |
|
return [["M", 0, 0]]; |
|
} |
|
var res = [], |
|
x = 0, |
|
y = 0, |
|
mx = 0, |
|
my = 0, |
|
start = 0; |
|
if (pathArray[0][0] == "M") { |
|
x = +pathArray[0][1]; |
|
y = +pathArray[0][2]; |
|
mx = x; |
|
my = y; |
|
start++; |
|
res[0] = ["M", x, y]; |
|
} |
|
var crz = pathArray.length == 3 && pathArray[0][0] == "M" && pathArray[1][0].toUpperCase() == "R" && pathArray[2][0].toUpperCase() == "Z"; |
|
for (var r, pa, i = start, ii = pathArray.length; i < ii; i++) { |
|
res.push(r = []); |
|
pa = pathArray[i]; |
|
if (pa[0] != upperCase.call(pa[0])) { |
|
r[0] = upperCase.call(pa[0]); |
|
switch (r[0]) { |
|
case "A": |
|
r[1] = pa[1]; |
|
r[2] = pa[2]; |
|
r[3] = pa[3]; |
|
r[4] = pa[4]; |
|
r[5] = pa[5]; |
|
r[6] = +(pa[6] + x); |
|
r[7] = +(pa[7] + y); |
|
break; |
|
case "V": |
|
r[1] = +pa[1] + y; |
|
break; |
|
case "H": |
|
r[1] = +pa[1] + x; |
|
break; |
|
case "R": |
|
var dots = [x, y][concat](pa.slice(1)); |
|
for (var j = 2, jj = dots.length; j < jj; j++) { |
|
dots[j] = +dots[j] + x; |
|
dots[++j] = +dots[j] + y; |
|
} |
|
res.pop(); |
|
res = res[concat](catmullRom2bezier(dots, crz)); |
|
break; |
|
case "M": |
|
mx = +pa[1] + x; |
|
my = +pa[2] + y; |
|
default: |
|
for (j = 1, jj = pa.length; j < jj; j++) { |
|
r[j] = +pa[j] + ((j % 2) ? x : y); |
|
} |
|
} |
|
} else if (pa[0] == "R") { |
|
dots = [x, y][concat](pa.slice(1)); |
|
res.pop(); |
|
res = res[concat](catmullRom2bezier(dots, crz)); |
|
r = ["R"][concat](pa.slice(-2)); |
|
} else { |
|
for (var k = 0, kk = pa.length; k < kk; k++) { |
|
r[k] = pa[k]; |
|
} |
|
} |
|
switch (r[0]) { |
|
case "Z": |
|
x = mx; |
|
y = my; |
|
break; |
|
case "H": |
|
x = r[1]; |
|
break; |
|
case "V": |
|
y = r[1]; |
|
break; |
|
case "M": |
|
mx = r[r.length - 2]; |
|
my = r[r.length - 1]; |
|
default: |
|
x = r[r.length - 2]; |
|
y = r[r.length - 1]; |
|
} |
|
} |
|
res.toString = R._path2string; |
|
pth.abs = pathClone(res); |
|
return res; |
|
}, |
|
l2c = function (x1, y1, x2, y2) { |
|
return [x1, y1, x2, y2, x2, y2]; |
|
}, |
|
q2c = function (x1, y1, ax, ay, x2, y2) { |
|
var _13 = 1 / 3, |
|
_23 = 2 / 3; |
|
return [ |
|
_13 * x1 + _23 * ax, |
|
_13 * y1 + _23 * ay, |
|
_13 * x2 + _23 * ax, |
|
_13 * y2 + _23 * ay, |
|
x2, |
|
y2 |
|
]; |
|
}, |
|
a2c = function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) { |
|
// for more information of where this math came from visit: |
|
// http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes |
|
var _120 = PI * 120 / 180, |
|
rad = PI / 180 * (+angle || 0), |
|
res = [], |
|
xy, |
|
rotate = cacher(function (x, y, rad) { |
|
var X = x * math.cos(rad) - y * math.sin(rad), |
|
Y = x * math.sin(rad) + y * math.cos(rad); |
|
return {x: X, y: Y}; |
|
}); |
|
if (!recursive) { |
|
xy = rotate(x1, y1, -rad); |
|
x1 = xy.x; |
|
y1 = xy.y; |
|
xy = rotate(x2, y2, -rad); |
|
x2 = xy.x; |
|
y2 = xy.y; |
|
var cos = math.cos(PI / 180 * angle), |
|
sin = math.sin(PI / 180 * angle), |
|
x = (x1 - x2) / 2, |
|
y = (y1 - y2) / 2; |
|
var h = (x * x) / (rx * rx) + (y * y) / (ry * ry); |
|
if (h > 1) { |
|
h = math.sqrt(h); |
|
rx = h * rx; |
|
ry = h * ry; |
|
} |
|
var rx2 = rx * rx, |
|
ry2 = ry * ry, |
|
k = (large_arc_flag == sweep_flag ? -1 : 1) * |
|
math.sqrt(abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x))), |
|
cx = k * rx * y / ry + (x1 + x2) / 2, |
|
cy = k * -ry * x / rx + (y1 + y2) / 2, |
|
f1 = math.asin(((y1 - cy) / ry).toFixed(9)), |
|
f2 = math.asin(((y2 - cy) / ry).toFixed(9)); |
|
|
|
f1 = x1 < cx ? PI - f1 : f1; |
|
f2 = x2 < cx ? PI - f2 : f2; |
|
f1 < 0 && (f1 = PI * 2 + f1); |
|
f2 < 0 && (f2 = PI * 2 + f2); |
|
if (sweep_flag && f1 > f2) { |
|
f1 = f1 - PI * 2; |
|
} |
|
if (!sweep_flag && f2 > f1) { |
|
f2 = f2 - PI * 2; |
|
} |
|
} else { |
|
f1 = recursive[0]; |
|
f2 = recursive[1]; |
|
cx = recursive[2]; |
|
cy = recursive[3]; |
|
} |
|
var df = f2 - f1; |
|
if (abs(df) > _120) { |
|
var f2old = f2, |
|
x2old = x2, |
|
y2old = y2; |
|
f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1); |
|
x2 = cx + rx * math.cos(f2); |
|
y2 = cy + ry * math.sin(f2); |
|
res = a2c(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]); |
|
} |
|
df = f2 - f1; |
|
var c1 = math.cos(f1), |
|
s1 = math.sin(f1), |
|
c2 = math.cos(f2), |
|
s2 = math.sin(f2), |
|
t = math.tan(df / 4), |
|
hx = 4 / 3 * rx * t, |
|
hy = 4 / 3 * ry * t, |
|
m1 = [x1, y1], |
|
m2 = [x1 + hx * s1, y1 - hy * c1], |
|
m3 = [x2 + hx * s2, y2 - hy * c2], |
|
m4 = [x2, y2]; |
|
m2[0] = 2 * m1[0] - m2[0]; |
|
m2[1] = 2 * m1[1] - m2[1]; |
|
if (recursive) { |
|
return [m2, m3, m4][concat](res); |
|
} else { |
|
res = [m2, m3, m4][concat](res).join()[split](","); |
|
var newres = []; |
|
for (var i = 0, ii = res.length; i < ii; i++) { |
|
newres[i] = i % 2 ? rotate(res[i - 1], res[i], rad).y : rotate(res[i], res[i + 1], rad).x; |
|
} |
|
return newres; |
|
} |
|
}, |
|
findDotAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) { |
|
var t1 = 1 - t; |
|
return { |
|
x: pow(t1, 3) * p1x + pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + pow(t, 3) * p2x, |
|
y: pow(t1, 3) * p1y + pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + pow(t, 3) * p2y |
|
}; |
|
}, |
|
curveDim = cacher(function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) { |
|
var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x), |
|
b = 2 * (c1x - p1x) - 2 * (c2x - c1x), |
|
c = p1x - c1x, |
|
t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a, |
|
t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a, |
|
y = [p1y, p2y], |
|
x = [p1x, p2x], |
|
dot; |
|
abs(t1) > "1e12" && (t1 = .5); |
|
abs(t2) > "1e12" && (t2 = .5); |
|
if (t1 > 0 && t1 < 1) { |
|
dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1); |
|
x.push(dot.x); |
|
y.push(dot.y); |
|
} |
|
if (t2 > 0 && t2 < 1) { |
|
dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2); |
|
x.push(dot.x); |
|
y.push(dot.y); |
|
} |
|
a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y); |
|
b = 2 * (c1y - p1y) - 2 * (c2y - c1y); |
|
c = p1y - c1y; |
|
t1 = (-b + math.sqrt(b * b - 4 * a * c)) / 2 / a; |
|
t2 = (-b - math.sqrt(b * b - 4 * a * c)) / 2 / a; |
|
abs(t1) > "1e12" && (t1 = .5); |
|
abs(t2) > "1e12" && (t2 = .5); |
|
if (t1 > 0 && t1 < 1) { |
|
dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1); |
|
x.push(dot.x); |
|
y.push(dot.y); |
|
} |
|
if (t2 > 0 && t2 < 1) { |
|
dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2); |
|
x.push(dot.x); |
|
y.push(dot.y); |
|
} |
|
return { |
|
min: {x: mmin[apply](0, x), y: mmin[apply](0, y)}, |
|
max: {x: mmax[apply](0, x), y: mmax[apply](0, y)} |
|
}; |
|
}), |
|
path2curve = R._path2curve = cacher(function (path, path2) { |
|
var pth = !path2 && paths(path); |
|
if (!path2 && pth.curve) { |
|
return pathClone(pth.curve); |
|
} |
|
var p = pathToAbsolute(path), |
|
p2 = path2 && pathToAbsolute(path2), |
|
attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, |
|
attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null}, |
|
processPath = function (path, d, pcom) { |
|
var nx, ny, tq = {T:1, Q:1}; |
|
if (!path) { |
|
return ["C", d.x, d.y, d.x, d.y, d.x, d.y]; |
|
} |
|
!(path[0] in tq) && (d.qx = d.qy = null); |
|
switch (path[0]) { |
|
case "M": |
|
d.X = path[1]; |
|
d.Y = path[2]; |
|
break; |
|
case "A": |
|
path = ["C"][concat](a2c[apply](0, [d.x, d.y][concat](path.slice(1)))); |
|
break; |
|
case "S": |
|
if (pcom == "C" || pcom == "S") { // In "S" case we have to take into account, if the previous command is C/S. |
|
nx = d.x * 2 - d.bx; // And reflect the previous |
|
ny = d.y * 2 - d.by; // command's control point relative to the current point. |
|
} |
|
else { // or some else or nothing |
|
nx = d.x; |
|
ny = d.y; |
|
} |
|
path = ["C", nx, ny][concat](path.slice(1)); |
|
break; |
|
case "T": |
|
if (pcom == "Q" || pcom == "T") { // In "T" case we have to take into account, if the previous command is Q/T. |
|
d.qx = d.x * 2 - d.qx; // And make a reflection similar |
|
d.qy = d.y * 2 - d.qy; // to case "S". |
|
} |
|
else { // or something else or nothing |
|
d.qx = d.x; |
|
d.qy = d.y; |
|
} |
|
path = ["C"][concat](q2c(d.x, d.y, d.qx, d.qy, path[1], path[2])); |
|
break; |
|
case "Q": |
|
d.qx = path[1]; |
|
d.qy = path[2]; |
|
path = ["C"][concat](q2c(d.x, d.y, path[1], path[2], path[3], path[4])); |
|
break; |
|
case "L": |
|
path = ["C"][concat](l2c(d.x, d.y, path[1], path[2])); |
|
break; |
|
case "H": |
|
path = ["C"][concat](l2c(d.x, d.y, path[1], d.y)); |
|
break; |
|
case "V": |
|
path = ["C"][concat](l2c(d.x, d.y, d.x, path[1])); |
|
break; |
|
case "Z": |
|
path = ["C"][concat](l2c(d.x, d.y, d.X, d.Y)); |
|
break; |
|
} |
|
return path; |
|
}, |
|
fixArc = function (pp, i) { |
|
if (pp[i].length > 7) { |
|
pp[i].shift(); |
|
var pi = pp[i]; |
|
while (pi.length) { |
|
pcoms1[i]="A"; // if created multiple C:s, their original seg is saved |
|
p2 && (pcoms2[i]="A"); // the same as above |
|
pp.splice(i++, 0, ["C"][concat](pi.splice(0, 6))); |
|
} |
|
pp.splice(i, 1); |
|
ii = mmax(p.length, p2 && p2.length || 0); |
|
} |
|
}, |
|
fixM = function (path1, path2, a1, a2, i) { |
|
if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") { |
|
path2.splice(i, 0, ["M", a2.x, a2.y]); |
|
a1.bx = 0; |
|
a1.by = 0; |
|
a1.x = path1[i][1]; |
|
a1.y = path1[i][2]; |
|
ii = mmax(p.length, p2 && p2.length || 0); |
|
} |
|
}, |
|
pcoms1 = [], // path commands of original path p |
|
pcoms2 = [], // path commands of original path p2 |
|
pfirst = "", // temporary holder for original path command |
|
pcom = ""; // holder for previous path command of original path |
|
for (var i = 0, ii = mmax(p.length, p2 && p2.length || 0); i < ii; i++) { |
|
p[i] && (pfirst = p[i][0]); // save current path command |
|
|
|
if (pfirst != "C") // C is not saved yet, because it may be result of conversion |
|
{ |
|
pcoms1[i] = pfirst; // Save current path command |
|
i && ( pcom = pcoms1[i-1]); // Get previous path command pcom |
|
} |
|
p[i] = processPath(p[i], attrs, pcom); // Previous path command is inputted to processPath |
|
|
|
if (pcoms1[i] != "A" && pfirst == "C") pcoms1[i] = "C"; // A is the only command |
|
// which may produce multiple C:s |
|
// so we have to make sure that C is also C in original path |
|
|
|
fixArc(p, i); // fixArc adds also the right amount of A:s to pcoms1 |
|
|
|
if (p2) { // the same procedures is done to p2 |
|
p2[i] && (pfirst = p2[i][0]); |
|
if (pfirst != "C") |
|
{ |
|
pcoms2[i] = pfirst; |
|
i && (pcom = pcoms2[i-1]); |
|
} |
|
p2[i] = processPath(p2[i], attrs2, pcom); |
|
|
|
if (pcoms2[i]!="A" && pfirst=="C") pcoms2[i]="C"; |
|
|
|
fixArc(p2, i); |
|
} |
|
fixM(p, p2, attrs, attrs2, i); |
|
fixM(p2, p, attrs2, attrs, i); |
|
var seg = p[i], |
|
seg2 = p2 && p2[i], |
|
seglen = seg.length, |
|
seg2len = p2 && seg2.length; |
|
attrs.x = seg[seglen - 2]; |
|
attrs.y = seg[seglen - 1]; |
|
attrs.bx = toFloat(seg[seglen - 4]) || attrs.x; |
|
attrs.by = toFloat(seg[seglen - 3]) || attrs.y; |
|
attrs2.bx = p2 && (toFloat(seg2[seg2len - 4]) || attrs2.x); |
|
attrs2.by = p2 && (toFloat(seg2[seg2len - 3]) || attrs2.y); |
|
attrs2.x = p2 && seg2[seg2len - 2]; |
|
attrs2.y = p2 && seg2[seg2len - 1]; |
|
} |
|
if (!p2) { |
|
pth.curve = pathClone(p); |
|
} |
|
return p2 ? [p, p2] : p; |
|
}, null, pathClone), |
|
parseDots = R._parseDots = cacher(function (gradient) { |
|
var dots = []; |
|
for (var i = 0, ii = gradient.length; i < ii; i++) { |
|
var dot = {}, |
|
par = gradient[i].match(/^([^:]*):?([\d\.]*)/); |
|
dot.color = R.getRGB(par[1]); |
|
if (dot.color.error) { |
|
return null; |
|
} |
|
dot.opacity = dot.color.opacity; |
|
dot.color = dot.color.hex; |
|
par[2] && (dot.offset = par[2] + "%"); |
|
dots.push(dot); |
|
} |
|
for (i = 1, ii = dots.length - 1; i < ii; i++) { |
|
if (!dots[i].offset) { |
|
var start = toFloat(dots[i - 1].offset || 0), |
|
end = 0; |
|
for (var j = i + 1; j < ii; j++) { |
|
if (dots[j].offset) { |
|
end = dots[j].offset; |
|
break; |
|
} |
|
} |
|
if (!end) { |
|
end = 100; |
|
j = ii; |
|
} |
|
end = toFloat(end); |
|
var d = (end - start) / (j - i + 1); |
|
for (; i < j; i++) { |
|
start += d; |
|
dots[i].offset = start + "%"; |
|
} |
|
} |
|
} |
|
return dots; |
|
}), |
|
tear = R._tear = function (el, paper) { |
|
el == paper.top && (paper.top = el.prev); |
|
el == paper.bottom && (paper.bottom = el.next); |
|
el.next && (el.next.prev = el.prev); |
|
el.prev && (el.prev.next = el.next); |
|
}, |
|
tofront = R._tofront = function (el, paper) { |
|
if (paper.top === el) { |
|
return; |
|
} |
|
tear(el, paper); |
|
el.next = null; |
|
el.prev = paper.top; |
|
paper.top.next = el; |
|
paper.top = el; |
|
}, |
|
toback = R._toback = function (el, paper) { |
|
if (paper.bottom === el) { |
|
return; |
|
} |
|
tear(el, paper); |
|
el.next = paper.bottom; |
|
el.prev = null; |
|
paper.bottom.prev = el; |
|
paper.bottom = el; |
|
}, |
|
insertafter = R._insertafter = function (el, el2, paper) { |
|
tear(el, paper); |
|
el2 == paper.top && (paper.top = el); |
|
el2.next && (el2.next.prev = el); |
|
el.next = el2.next; |
|
el.prev = el2; |
|
el2.next = el; |
|
}, |
|
insertbefore = R._insertbefore = function (el, el2, paper) { |
|
tear(el, paper); |
|
el2 == paper.bottom && (paper.bottom = el); |
|
el2.prev && (el2.prev.next = el); |
|
el.prev = el2.prev; |
|
el2.prev = el; |
|
el.next = el2; |
|
}, |
|
/*\ |
|
* Raphael.toMatrix |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Returns matrix of transformations applied to a given path |
|
> Parameters |
|
- path (string) path string |
|
- transform (string|array) transformation string |
|
= (object) @Matrix |
|
\*/ |
|
toMatrix = R.toMatrix = function (path, transform) { |
|
var bb = pathDimensions(path), |
|
el = { |
|
_: { |
|
transform: E |
|
}, |
|
getBBox: function () { |
|
return bb; |
|
} |
|
}; |
|
extractTransform(el, transform); |
|
return el.matrix; |
|
}, |
|
/*\ |
|
* Raphael.transformPath |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Returns path transformed by a given transformation |
|
> Parameters |
|
- path (string) path string |
|
- transform (string|array) transformation string |
|
= (string) path |
|
\*/ |
|
transformPath = R.transformPath = function (path, transform) { |
|
return mapPath(path, toMatrix(path, transform)); |
|
}, |
|
extractTransform = R._extractTransform = function (el, tstr) { |
|
if (tstr == null) { |
|
return el._.transform; |
|
} |
|
tstr = Str(tstr).replace(/\.{3}|\u2026/g, el._.transform || E); |
|
var tdata = R.parseTransformString(tstr), |
|
deg = 0, |
|
dx = 0, |
|
dy = 0, |
|
sx = 1, |
|
sy = 1, |
|
_ = el._, |
|
m = new Matrix; |
|
_.transform = tdata || []; |
|
if (tdata) { |
|
for (var i = 0, ii = tdata.length; i < ii; i++) { |
|
var t = tdata[i], |
|
tlen = t.length, |
|
command = Str(t[0]).toLowerCase(), |
|
absolute = t[0] != command, |
|
inver = absolute ? m.invert() : 0, |
|
x1, |
|
y1, |
|
x2, |
|
y2, |
|
bb; |
|
if (command == "t" && tlen == 3) { |
|
if (absolute) { |
|
x1 = inver.x(0, 0); |
|
y1 = inver.y(0, 0); |
|
x2 = inver.x(t[1], t[2]); |
|
y2 = inver.y(t[1], t[2]); |
|
m.translate(x2 - x1, y2 - y1); |
|
} else { |
|
m.translate(t[1], t[2]); |
|
} |
|
} else if (command == "r") { |
|
if (tlen == 2) { |
|
bb = bb || el.getBBox(1); |
|
m.rotate(t[1], bb.x + bb.width / 2, bb.y + bb.height / 2); |
|
deg += t[1]; |
|
} else if (tlen == 4) { |
|
if (absolute) { |
|
x2 = inver.x(t[2], t[3]); |
|
y2 = inver.y(t[2], t[3]); |
|
m.rotate(t[1], x2, y2); |
|
} else { |
|
m.rotate(t[1], t[2], t[3]); |
|
} |
|
deg += t[1]; |
|
} |
|
} else if (command == "s") { |
|
if (tlen == 2 || tlen == 3) { |
|
bb = bb || el.getBBox(1); |
|
m.scale(t[1], t[tlen - 1], bb.x + bb.width / 2, bb.y + bb.height / 2); |
|
sx *= t[1]; |
|
sy *= t[tlen - 1]; |
|
} else if (tlen == 5) { |
|
if (absolute) { |
|
x2 = inver.x(t[3], t[4]); |
|
y2 = inver.y(t[3], t[4]); |
|
m.scale(t[1], t[2], x2, y2); |
|
} else { |
|
m.scale(t[1], t[2], t[3], t[4]); |
|
} |
|
sx *= t[1]; |
|
sy *= t[2]; |
|
} |
|
} else if (command == "m" && tlen == 7) { |
|
m.add(t[1], t[2], t[3], t[4], t[5], t[6]); |
|
} |
|
_.dirtyT = 1; |
|
el.matrix = m; |
|
} |
|
} |
|
|
|
/*\ |
|
* Element.matrix |
|
[ property (object) ] |
|
** |
|
* Keeps @Matrix object, which represents element transformation |
|
\*/ |
|
el.matrix = m; |
|
|
|
_.sx = sx; |
|
_.sy = sy; |
|
_.deg = deg; |
|
_.dx = dx = m.e; |
|
_.dy = dy = m.f; |
|
|
|
if (sx == 1 && sy == 1 && !deg && _.bbox) { |
|
_.bbox.x += +dx; |
|
_.bbox.y += +dy; |
|
} else { |
|
_.dirtyT = 1; |
|
} |
|
}, |
|
getEmpty = function (item) { |
|
var l = item[0]; |
|
switch (l.toLowerCase()) { |
|
case "t": return [l, 0, 0]; |
|
case "m": return [l, 1, 0, 0, 1, 0, 0]; |
|
case "r": if (item.length == 4) { |
|
return [l, 0, item[2], item[3]]; |
|
} else { |
|
return [l, 0]; |
|
} |
|
case "s": if (item.length == 5) { |
|
return [l, 1, 1, item[3], item[4]]; |
|
} else if (item.length == 3) { |
|
return [l, 1, 1]; |
|
} else { |
|
return [l, 1]; |
|
} |
|
} |
|
}, |
|
equaliseTransform = R._equaliseTransform = function (t1, t2) { |
|
t2 = Str(t2).replace(/\.{3}|\u2026/g, t1); |
|
t1 = R.parseTransformString(t1) || []; |
|
t2 = R.parseTransformString(t2) || []; |
|
var maxlength = mmax(t1.length, t2.length), |
|
from = [], |
|
to = [], |
|
i = 0, j, jj, |
|
tt1, tt2; |
|
for (; i < maxlength; i++) { |
|
tt1 = t1[i] || getEmpty(t2[i]); |
|
tt2 = t2[i] || getEmpty(tt1); |
|
if ((tt1[0] != tt2[0]) || |
|
(tt1[0].toLowerCase() == "r" && (tt1[2] != tt2[2] || tt1[3] != tt2[3])) || |
|
(tt1[0].toLowerCase() == "s" && (tt1[3] != tt2[3] || tt1[4] != tt2[4])) |
|
) { |
|
return; |
|
} |
|
from[i] = []; |
|
to[i] = []; |
|
for (j = 0, jj = mmax(tt1.length, tt2.length); j < jj; j++) { |
|
j in tt1 && (from[i][j] = tt1[j]); |
|
j in tt2 && (to[i][j] = tt2[j]); |
|
} |
|
} |
|
return { |
|
from: from, |
|
to: to |
|
}; |
|
}; |
|
R._getContainer = function (x, y, w, h) { |
|
var container; |
|
container = h == null && !R.is(x, "object") ? g.doc.getElementById(x) : x; |
|
if (container == null) { |
|
return; |
|
} |
|
if (container.tagName) { |
|
if (y == null) { |
|
return { |
|
container: container, |
|
width: container.style.pixelWidth || container.offsetWidth, |
|
height: container.style.pixelHeight || container.offsetHeight |
|
}; |
|
} else { |
|
return { |
|
container: container, |
|
width: y, |
|
height: w |
|
}; |
|
} |
|
} |
|
return { |
|
container: 1, |
|
x: x, |
|
y: y, |
|
width: w, |
|
height: h |
|
}; |
|
}; |
|
/*\ |
|
* Raphael.pathToRelative |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Converts path to relative form |
|
> Parameters |
|
- pathString (string|array) path string or array of segments |
|
= (array) array of segments. |
|
\*/ |
|
R.pathToRelative = pathToRelative; |
|
R._engine = {}; |
|
/*\ |
|
* Raphael.path2curve |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Converts path to a new path where all segments are cubic bezier curves. |
|
> Parameters |
|
- pathString (string|array) path string or array of segments |
|
= (array) array of segments. |
|
\*/ |
|
R.path2curve = path2curve; |
|
/*\ |
|
* Raphael.matrix |
|
[ method ] |
|
** |
|
* Utility method |
|
** |
|
* Returns matrix based on given parameters. |
|
> Parameters |
|
- a (number) |
|
- b (number) |
|
- c (number) |
|
- d (number) |
|
- e (number) |
|
- f (number) |
|
= (object) @Matrix |
|
\*/ |
|
R.matrix = function (a, b, c, d, e, f) { |
|
return new Matrix(a, b, c, d, e, f); |
|
}; |
|
function Matrix(a, b, c, d, e, f) { |
|
if (a != null) { |
|
this.a = +a; |
|
this.b = +b; |
|
this.c = +c; |
|
this.d = +d; |
|
this.e = +e; |
|
this.f = +f; |
|
} else { |
|
this.a = 1; |
|
this.b = 0; |
|
this.c = 0; |
|
this.d = 1; |
|
this.e = 0; |
|
this.f = 0; |
|
} |
|
} |
|
(function (matrixproto) { |
|
/*\ |
|
* Matrix.add |
|
[ method ] |
|
** |
|
* Adds given matrix to existing one. |
|
> Parameters |
|
- a (number) |
|
- b (number) |
|
- c (number) |
|
- d (number) |
|
- e (number) |
|
- f (number) |
|
or |
|
- matrix (object) @Matrix |
|
\*/ |
|
matrixproto.add = function (a, b, c, d, e, f) { |
|
var out = [[], [], []], |
|
m = [[this.a, this.c, this.e], [this.b, this.d, this.f], [0, 0, 1]], |
|
matrix = [[a, c, e], [b, d, f], [0, 0, 1]], |
|
x, y, z, res; |
|
|
|
if (a && a instanceof Matrix) { |
|
matrix = [[a.a, a.c, a.e], [a.b, a.d, a.f], [0, 0, 1]]; |
|
} |
|
|
|
for (x = 0; x < 3; x++) { |
|
for (y = 0; y < 3; y++) { |
|
res = 0; |
|
for (z = 0; z < 3; z++) { |
|
res += m[x][z] * matrix[z][y]; |
|
} |
|
out[x][y] = res; |
|
} |
|
} |
|
this.a = out[0][0]; |
|
this.b = out[1][0]; |
|
this.c = out[0][1]; |
|
this.d = out[1][1]; |
|
this.e = out[0][2]; |
|
this.f = out[1][2]; |
|
}; |
|
/*\ |
|
* Matrix.invert |
|
[ method ] |
|
** |
|
* Returns inverted version of the matrix |
|
= (object) @Matrix |
|
\*/ |
|
matrixproto.invert = function () { |
|
var me = this, |
|
x = me.a * me.d - me.b * me.c; |
|
return new Matrix(me.d / x, -me.b / x, -me.c / x, me.a / x, (me.c * me.f - me.d * me.e) / x, (me.b * me.e - me.a * me.f) / x); |
|
}; |
|
/*\ |
|
* Matrix.clone |
|
[ method ] |
|
** |
|
* Returns copy of the matrix |
|
= (object) @Matrix |
|
\*/ |
|
matrixproto.clone = function () { |
|
return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f); |
|
}; |
|
/*\ |
|
* Matrix.translate |
|
[ method ] |
|
** |
|
* Translate the matrix |
|
> Parameters |
|
- x (number) |
|
- y (number) |
|
\*/ |
|
matrixproto.translate = function (x, y) { |
|
this.add(1, 0, 0, 1, x, y); |
|
}; |
|
/*\ |
|
* Matrix.scale |
|
[ method ] |
|
** |
|
* Scales the matrix |
|
> Parameters |
|
- x (number) |
|
- y (number) #optional |
|
- cx (number) #optional |
|
- cy (number) #optional |
|
\*/ |
|
matrixproto.scale = function (x, y, cx, cy) { |
|
y == null && (y = x); |
|
(cx || cy) && this.add(1, 0, 0, 1, cx, cy); |
|
this.add(x, 0, 0, y, 0, 0); |
|
(cx || cy) && this.add(1, 0, 0, 1, -cx, -cy); |
|
}; |
|
/*\ |
|
* Matrix.rotate |
|
[ method ] |
|
** |
|
* Rotates the matrix |
|
> Parameters |
|
- a (number) |
|
- x (number) |
|
- y (number) |
|
\*/ |
|
matrixproto.rotate = function (a, x, y) { |
|
a = R.rad(a); |
|
x = x || 0; |
|
y = y || 0; |
|
var cos = +math.cos(a).toFixed(9), |
|
sin = +math.sin(a).toFixed(9); |
|
this.add(cos, sin, -sin, cos, x, y); |
|
this.add(1, 0, 0, 1, -x, -y); |
|
}; |
|
/*\ |
|
* Matrix.x |
|
[ method ] |
|
** |
|
* Return x coordinate for given point after transformation described by the matrix. See also @Matrix.y |
|
> Parameters |
|
- x (number) |
|
- y (number) |
|
= (number) x |
|
\*/ |
|
matrixproto.x = function (x, y) { |
|
return x * this.a + y * this.c + this.e; |
|
}; |
|
/*\ |
|
* Matrix.y |
|
[ method ] |
|
** |
|
* Return y coordinate for given point after transformation described by the matrix. See also @Matrix.x |
|
> Parameters |
|
- x (number) |
|
- y (number) |
|
= (number) y |
|
\*/ |
|
matrixproto.y = function (x, y) { |
|
return x * this.b + y * this.d + this.f; |
|
}; |
|
matrixproto.get = function (i) { |
|
return +this[Str.fromCharCode(97 + i)].toFixed(4); |
|
}; |
|
matrixproto.toString = function () { |
|
return R.svg ? |
|
"matrix(" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)].join() + ")" : |
|
[this.get(0), this.get(2), this.get(1), this.get(3), 0, 0].join(); |
|
}; |
|
matrixproto.toFilter = function () { |
|
return "progid:DXImageTransform.Microsoft.Matrix(M11=" + this.get(0) + |
|
", M12=" + this.get(2) + ", M21=" + this.get(1) + ", M22=" + this.get(3) + |
|
", Dx=" + this.get(4) + ", Dy=" + this.get(5) + ", sizingmethod='auto expand')"; |
|
}; |
|
matrixproto.offset = function () { |
|
return [this.e.toFixed(4), this.f.toFixed(4)]; |
|
}; |
|
function norm(a) { |
|
return a[0] * a[0] + a[1] * a[1]; |
|
} |
|
function normalize(a) { |
|
var mag = math.sqrt(norm(a)); |
|
a[0] && (a[0] /= mag); |
|
a[1] && (a[1] /= mag); |
|
} |
|
/*\ |
|
* Matrix.split |
|
[ method ] |
|
** |
|
* Splits matrix into primitive transformations |
|
= (object) in format: |
|
o dx (number) translation by x |
|
o dy (number) translation by y |
|
o scalex (number) scale by x |
|
o scaley (number) scale by y |
|
o shear (number) shear |
|
o rotate (number) rotation in deg |
|
o isSimple (boolean) could it be represented via simple transformations |
|
\*/ |
|
matrixproto.split = function () { |
|
var out = {}; |
|
// translation |
|
out.dx = this.e; |
|
out.dy = this.f; |
|
|
|
// scale and shear |
|
var row = [[this.a, this.c], [this.b, this.d]]; |
|
out.scalex = math.sqrt(norm(row[0])); |
|
normalize(row[0]); |
|
|
|
out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1]; |
|
row[1] = [row[1][0] - row[0][0] * out.shear, row[1][1] - row[0][1] * out.shear]; |
|
|
|
out.scaley = math.sqrt(norm(row[1])); |
|
normalize(row[1]); |
|
out.shear /= out.scaley; |
|
|
|
// rotation |
|
var sin = -row[0][1], |
|
cos = row[1][1]; |
|
if (cos < 0) { |
|
out.rotate = R.deg(math.acos(cos)); |
|
if (sin < 0) { |
|
out.rotate = 360 - out.rotate; |
|
} |
|
} else { |
|
out.rotate = R.deg(math.asin(sin)); |
|
} |
|
|
|
out.isSimple = !+out.shear.toFixed(9) && (out.scalex.toFixed(9) == out.scaley.toFixed(9) || !out.rotate); |
|
out.isSuperSimple = !+out.shear.toFixed(9) && out.scalex.toFixed(9) == out.scaley.toFixed(9) && !out.rotate; |
|
out.noRotation = !+out.shear.toFixed(9) && !out.rotate; |
|
return out; |
|
}; |
|
/*\ |
|
* Matrix.toTransformString |
|
[ method ] |
|
** |
|
* Return transform string that represents given matrix |
|
= (string) transform string |
|
\*/ |
|
matrixproto.toTransformString = function (shorter) { |
|
var s = shorter || this[split](); |
|
if (s.isSimple) { |
|
s.scalex = +s.scalex.toFixed(4); |
|
s.scaley = +s.scaley.toFixed(4); |
|
s.rotate = +s.rotate.toFixed(4); |
|
return (s.dx || s.dy ? "t" + [s.dx, s.dy] : E) + |
|
(s.scalex != 1 || s.scaley != 1 ? "s" + [s.scalex, s.scaley, 0, 0] : E) + |
|
(s.rotate ? "r" + [s.rotate, 0, 0] : E); |
|
} else { |
|
return "m" + [this.get(0), this.get(1), this.get(2), this.get(3), this.get(4), this.get(5)]; |
|
} |
|
}; |
|
})(Matrix.prototype); |
|
|
|
var preventDefault = function () { |
|
this.returnValue = false; |
|
}, |
|
preventTouch = function () { |
|
return this.originalEvent.preventDefault(); |
|
}, |
|
stopPropagation = function () { |
|
this.cancelBubble = true; |
|
}, |
|
stopTouch = function () { |
|
return this.originalEvent.stopPropagation(); |
|
}, |
|
getEventPosition = function (e) { |
|
var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop, |
|
scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft; |
|
|
|
return { |
|
x: e.clientX + scrollX, |
|
y: e.clientY + scrollY |
|
}; |
|
}, |
|
addEvent = (function () { |
|
if (g.doc.addEventListener) { |
|
return function (obj, type, fn, element) { |
|
var f = function (e) { |
|
var pos = getEventPosition(e); |
|
return fn.call(element, e, pos.x, pos.y); |
|
}; |
|
obj.addEventListener(type, f, false); |
|
|
|
if (supportsTouch && touchMap[type]) { |
|
var _f = function (e) { |
|
var pos = getEventPosition(e), |
|
olde = e; |
|
|
|
for (var i = 0, ii = e.targetTouches && e.targetTouches.length; i < ii; i++) { |
|
if (e.targetTouches[i].target == obj) { |
|
e = e.targetTouches[i]; |
|
e.originalEvent = olde; |
|
e.preventDefault = preventTouch; |
|
e.stopPropagation = stopTouch; |
|
break; |
|
} |
|
} |
|
|
|
return fn.call(element, e, pos.x, pos.y); |
|
}; |
|
obj.addEventListener(touchMap[type], _f, false); |
|
} |
|
|
|
return function () { |
|
obj.removeEventListener(type, f, false); |
|
|
|
if (supportsTouch && touchMap[type]) |
|
obj.removeEventListener(touchMap[type], _f, false); |
|
|
|
return true; |
|
}; |
|
}; |
|
} else if (g.doc.attachEvent) { |
|
return function (obj, type, fn, element) { |
|
var f = function (e) { |
|
e = e || g.win.event; |
|
var scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop, |
|
scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft, |
|
x = e.clientX + scrollX, |
|
y = e.clientY + scrollY; |
|
e.preventDefault = e.preventDefault || preventDefault; |
|
e.stopPropagation = e.stopPropagation || stopPropagation; |
|
return fn.call(element, e, x, y); |
|
}; |
|
obj.attachEvent("on" + type, f); |
|
var detacher = function () { |
|
obj.detachEvent("on" + type, f); |
|
return true; |
|
}; |
|
return detacher; |
|
}; |
|
} |
|
})(), |
|
drag = [], |
|
dragMove = function (e) { |
|
var x = e.clientX, |
|
y = e.clientY, |
|
scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop, |
|
scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft, |
|
dragi, |
|
j = drag.length; |
|
while (j--) { |
|
dragi = drag[j]; |
|
if (supportsTouch && e.touches) { |
|
var i = e.touches.length, |
|
touch; |
|
while (i--) { |
|
touch = e.touches[i]; |
|
if (touch.identifier == dragi.el._drag.id) { |
|
x = touch.clientX; |
|
y = touch.clientY; |
|
(e.originalEvent ? e.originalEvent : e).preventDefault(); |
|
break; |
|
} |
|
} |
|
} else { |
|
e.preventDefault(); |
|
} |
|
var node = dragi.el.node, |
|
o, |
|
next = node.nextSibling, |
|
parent = node.parentNode, |
|
display = node.style.display; |
|
g.win.opera && parent.removeChild(node); |
|
node.style.display = "none"; |
|
o = dragi.el.paper.getElementByPoint(x, y); |
|
node.style.display = display; |
|
g.win.opera && (next ? parent.insertBefore(node, next) : parent.appendChild(node)); |
|
o && eve("raphael.drag.over." + dragi.el.id, dragi.el, o); |
|
x += scrollX; |
|
y += scrollY; |
|
eve("raphael.drag.move." + dragi.el.id, dragi.move_scope || dragi.el, x - dragi.el._drag.x, y - dragi.el._drag.y, x, y, e); |
|
} |
|
}, |
|
dragUp = function (e) { |
|
R.unmousemove(dragMove).unmouseup(dragUp); |
|
var i = drag.length, |
|
dragi; |
|
while (i--) { |
|
dragi = drag[i]; |
|
dragi.el._drag = {}; |
|
eve("raphael.drag.end." + dragi.el.id, dragi.end_scope || dragi.start_scope || dragi.move_scope || dragi.el, e); |
|
} |
|
drag = []; |
|
}, |
|
/*\ |
|
* Raphael.el |
|
[ property (object) ] |
|
** |
|
* You can add your own method to elements. This is usefull when you want to hack default functionality or |
|
* want to wrap some common transformation or attributes in one method. In difference to canvas methods, |
|
* you can redefine element method at any time. Expending element methods wouldn’t affect set. |
|
> Usage |
|
| Raphael.el.red = function () { |
|
| this.attr({fill: "#f00"}); |
|
| }; |
|
| // then use it |
|
| paper.circle(100, 100, 20).red(); |
|
\*/ |
|
elproto = R.el = {}; |
|
/*\ |
|
* Element.click |
|
[ method ] |
|
** |
|
* Adds event handler for click for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.unclick |
|
[ method ] |
|
** |
|
* Removes event handler for click for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.dblclick |
|
[ method ] |
|
** |
|
* Adds event handler for double click for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.undblclick |
|
[ method ] |
|
** |
|
* Removes event handler for double click for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.mousedown |
|
[ method ] |
|
** |
|
* Adds event handler for mousedown for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.unmousedown |
|
[ method ] |
|
** |
|
* Removes event handler for mousedown for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.mousemove |
|
[ method ] |
|
** |
|
* Adds event handler for mousemove for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.unmousemove |
|
[ method ] |
|
** |
|
* Removes event handler for mousemove for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.mouseout |
|
[ method ] |
|
** |
|
* Adds event handler for mouseout for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.unmouseout |
|
[ method ] |
|
** |
|
* Removes event handler for mouseout for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.mouseover |
|
[ method ] |
|
** |
|
* Adds event handler for mouseover for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.unmouseover |
|
[ method ] |
|
** |
|
* Removes event handler for mouseover for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.mouseup |
|
[ method ] |
|
** |
|
* Adds event handler for mouseup for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.unmouseup |
|
[ method ] |
|
** |
|
* Removes event handler for mouseup for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.touchstart |
|
[ method ] |
|
** |
|
* Adds event handler for touchstart for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.untouchstart |
|
[ method ] |
|
** |
|
* Removes event handler for touchstart for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.touchmove |
|
[ method ] |
|
** |
|
* Adds event handler for touchmove for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.untouchmove |
|
[ method ] |
|
** |
|
* Removes event handler for touchmove for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.touchend |
|
[ method ] |
|
** |
|
* Adds event handler for touchend for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.untouchend |
|
[ method ] |
|
** |
|
* Removes event handler for touchend for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
|
|
/*\ |
|
* Element.touchcancel |
|
[ method ] |
|
** |
|
* Adds event handler for touchcancel for the element. |
|
> Parameters |
|
- handler (function) handler for the event |
|
= (object) @Element |
|
\*/ |
|
/*\ |
|
* Element.untouchcancel |
|
[ method ] |
|
** |
|
* Removes event handler for touchcancel for the element. |
|
> Parameters |
|
- handler (function) #optional handler for the event |
|
= (object) @Element |
|
\*/ |
|
for (var i = events.length; i--;) { |
|
(function (eventName) { |
|
R[eventName] = elproto[eventName] = function (fn, scope) { |
|
if (R.is(fn, "function")) { |
|
this.events = this.events || []; |
|
this.events.push({name: eventName, f: fn, unbind: addEvent(this.shape || this.node || g.doc, eventName, fn, scope || this)}); |
|
} |
|
return this; |
|
}; |
|
R["un" + eventName] = elproto["un" + eventName] = function (fn) { |
|
var events = this.events || [], |
|
l = events.length; |
|
while (l--){ |
|
if (events[l].name == eventName && (R.is(fn, "undefined") || events[l].f == fn)) { |
|
events[l].unbind(); |
|
events.splice(l, 1); |
|
!events.length && delete this.events; |
|
} |
|
} |
|
return this; |
|
}; |
|
})(events[i]); |
|
} |
|
|
|
/*\ |
|
* Element.data |
|
[ method ] |
|
** |
|
* Adds or retrieves given value asociated with given key. |
|
** |
|
* See also @Element.removeData |
|
> Parameters |
|
- key (string) key to store data |
|
- value (any) #optional value to store |
|
= (object) @Element |
|
* or, if value is not specified: |
|
= (any) value |
|
* or, if key and value are not specified: |
|
= (object) Key/value pairs for all the data associated with the element. |
|
> Usage |
|
| for (var i = 0, i < 5, i++) { |
|
| paper.circle(10 + 15 * i, 10, 10) |
|
| .attr({fill: "#000"}) |
|
| .data("i", i) |
|
| .click(function () { |
|
| alert(this.data("i")); |
|
| }); |
|
| } |
|
\*/ |
|
elproto.data = function (key, value) { |
|
var data = eldata[this.id] = eldata[this.id] || {}; |
|
if (arguments.length == 0) { |
|
return data; |
|
} |
|
if (arguments.length == 1) { |
|
if (R.is(key, "object")) { |
|
for (var i in key) if (key[has](i)) { |
|
this.data(i, key[i]); |
|
} |
|
return this; |
|
} |
|
eve("raphael.data.get." + this.id, this, data[key], key); |
|
return data[key]; |
|
} |
|
data[key] = value; |
|
eve("raphael.data.set." + this.id, this, value, key); |
|
return this; |
|
}; |
|
|
|
elproto.datum = function(){ |
|
return arguments.length == 0 ? this._bindData : ((this._bindData = arguments[0]), this); |
|
}; |
|
|
|
/*\ |
|
* Element.removeData |
|
[ method ] |
|
** |
|
* Removes value associated with an element by given key. |
|
* If key is not provided, removes all the data of the element. |
|
> Parameters |
|
- key (string) #optional key |
|
= (object) @Element |
|
\*/ |
|
elproto.removeData = function (key) { |
|
if (key == null) { |
|
eldata[this.id] = {}; |
|
} else { |
|
eldata[this.id] && delete eldata[this.id][key]; |
|
} |
|
return this; |
|
}; |
|
/*\ |
|
* Element.getData |
|
[ method ] |
|
** |
|
* Retrieves the element data |
|
= (object) data |
|
\*/ |
|
elproto.getData = function () { |
|
return clone(eldata[this.id] || {}); |
|
}; |
|
/*\ |
|
* Element.hover |
|
[ method ] |
|
** |
|
* Adds event handlers for hover for the element. |
|
> Parameters |
|
- f_in (function) handler for hover in |
|
- f_out (function) handler for hover out |
|
- icontext (object) #optional context for hover in handler |
|
- ocontext (object) #optional context for hover out handler |
|
= (object) @Element |
|
\*/ |
|
elproto.hover = function (f_in, f_out, scope_in, scope_out) { |
|
return this.mouseover(f_in, scope_in).mouseout(f_out, scope_out || scope_in); |
|
}; |
|
/*\ |
|
* Element.unhover |
|
[ method ] |
|
** |
|
* Removes event handlers for hover for the element. |
|
> Parameters |
|
- f_in (function) handler for hover in |
|
- f_out (function) handler for hover out |
|
= (object) @Element |
|
\*/ |
|
elproto.unhover = function (f_in, f_out) { |
|
return this.unmouseover(f_in).unmouseout(f_out); |
|
}; |
|
var draggable = []; |
|
/*\ |
|
* Element.drag |
|
[ method ] |
|
** |
|
* Adds event handlers for drag of the element. |
|
> Parameters |
|
- onmove (function) handler for moving |
|
- onstart (function) handler for drag start |
|
- onend (function) handler for drag end |
|
- mcontext (object) #optional context for moving handler |
|
- scontext (object) #optional context for drag start handler |
|
- econtext (object) #optional context for drag end handler |
|
* Additionaly following `drag` events will be triggered: `drag.start.<id>` on start, |
|
* `drag.end.<id>` on end and `drag.move.<id>` on every move. When element will be dragged over another element |
|
* `drag.over.<id>` will be fired as well. |
|
* |
|
* Start event and start handler will be called in specified context or in context of the element with following parameters: |
|
o x (number) x position of the mouse |
|
o y (number) y position of the mouse |
|
o event (object) DOM event object |
|
* Move event and move handler will be called in specified context or in context of the element with following parameters: |
|
o dx (number) shift by x from the start point |
|
o dy (number) shift by y from the start point |
|
o x (number) x position of the mouse |
|
o y (number) y position of the mouse |
|
o event (object) DOM event object |
|
* End event and end handler will be called in specified context or in context of the element with following parameters: |
|
o event (object) DOM event object |
|
= (object) @Element |
|
\*/ |
|
elproto.drag = function (onmove, onstart, onend, move_scope, start_scope, end_scope) { |
|
function start(e) { |
|
(e.originalEvent || e).preventDefault(); |
|
var x = e.clientX, |
|
y = e.clientY, |
|
scrollY = g.doc.documentElement.scrollTop || g.doc.body.scrollTop, |
|
scrollX = g.doc.documentElement.scrollLeft || g.doc.body.scrollLeft; |
|
this._drag.id = e.identifier; |
|
if (supportsTouch && e.touches) { |
|
var i = e.touches.length, touch; |
|
while (i--) { |
|
touch = e.touches[i]; |
|
this._drag.id = touch.identifier; |
|
if (touch.identifier == this._drag.id) { |
|
x = touch.clientX; |
|
y = touch.clientY; |
|
break; |
|
} |
|
} |
|
} |
|
this._drag.x = x + scrollX; |
|
this._drag.y = y + scrollY; |
|
!drag.length && R.mousemove(dragMove).mouseup(dragUp); |
|
drag.push({el: this, move_scope: move_scope, start_scope: start_scope, end_scope: end_scope}); |
|
onstart && eve.on("raphael.drag.start." + this.id, onstart); |
|
onmove && eve.on("raphael.drag.move." + this.id, onmove); |
|
onend && eve.on("raphael.drag.end." + this.id, onend); |
|
eve("raphael.drag.start." + this.id, start_scope || move_scope || this, e.clientX + scrollX, e.clientY + scrollY, e); |
|
} |
|
this._drag = {}; |
|
draggable.push({el: this, start: start}); |
|
this.mousedown(start); |
|
return this; |
|
}; |
|
/*\ |
|
* Element.onDragOver |
|
[ method ] |
|
** |
|
* Shortcut for assigning event handler for `drag.over.<id>` event, where id is id of the element (see @Element.id). |
|
> Parameters |
|
- f (function) handler for event, first argument would be the element you are dragging over |
|
\*/ |
|
elproto.onDragOver = function (f) { |
|
f ? eve.on("raphael.drag.over." + this.id, f) : eve.unbind("raphael.drag.over." + this.id); |
|
}; |
|
/*\ |
|
* Element.undrag |
|
[ method ] |
|
** |
|
* Removes all drag event handlers from given element. |
|
\*/ |
|
elproto.undrag = function () { |
|
var i = draggable.length; |
|
while (i--) if (draggable[i].el == this) { |
|
this.unmousedown(draggable[i].start); |
|
draggable.splice(i, 1); |
|
eve.unbind("raphael.drag.*." + this.id); |
|
} |
|
!draggable.length && R.unmousemove(dragMove).unmouseup(dragUp); |
|
drag = []; |
|
}; |
|
/*\ |
|
* Paper.circle |
|
[ method ] |
|
** |
|
* Draws a circle. |
|
** |
|
> Parameters |
|
** |
|
- x (number) x coordinate of the centre |
|
- y (number) y coordinate of the centre |
|
- r (number) radius |
|
= (object) Raphaël element object with type “circle” |
|
** |
|
> Usage |
|
| var c = paper.circle(50, 50, 40); |
|
\*/ |
|
paperproto.circle = function (x, y, r) { |
|
var out = R._engine.circle(this, x || 0, y || 0, r || 0); |
|
this.__set__ && this.__set__.push(out); |
|
return out; |
|
}; |
|
/*\ |
|
* Paper.rect |
|
[ method ] |
|
* |
|
* Draws a rectangle. |
|
** |
|
> Parameters |
|
** |
|
- x (number) x coordinate of the top left corner |
|
- y (number) y coordinate of the top left corner |
|
- width (number) width |
|
- height (number) height |
|
- r (number) #optional radius for rounded corners, default is 0 |
|
= (object) Raphaël element object with type “rect” |
|
** |
|
> Usage |
|
| // regular rectangle |
|
| var c = paper.rect(10, 10, 50, 50); |
|
| // rectangle with rounded corners |
|
| var c = paper.rect(40, 40, 50, 50, 10); |
|
\*/ |
|
paperproto.rect = function (x, y, w, h, r) { |
|
var out = R._engine.rect(this, x || 0, y || 0, w || 0, h || 0, r || 0); |
|
this.__set__ && this.__set__.push(out); |
|
return out; |
|
}; |
|
/*\ |
|
* Paper.ellipse |
|
[ method ] |
|
** |
|
* Draws an ellipse. |
|
** |
|
> Parameters |
|
** |
|
- x (number) x coordinate of the centre |
|
- y (number) y coordinate of the centre |
|
- rx (number) horizontal radius |
|
- ry (number) vertical radius |
|
= (object) Raphaël element object with type “ellipse” |
|
** |
|
> Usage |
|
| var c = paper.ellipse(50, 50, 40, 20); |
|
\*/ |
|
paperproto.ellipse = function (x, y, rx, ry) { |
|
var out = R._engine.ellipse(this, x || 0, y || 0, rx || 0, ry || 0); |
|
this.__set__ && this.__set__.push(out); |
|
return out; |
|
}; |
|
/*\ |
|
* Paper.path |
|
[ method ] |
|
** |
|
* Creates a path element by given path data string. |
|
> Parameters |
|
- pathString (string) #optional path string in SVG format. |
|
* Path string consists of one-letter commands, followed by comma seprarated arguments in numercal form. Example: |
|
| "M10,20L30,40" |
|
* Here we can see two commands: “M”, with arguments `(10, 20)` and “L” with arguments `(30, 40)`. Upper case letter mean command is absolute, lower case—relative. |
|
* |
|
# <p>Here is short list of commands available, for more details see <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path's data attribute's format are described in the SVG specification.">SVG path string format</a>.</p> |
|
# <table><thead><tr><th>Command</th><th>Name</th><th>Parameters</th></tr></thead><tbody> |
|
# <tr><td>M</td><td>moveto</td><td>(x y)+</td></tr> |
|
# <tr><td>Z</td><td>closepath</td><td>(none)</td></tr> |
|
# <tr><td>L</td><td>lineto</td><td>(x y)+</td></tr> |
|
# <tr><td>H</td><td>horizontal lineto</td><td>x+</td></tr> |
|
# <tr><td>V</td><td>vertical lineto</td><td>y+</td></tr> |
|
# <tr><td>C</td><td>curveto</td><td>(x1 y1 x2 y2 x y)+</td></tr> |
|
# <tr><td>S</td><td>smooth curveto</td><td>(x2 y2 x y)+</td></tr> |
|
# <tr><td>Q</td><td>quadratic Bézier curveto</td><td>(x1 y1 x y)+</td></tr> |
|
# <tr><td>T</td><td>smooth quadratic Bézier curveto</td><td>(x y)+</td></tr> |
|
# <tr><td>A</td><td>elliptical arc</td><td>(rx ry x-axis-rotation large-arc-flag sweep-flag x y)+</td></tr> |
|
# <tr><td>R</td><td><a href="http://en.wikipedia.org/wiki/Catmull–Rom_spline#Catmull.E2.80.93Rom_spline">Catmull-Rom curveto</a>*</td><td>x1 y1 (x y)+</td></tr></tbody></table> |
|
* * “Catmull-Rom curveto” is a not standard SVG command and added in 2.0 to make life easier. |
|
* Note: there is a special case when path consist of just three commands: “M10,10R…z”. In this case path will smoothly connects to its beginning. |
|
> Usage |
|
| var c = paper.path("M10 10L90 90"); |
|
| // draw a diagonal line: |
|
| // move to 10,10, line to 90,90 |
|
* For example of path strings, check out these icons: http://raphaeljs.com/icons/ |
|
\*/ |
|
paperproto.path = function (pathString) { |
|
pathString && !R.is(pathString, string) && !R.is(pathString[0], array) && (pathString += E); |
|
var out = R._engine.path(R.format[apply](R, arguments), this); |
|
this.__set__ && this.__set__.push(out); |
|
return out; |
|
}; |
|
/*\ |
|
* Paper.image |
|
[ method ] |
|
** |
|
* Embeds an image into the surface. |
|
** |
|
> Parameters |
|
** |
|
- src (string) URI of the source image |
|
- x (number) x coordinate position |
|
- y (number) y coordinate position |
|
- width (number) width of the image |
|
- height (number) height of the image |
|
= (object) Raphaël element object with type “image” |
|
** |
|
> Usage |
|
| var c = paper.image("apple.png", 10, 10, 80, 80); |
|
\*/ |
|
paperproto.image = function (src, x, y, w, h) { |
|
var out = R._engine.image(this, src || "about:blank", x || 0, y || 0, w || 0, h || 0); |
|
this.__set__ && this.__set__.push(out); |
|
return out; |
|
}; |
|
/*\ |
|
* Paper.text |
|
[ method ] |
|
** |
|
* Draws a text string. If you need line breaks, put “\n” in the string. |
|
** |
|
> Parameters |
|
** |
|
- x (number) x coordinate position |
|
- y (number) y coordinate position |
|
- text (string) The text string to draw |
|
= (object) Raphaël element object with type “text” |
|
** |
|
> Usage |
|
| var t = paper.text(50, 50, "Raphaël\nkicks\nbutt!"); |
|
\*/ |
|
paperproto.text = function (x, y, text) { |
|
var out = R._engine.text(this, x || 0, y || 0, Str(text)); |
|
this.__set__ && this.__set__.push(out); |
|
return out; |
|
}; |
|
/*\ |
|
* Paper.set |
|
[ method ] |
|
** |
|
* Creates array-like object to keep and operate several elements at once. |
|
* Warning: it doesn’t create any elements for itself in the page, it just groups existing elements. |
|
* Sets act as pseudo elements — all methods available to an element can be used on a set. |
|
= (object) array-like object that represents set of elements |
|
** |
|
> Usage |
|
| var st = paper.set(); |
|
| st.push( |
|
| paper.circle(10, 10, 5), |
|
| paper.circle(30, 10, 5) |
|
| ); |
|
| st.attr({fill: "red"}); // changes the fill of both circles |
|
\*/ |
|
paperproto.set = function (itemsArray) { |
|
!R.is(itemsArray, "array") && (itemsArray = Array.prototype.splice.call(arguments, 0, arguments.length)); |
|
var out = new Set(itemsArray); |
|
this.__set__ && this.__set__.push(out); |
|
out["paper"] = this; |
|
out["type"] = "set"; |
|
return out; |
|
}; |
|
/*\ |
|
* Paper.setStart |
|
[ method ] |
|
** |
|
* Creates @Paper.set. All elements that will be created after calling this method and before calling |
|
* @Paper.setFinish will be added to the set. |
|
** |
|
> Usage |
|
| paper.setStart(); |
|
| paper.circle(10, 10, 5), |
|
| paper.circle(30, 10, 5) |
|
| var st = paper.setFinish(); |
|
| st.attr({fill: "red"}); // changes the fill of both circles |
|
\*/ |
|
paperproto.setStart = function (set) { |
|
this.__set__ = set || this.set(); |
|
}; |
|
/*\ |
|
* Paper.setFinish |
|
[ method ] |
|
** |
|
* See @Paper.setStart. This method finishes catching and returns resulting set. |
|
** |
|
= (object) set |
|
\*/ |
|
paperproto.setFinish = function (set) { |
|
var out = this.__set__; |
|
delete this.__set__; |
|
return out; |
|
}; |
|
/*\ |
|
* Paper.getSize |
|
[ method ] |
|
** |
|
* Obtains current paper actual size. |
|
** |
|
= (object) |
|
\*/ |
|
paperproto.getSize = function () { |
|
var container = this.canvas.parentNode; |
|
return { |
|
width: container.offsetWidth, |
|
height: container.offsetHeight |
|
}; |
|
}; |
|
/*\ |
|
* Paper.setSize |
|
[ method ] |
|
** |
|
* If you need to change dimensions of the canvas call this method |
|
** |
|
> Parameters |
|
** |
|
- width (number) new width of the canvas |
|
- height (number) new height of the canvas |
|
\*/ |
|
paperproto.setSize = function (width, height) { |
|
return R._engine.setSize.call(this, width, height); |
|
}; |
|
/*\ |
|
* Paper.setViewBox |
|
[ method ] |
|
** |
|
* Sets the view box of the paper. Practically it gives you ability to zoom and pan whole paper surface by |
|
* specifying new boundaries. |
|
** |
|
> Parameters |
|
** |
|
- x (number) new x position, default is `0` |
|
- y (number) new y position, default is `0` |
|
- w (number) new width of the canvas |
|
- h (number) new height of the canvas |
|
- fit (boolean) `true` if you want graphics to fit into new boundary box |
|
\*/ |
|
paperproto.setViewBox = function (x, y, w, h, fit) { |
|
return R._engine.setViewBox.call(this, x, y, w, h, fit); |
|
}; |
|
/*\ |
|
* Paper.top |
|
[ property ] |
|
** |
|
* Points to the topmost element on the paper |
|
\*/ |
|
/*\ |
|
* Paper.bottom |
|
[ property ] |
|
** |
|
* Points to the bottom element on the paper |
|
\*/ |
|
paperproto.top = paperproto.bottom = null; |
|
/*\ |
|
* Paper.raphael |
|
[ property ] |
|
** |
|
* Points to the @Raphael object/function |
|
\*/ |
|
paperproto.raphael = R; |
|
var getOffset = function (elem) { |
|
var box = elem.getBoundingClientRect(), |
|
doc = elem.ownerDocument, |
|
body = doc.body, |
|
docElem = doc.documentElement, |
|
clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, |
|
top = box.top + (g.win.pageYOffset || docElem.scrollTop || body.scrollTop ) - clientTop, |
|
left = box.left + (g.win.pageXOffset || docElem.scrollLeft || body.scrollLeft) - clientLeft; |
|
return { |
|
y: top, |
|
x: left |
|
}; |
|
}; |
|
/*\ |
|
* Paper.getElementByPoint |
|
[ method ] |
|
** |
|
* Returns you topmost element under given point. |
|
** |
|
= (object) Raphaël element object |
|
> Parameters |
|
** |
|
- x (number) x coordinate from the top left corner of the window |
|
- y (number) y coordinate from the top left corner of the window |
|
> Usage |
|
| paper.getElementByPoint(mouseX, mouseY).attr({stroke: "#f00"}); |
|
\*/ |
|
paperproto.getElementByPoint = function (x, y) { |
|
var paper = this, |
|
svg = paper.canvas, |
|
target = g.doc.elementFromPoint(x, y); |
|
if (g.win.opera && target.tagName == "svg") { |
|
var so = getOffset(svg), |
|
sr = svg.createSVGRect(); |
|
sr.x = x - so.x; |
|
sr.y = y - so.y; |
|
sr.width = sr.height = 1; |
|
var hits = svg.getIntersectionList(sr, null); |
|
if (hits.length) { |
|
target = hits[hits.length - 1]; |
|
} |
|
} |
|
if (!target) { |
|
return null; |
|
} |
|
while (target.parentNode && target != svg.parentNode && !target.raphael) { |
|
target = target.parentNode; |
|
} |
|
target == paper.canvas.parentNode && (target = svg); |
|
target = target && target.raphael ? paper.getById(target.raphaelid) : null; |
|
return target; |
|
}; |
|
|
|
/*\ |
|
* Paper.getElementsByBBox |
|
[ method ] |
|
** |
|
* Returns set of elements that have an intersecting bounding box |
|
** |
|
> Parameters |
|
** |
|
- bbox (object) bbox to check with |
|
= (object) @Set |
|
\*/ |
|
paperproto.getElementsByBBox = function (bbox) { |
|
var set = this.set(); |
|
this.forEach(function (el) { |
|
if (R.isBBoxIntersect(el.getBBox(), bbox)) { |
|
set.push(el); |
|
} |
|
}); |
|
return set; |
|
}; |
|
|
|
/*\ |
|
* Paper.getById |
|
[ method ] |
|
** |
|
* Returns you element by its internal ID. |
|
** |
|
> Parameters |
|
** |
|
- id (number) id |
|
= (object) Raphaël element object |
|
\*/ |
|
paperproto.getById = function (id) { |
|
var bot = this.bottom; |
|
while (bot) { |
|
if (bot.id == id) { |
|
return bot; |
|
} |
|
bot = bot.next; |
|
} |
|
return null; |
|
}; |
|
/*\ |
|
* Paper.forEach |
|
[ method ] |
|
** |
|
* Executes given function for each element on the paper |
|
* |
|
* If callback function returns `false` it will stop loop running. |
|
** |
|
> Parameters |
|
** |
|
- callback (function) function to run |
|
- thisArg (object) context object for the callback |
|
= (object) Paper object |
|
> Usage |
|
| paper.forEach(function (el) { |
|
| el.attr({ stroke: "blue" }); |
|
| }); |
|
\*/ |
|
paperproto.forEach = function (callback, thisArg) { |
|
var bot = this.bottom; |
|
while (bot) { |
|
if (callback.call(thisArg, bot) === false) { |
|
return this; |
|
} |
|
bot = bot.next; |
|
} |
|
return this; |
|
}; |
|
/*\ |
|
* Paper.getElementsByPoint |
|
[ method ] |
|
** |
|
* Returns set of elements that have common point inside |
|
** |
|
> Parameters |
|
** |
|
- x (number) x coordinate of the point |
|
- y (number) y coordinate of the point |
|
= (object) @Set |
|
\*/ |
|
paperproto.getElementsByPoint = function (x, y) { |
|
var set = this.set(); |
|
this.forEach(function (el) { |
|
if (el.isPointInside(x, y)) { |
|
set.push(el); |
|
} |
|
}); |
|
return set; |
|
}; |
|
function x_y() { |
|
return this.x + S + this.y; |
|
} |
|
function x_y_w_h() { |
|
return this.x + S + this.y + S + this.width + " \xd7 " + this.height; |
|
} |
|
/*\ |
|
* Element.isPointInside |
|
[ method ] |
|
** |
|
* Determine if given point is inside this element’s shape |
|
** |
|
> Parameters |
|
** |
|
- x (number) x coordinate of the point |
|
- y (number) y coordinate of the point |
|
= (boolean) `true` if point inside the shape |
|
\*/ |
|
elproto.isPointInside = function (x, y) { |
|
var rp = this.realPath = getPath[this.type](this); |
|
if (this.attr('transform') && this.attr('transform').length) { |
|
rp = R.transformPath(rp, this.attr('transform')); |
|
} |
|
return R.isPointInsidePath(rp, x, y); |
|
}; |
|
/*\ |
|
* Element.getBBox |
|
[ method ] |
|
** |
|
* Return bounding box for a given element |
|
** |
|
> Parameters |
|
** |
|
- isWithoutTransform (boolean) flag, `true` if you want to have bounding box before transformations. Default is `false`. |
|
= (object) Bounding box object: |
|
o { |
|
o x: (number) top left corner x |
|
o y: (number) top left corner y |
|
o x2: (number) bottom right corner x |
|
o y2: (number) bottom right corner y |
|
o width: (number) width |
|
o height: (number) height |
|
o } |
|
\*/ |
|
elproto.getBBox = function (isWithoutTransform) { |
|
if (this.removed) { |
|
return {}; |
|
} |
|
var _ = this._; |
|
if (isWithoutTransform) { |
|
if (_.dirty || !_.bboxwt) { |
|
this.realPath = getPath[this.type](this); |
|
_.bboxwt = pathDimensions(this.realPath); |
|
_.bboxwt.toString = x_y_w_h; |
|
_.dirty = 0; |
|
} |
|
return _.bboxwt; |
|
} |
|
if (_.dirty || _.dirtyT || !_.bbox) { |
|
if (_.dirty || !this.realPath) { |
|
_.bboxwt = 0; |
|
this.realPath = getPath[this.type](this); |
|
} |
|
_.bbox = pathDimensions(mapPath(this.realPath, this.matrix)); |
|
_.bbox.toString = x_y_w_h; |
|
_.dirty = _.dirtyT = 0; |
|
} |
|
return _.bbox; |
|
}; |
|
/*\ |
|
* Element.clone |
|
[ method ] |
|
** |
|
= (object) clone of a given element |
|
** |
|
\*/ |
|
elproto.clone = function () { |
|
if (this.removed) { |
|
return null; |
|
} |
|
var out = this.paper[this.type]().attr(this.attr()); |
|
this.__set__ && this.__set__.push(out); |
|
return out; |
|
}; |
|
/*\ |
|
* Element.glow |
|
[ method ] |
|
** |
|
* Return set of elements that create glow-like effect around given element. See @Paper.set. |
|
* |
|
* Note: Glow is not connected to the element. If you change element attributes it won’t adjust itself. |
|
** |
|
> Parameters |
|
** |
|
- glow (object) #optional parameters object with all properties optional: |
|
o { |
|
o width (number) size of the glow, default is `10` |
|
o fill (boolean) will it be filled, default is `false` |
|
o opacity (number) opacity, default is `0.5` |
|
o offsetx (number) horizontal offset, default is `0` |
|
o offsety (number) vertical offset, default is `0` |
|
o color (string) glow colour, default is `black` |
|
o } |
|
= (object) @Paper.set of elements that represents glow |
|
\*/ |
|
elproto.glow = function (glow) { |
|
if (this.type == "text") { |
|
return null; |
|
} |
|
glow = glow || {}; |
|
var s = { |
|
width: (glow.width || 10) + (+this.attr("stroke-width") || 1), |
|
fill: glow.fill || false, |
|
opacity: glow.opacity == null ? .5 : glow.opacity, |
|
offsetx: glow.offsetx || 0, |
|
offsety: glow.offsety || 0, |
|
color: glow.color || "#000" |
|
}, |
|
c = s.width / 2, |
|
r = this.paper, |
|
out = r.set(), |
|
path = this.realPath || getPath[this.type](this); |
|
path = this.matrix ? mapPath(path, this.matrix) : path; |
|
for (var i = 1; i < c + 1; i++) { |
|
out.push(r.path(path).attr({ |
|
stroke: s.color, |
|
fill: s.fill ? s.color : "none", |
|
"stroke-linejoin": "round", |
|
"stroke-linecap": "round", |
|
"stroke-width": +(s.width / c * i).toFixed(3), |
|
opacity: +(s.opacity / c).toFixed(3) |
|
})); |
|
} |
|
return out.insertBefore(this).translate(s.offsetx, s.offsety); |
|
}; |
|
var curveslengths = {}, |
|
getPointAtSegmentLength = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length) { |
|
if (length == null) { |
|
return bezlen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y); |
|
} else { |
|
return R.findDotsAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, getTatLen(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, length)); |
|
} |
|
}, |
|
getLengthFactory = function (istotal, subpath) { |
|
return function (path, length, onlystart) { |
|
path = path2curve(path); |
|
var x, y, p, l, sp = "", subpaths = {}, point, |
|
len = 0; |
|
for (var i = 0, ii = path.length; i < ii; i++) { |
|
p = path[i]; |
|
if (p[0] == "M") { |
|
x = +p[1]; |
|
y = +p[2]; |
|
} else { |
|
l = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6]); |
|
if (len + l > length) { |
|
if (subpath && !subpaths.start) { |
|
point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len); |
|
sp += ["C" + point.start.x, point.start.y, point.m.x, point.m.y, point.x, point.y]; |
|
if (onlystart) {return sp;} |
|
subpaths.start = sp; |
|
sp = ["M" + point.x, point.y + "C" + point.n.x, point.n.y, point.end.x, point.end.y, p[5], p[6]].join(); |
|
len += l; |
|
x = +p[5]; |
|
y = +p[6]; |
|
continue; |
|
} |
|
if (!istotal && !subpath) { |
|
point = getPointAtSegmentLength(x, y, p[1], p[2], p[3], p[4], p[5], p[6], length - len); |
|
return {x: point.x, y: point.y, alpha: point.alpha}; |
|
} |
|
} |
|
len += l; |
|
x = +p[5]; |
|
y = +p[6]; |
|
} |
|
sp += p.shift() + p; |
|
} |
|
subpaths.end = sp; |
|
point = istotal ? len : subpath ? subpaths : R.findDotsAtSegment(x, y, p[0], p[1], p[2], p[3], p[4], p[5], 1); |
|
point.alpha && (point = {x: point.x, y: point.y, alpha: point.alpha}); |
|
return point; |
|
}; |
|
}; |
|
var getTotalLength = getLengthFactory(1), |
|
getPointAtLength = getLengthFactory(), |
|
getSubpathsAtLength = getLengthFactory(0, 1); |
|
/*\ |
|
* Raphael.getTotalLength |
|
[ method ] |
|
** |
|
* Returns length of the given path in pixels. |
|
** |
|
> Parameters |
|
** |
|
- path (string) SVG path string. |
|
** |
|
= (number) length. |
|
\*/ |
|
R.getTotalLength = getTotalLength; |
|
/*\ |
|
* Raphael.getPointAtLength |
|
[ method ] |
|
** |
|
* Return coordinates of the point located at the given length on the given path. |
|
** |
|
> Parameters |
|
** |
|
- path (string) SVG path string |
|
- length (number) |
|
** |
|
= (object) representation of the point: |
|
o { |
|
o x: (number) x coordinate |
|
o y: (number) y coordinate |
|
o alpha: (number) angle of derivative |
|
o } |
|
\*/ |
|
R.getPointAtLength = getPointAtLength; |
|
/*\ |
|
* Raphael.getSubpath |
|
[ method ] |
|
** |
|
* Return subpath of a given path from given length to given length. |
|
** |
|
> Parameters |
|
** |
|
- path (string) SVG path string |
|
- from (number) position of the start of the segment |
|
- to (number) position of the end of the segment |
|
** |
|
= (string) pathstring for the segment |
|
\*/ |
|
R.getSubpath = function (path, from, to) { |
|
if (this.getTotalLength(path) - to < 1e-6) { |
|
return getSubpathsAtLength(path, from).end; |
|
} |
|
var a = getSubpathsAtLength(path, to, 1); |
|
return from ? getSubpathsAtLength(a, from).end : a; |
|
}; |
|
/*\ |
|
* Element.getTotalLength |
|
[ method ] |
|
** |
|
* Returns length of the path in pixels. Only works for element of “path” type. |
|
= (number) length. |
|
\*/ |
|
elproto.getTotalLength = function () { |
|
var path = this.getPath(); |
|
if (!path) { |
|
return; |
|
} |
|
|
|
if (this.node.getTotalLength) { |
|
return this.node.getTotalLength(); |
|
} |
|
|
|
return getTotalLength(path); |
|
}; |
|
/*\ |
|
* Element.getPointAtLength |
|
[ method ] |
|
** |
|
* Return coordinates of the point located at the given length on the given path. Only works for element of “path” type. |
|
** |
|
> Parameters |
|
** |
|
- length (number) |
|
** |
|
= (object) representation of the point: |
|
o { |
|
o x: (number) x coordinate |
|
o y: (number) y coordinate |
|
o alpha: (number) angle of derivative |
|
o } |
|
\*/ |
|
elproto.getPointAtLength = function (length) { |
|
var path = this.getPath(); |
|
if (!path) { |
|
return; |
|
} |
|
|
|
return getPointAtLength(path, length); |
|
}; |
|
/*\ |
|
* Element.getPath |
|
[ method ] |
|
** |
|
* Returns path of the element. Only works for elements of “path” type and simple elements like circle. |
|
= (object) path |
|
** |
|
\*/ |
|
elproto.getPath = function () { |
|
var path, |
|
getPath = R._getPath[this.type]; |
|
|
|
if (this.type == "text" || this.type == "set") { |
|
return; |
|
} |
|
|
|
if (getPath) { |
|
path = getPath(this); |
|
} |
|
|
|
return path; |
|
}; |
|
/*\ |
|
* Element.getSubpath |
|
[ method ] |
|
** |
|
* Return subpath of a given element from given length to given length. Only works for element of “path” type. |
|
** |
|
> Parameters |
|
** |
|
- from (number) position of the start of the segment |
|
- to (number) position of the end of the segment |
|
** |
|
= (string) pathstring for the segment |
|
\*/ |
|
elproto.getSubpath = function (from, to) { |
|
var path = this.getPath(); |
|
if (!path) { |
|
return; |
|
} |
|
|
|
return R.getSubpath(path, from, to); |
|
}; |
|
/*\ |
|
* Raphael.easing_formulas |
|
[ property ] |
|
** |
|
* Object that contains easing formulas for animation. You could extend it with your own. By default it has following list of easing: |
|
# <ul> |
|
# <li>“linear”</li> |
|
# <li>“<” or “easeIn” or “ease-in”</li> |
|
# <li>“>” or “easeOut” or “ease-out”</li> |
|
# <li>“<>” or “easeInOut” or “ease-in-out”</li> |
|
# <li>“backIn” or “back-in”</li> |
|
# <li>“backOut” or “back-out”</li> |
|
# <li>“elastic”</li> |
|
# <li>“bounce”</li> |
|
# </ul> |
|
# <p>See also <a href="http://raphaeljs.com/easing.html">Easing demo</a>.</p> |
|
\*/ |
|
var ef = R.easing_formulas = { |
|
linear: function (n) { |
|
return n; |
|
}, |
|
"<": function (n) { |
|
return pow(n, 1.7); |
|
}, |
|
">": function (n) { |
|
return pow(n, .48); |
|
}, |
|
"<>": function (n) { |
|
var q = .48 - n / 1.04, |
|
Q = math.sqrt(.1734 + q * q), |
|
x = Q - q, |
|
X = pow(abs(x), 1 / 3) * (x < 0 ? -1 : 1), |
|
y = -Q - q, |
|
Y = pow(abs(y), 1 / 3) * (y < 0 ? -1 : 1), |
|
t = X + Y + .5; |
|
return (1 - t) * 3 * t * t + t * t * t; |
|
}, |
|
backIn: function (n) { |
|
var s = 1.70158; |
|
return n * n * ((s + 1) * n - s); |
|
}, |
|
backOut: function (n) { |
|
n = n - 1; |
|
var s = 1.70158; |
|
return n * n * ((s + 1) * n + s) + 1; |
|
}, |
|
elastic: function (n) { |
|
if (n == !!n) { |
|
return n; |
|
} |
|
return pow(2, -10 * n) * math.sin((n - .075) * (2 * PI) / .3) + 1; |
|
}, |
|
bounce: function (n) { |
|
var s = 7.5625, |
|
p = 2.75, |
|
l; |
|
if (n < (1 / p)) { |
|
l = s * n * n; |
|
} else { |
|
if (n < (2 / p)) { |
|
n -= (1.5 / p); |
|
l = s * n * n + .75; |
|
} else { |
|
if (n < (2.5 / p)) { |
|
n -= (2.25 / p); |
|
l = s * n * n + .9375; |
|
} else { |
|
n -= (2.625 / p); |
|
l = s * n * n + .984375; |
|
} |
|
} |
|
} |
|
return l; |
|
} |
|
}; |
|
ef.easeIn = ef["ease-in"] = ef["<"]; |
|
ef.easeOut = ef["ease-out"] = ef[">"]; |
|
ef.easeInOut = ef["ease-in-out"] = ef["<>"]; |
|
ef["back-in"] = ef.backIn; |
|
ef["back-out"] = ef.backOut; |
|
|
|
var animationElements = [], |
|
requestAnimFrame = window.requestAnimationFrame || |
|
window.webkitRequestAnimationFrame || |
|
window.mozRequestAnimationFrame || |
|
window.oRequestAnimationFrame || |
|
window.msRequestAnimationFrame || |
|
function (callback) { |
|
setTimeout(callback, 16); |
|
}, |
|
animation = function () { |
|
var Now = +new Date, |
|
l = 0; |
|
for (; l < animationElements.length; l++) { |
|
var e = animationElements[l]; |
|
if (e.el.removed || e.paused) { |
|
continue; |
|
} |
|
var time = Now - e.start, |
|
ms = e.ms, |
|
easing = e.easing, |
|
from = e.from, |
|
diff = e.diff, |
|
to = e.to, |
|
t = e.t, |
|
that = e.el, |
|
set = {}, |
|
now, |
|
init = {}, |
|
key; |
|
if (e.initstatus) { |
|
time = (e.initstatus * e.anim.top - e.prev) / (e.percent - e.prev) * ms; |
|
e.status = e.initstatus; |
|
delete e.initstatus; |
|
e.stop && animationElements.splice(l--, 1); |
|
} else { |
|
e.status = (e.prev + (e.percent - e.prev) * (time / ms)) / e.anim.top; |
|
} |
|
if (time < 0) { |
|
continue; |
|
} |
|
if (time < ms) { |
|
var pos = easing(time / ms); |
|
for (var attr in from) if (from[has](attr)) { |
|
switch (availableAnimAttrs[attr]) { |
|
case nu: |
|
now = +from[attr] + pos * ms * diff[attr]; |
|
break; |
|
case "colour": |
|
now = "rgb(" + [ |
|
upto255(round(from[attr].r + pos * ms * diff[attr].r)), |
|
upto255(round(from[attr].g + pos * ms * diff[attr].g)), |
|
upto255(round(from[attr].b + pos * ms * diff[attr].b)) |
|
].join(",") + ")"; |
|
break; |
|
case "path": |
|
now = []; |
|
for (var i = 0, ii = from[attr].length; i < ii; i++) { |
|
now[i] = [from[attr][i][0]]; |
|
for (var j = 1, jj = from[attr][i].length; j < jj; j++) { |
|
now[i][j] = +from[attr][i][j] + pos * ms * diff[attr][i][j]; |
|
} |
|
now[i] = now[i].join(S); |
|
} |
|
now = now.join(S); |
|
break; |
|
case "transform": |
|
if (diff[attr].real) { |
|
now = []; |
|
for (i = 0, ii = from[attr].length; i < ii; i++) { |
|
now[i] = [from[attr][i][0]]; |
|
for (j = 1, jj = from[attr][i].length; j < jj; j++) { |
|
now[i][j] = from[attr][i][j] + pos * ms * diff[attr][i][j]; |
|
} |
|
} |
|
} else { |
|
var get = function (i) { |
|
return +from[attr][i] + pos * ms * diff[attr][i]; |
|
}; |
|
// now = [["r", get(2), 0, 0], ["t", get(3), get(4)], ["s", get(0), get(1), 0, 0]]; |
|
now = [["m", get(0), get(1), get(2), get(3), get(4), get(5)]]; |
|
} |
|
break; |
|
case "csv": |
|
if (attr == "clip-rect") { |
|
now = []; |
|
i = 4; |
|
while (i--) { |
|
now[i] = +from[attr][i] + pos * ms * diff[attr][i]; |
|
} |
|
} |
|
break; |
|
default: |
|
var from2 = [][concat](from[attr]); |
|
now = []; |
|
i = that.paper.customAttributes[attr].length; |
|
while (i--) { |
|
now[i] = +from2[i] + pos * ms * diff[attr][i]; |
|
} |
|
break; |
|
} |
|
set[attr] = now; |
|
} |
|
that.attr(set); |
|
(function (id, that, anim) { |
|
setTimeout(function () { |
|
eve("raphael.anim.frame." + id, that, anim); |
|
}); |
|
})(that.id, that, e.anim); |
|
} else { |
|
(function(f, el, a) { |
|
setTimeout(function() { |
|
eve("raphael.anim.frame." + el.id, el, a); |
|
eve("raphael.anim.finish." + el.id, el, a); |
|
R.is(f, "function") && f.call(el); |
|
}); |
|
})(e.callback, that, e.anim); |
|
that.attr(to); |
|
animationElements.splice(l--, 1); |
|
if (e.repeat > 1 && !e.next) { |
|
for (key in to) if (to[has](key)) { |
|
init[key] = e.totalOrigin[key]; |
|
} |
|
e.el.attr(init); |
|
runAnimation(e.anim, e.el, e.anim.percents[0], null, e.totalOrigin, e.repeat - 1); |
|
} |
|
if (e.next && !e.stop) { |
|
runAnimation(e.anim, e.el, e.next, null, e.totalOrigin, e.repeat); |
|
} |
|
} |
|
} |
|
animationElements.length && requestAnimFrame(animation); |
|
}, |
|
upto255 = function (color) { |
|
return color > 255 ? 255 : color < 0 ? 0 : color; |
|
}; |
|
/*\ |
|
* Element.animateWith |
|
[ method ] |
|
** |
|
* Acts similar to @Element.animate, but ensure that given animation runs in sync with another given element. |
|
** |
|
> Parameters |
|
** |
|
- el (object) element to sync with |
|
- anim (object) animation to sync with |
|
- params (object) #optional final attributes for the element, see also @Element.attr |
|
- ms (number) #optional number of milliseconds for animation to run |
|
- easing (string) #optional easing type. Accept on of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` |
|
- callback (function) #optional callback function. Will be called at the end of animation. |
|
* or |
|
- element (object) element to sync with |
|
- anim (object) animation to sync with |
|
- animation (object) #optional animation object, see @Raphael.animation |
|
** |
|
= (object) original element |
|
\*/ |
|
elproto.animateWith = function (el, anim, params, ms, easing, callback) { |
|
var element = this; |
|
if (element.removed) { |
|
callback && callback.call(element); |
|
return element; |
|
} |
|
var a = params instanceof Animation ? params : R.animation(params, ms, easing, callback), |
|
x, y; |
|
runAnimation(a, element, a.percents[0], null, element.attr()); |
|
for (var i = 0, ii = animationElements.length; i < ii; i++) { |
|
if (animationElements[i].anim == anim && animationElements[i].el == el) { |
|
animationElements[ii - 1].start = animationElements[i].start; |
|
break; |
|
} |
|
} |
|
return element; |
|
// |
|
// |
|
// var a = params ? R.animation(params, ms, easing, callback) : anim, |
|
// status = element.status(anim); |
|
// return this.animate(a).status(a, status * anim.ms / a.ms); |
|
}; |
|
function CubicBezierAtTime(t, p1x, p1y, p2x, p2y, duration) { |
|
var cx = 3 * p1x, |
|
bx = 3 * (p2x - p1x) - cx, |
|
ax = 1 - cx - bx, |
|
cy = 3 * p1y, |
|
by = 3 * (p2y - p1y) - cy, |
|
ay = 1 - cy - by; |
|
function sampleCurveX(t) { |
|
return ((ax * t + bx) * t + cx) * t; |
|
} |
|
function solve(x, epsilon) { |
|
var t = solveCurveX(x, epsilon); |
|
return ((ay * t + by) * t + cy) * t; |
|
} |
|
function solveCurveX(x, epsilon) { |
|
var t0, t1, t2, x2, d2, i; |
|
for(t2 = x, i = 0; i < 8; i++) { |
|
x2 = sampleCurveX(t2) - x; |
|
if (abs(x2) < epsilon) { |
|
return t2; |
|
} |
|
d2 = (3 * ax * t2 + 2 * bx) * t2 + cx; |
|
if (abs(d2) < 1e-6) { |
|
break; |
|
} |
|
t2 = t2 - x2 / d2; |
|
} |
|
t0 = 0; |
|
t1 = 1; |
|
t2 = x; |
|
if (t2 < t0) { |
|
return t0; |
|
} |
|
if (t2 > t1) { |
|
return t1; |
|
} |
|
while (t0 < t1) { |
|
x2 = sampleCurveX(t2); |
|
if (abs(x2 - x) < epsilon) { |
|
return t2; |
|
} |
|
if (x > x2) { |
|
t0 = t2; |
|
} else { |
|
t1 = t2; |
|
} |
|
t2 = (t1 - t0) / 2 + t0; |
|
} |
|
return t2; |
|
} |
|
return solve(t, 1 / (200 * duration)); |
|
} |
|
elproto.onAnimation = function (f) { |
|
f ? eve.on("raphael.anim.frame." + this.id, f) : eve.unbind("raphael.anim.frame." + this.id); |
|
return this; |
|
}; |
|
function Animation(anim, ms) { |
|
var percents = [], |
|
newAnim = {}; |
|
this.ms = ms; |
|
this.times = 1; |
|
if (anim) { |
|
for (var attr in anim) if (anim[has](attr)) { |
|
newAnim[toFloat(attr)] = anim[attr]; |
|
percents.push(toFloat(attr)); |
|
} |
|
percents.sort(sortByNumber); |
|
} |
|
this.anim = newAnim; |
|
this.top = percents[percents.length - 1]; |
|
this.percents = percents; |
|
} |
|
/*\ |
|
* Animation.delay |
|
[ method ] |
|
** |
|
* Creates a copy of existing animation object with given delay. |
|
** |
|
> Parameters |
|
** |
|
- delay (number) number of ms to pass between animation start and actual animation |
|
** |
|
= (object) new altered Animation object |
|
| var anim = Raphael.animation({cx: 10, cy: 20}, 2e3); |
|
| circle1.animate(anim); // run the given animation immediately |
|
| circle2.animate(anim.delay(500)); // run the given animation after 500 ms |
|
\*/ |
|
Animation.prototype.delay = function (delay) { |
|
var a = new Animation(this.anim, this.ms); |
|
a.times = this.times; |
|
a.del = +delay || 0; |
|
return a; |
|
}; |
|
/*\ |
|
* Animation.repeat |
|
[ method ] |
|
** |
|
* Creates a copy of existing animation object with given repetition. |
|
** |
|
> Parameters |
|
** |
|
- repeat (number) number iterations of animation. For infinite animation pass `Infinity` |
|
** |
|
= (object) new altered Animation object |
|
\*/ |
|
Animation.prototype.repeat = function (times) { |
|
var a = new Animation(this.anim, this.ms); |
|
a.del = this.del; |
|
a.times = math.floor(mmax(times, 0)) || 1; |
|
return a; |
|
}; |
|
function runAnimation(anim, element, percent, status, totalOrigin, times) { |
|
percent = toFloat(percent); |
|
var params, |
|
isInAnim, |
|
isInAnimSet, |
|
percents = [], |
|
next, |
|
prev, |
|
timestamp, |
|
ms = anim.ms, |
|
from = {}, |
|
to = {}, |
|
diff = {}; |
|
if (status) { |
|
for (i = 0, ii = animationElements.length; i < ii; i++) { |
|
var e = animationElements[i]; |
|
if (e.el.id == element.id && e.anim == anim) { |
|
if (e.percent != percent) { |
|
animationElements.splice(i, 1); |
|
isInAnimSet = 1; |
|
} else { |
|
isInAnim = e; |
|
} |
|
element.attr(e.totalOrigin); |
|
break; |
|
} |
|
} |
|
} else { |
|
status = +to; // NaN |
|
} |
|
for (var i = 0, ii = anim.percents.length; i < ii; i++) { |
|
if (anim.percents[i] == percent || anim.percents[i] > status * anim.top) { |
|
percent = anim.percents[i]; |
|
prev = anim.percents[i - 1] || 0; |
|
ms = ms / anim.top * (percent - prev); |
|
next = anim.percents[i + 1]; |
|
params = anim.anim[percent]; |
|
break; |
|
} else if (status) { |
|
element.attr(anim.anim[anim.percents[i]]); |
|
} |
|
} |
|
if (!params) { |
|
return; |
|
} |
|
if (!isInAnim) { |
|
for (var attr in params) if (params[has](attr)) { |
|
if (availableAnimAttrs[has](attr) || element.paper.customAttributes[has](attr)) { |
|
from[attr] = element.attr(attr); |
|
(from[attr] == null) && (from[attr] = availableAttrs[attr]); |
|
to[attr] = params[attr]; |
|
switch (availableAnimAttrs[attr]) { |
|
case nu: |
|
diff[attr] = (to[attr] - from[attr]) / ms; |
|
break; |
|
case "colour": |
|
from[attr] = R.getRGB(from[attr]); |
|
var toColour = R.getRGB(to[attr]); |
|
diff[attr] = { |
|
r: (toColour.r - from[attr].r) / ms, |
|
g: (toColour.g - from[attr].g) / ms, |
|
b: (toColour.b - from[attr].b) / ms |
|
}; |
|
break; |
|
case "path": |
|
var pathes = path2curve(from[attr], to[attr]), |
|
toPath = pathes[1]; |
|
from[attr] = pathes[0]; |
|
diff[attr] = []; |
|
for (i = 0, ii = from[attr].length; i < ii; i++) { |
|
diff[attr][i] = [0]; |
|
for (var j = 1, jj = from[attr][i].length; j < jj; j++) { |
|
diff[attr][i][j] = (toPath[i][j] - from[attr][i][j]) / ms; |
|
} |
|
} |
|
break; |
|
case "transform": |
|
var _ = element._, |
|
eq = equaliseTransform(_[attr], to[attr]); |
|
if (eq) { |
|
from[attr] = eq.from; |
|
to[attr] = eq.to; |
|
diff[attr] = []; |
|
diff[attr].real = true; |
|
for (i = 0, ii = from[attr].length; i < ii; i++) { |
|
diff[attr][i] = [from[attr][i][0]]; |
|
for (j = 1, jj = from[attr][i].length; j < jj; j++) { |
|
diff[attr][i][j] = (to[attr][i][j] - from[attr][i][j]) / ms; |
|
} |
|
} |
|
} else { |
|
var m = (element.matrix || new Matrix), |
|
to2 = { |
|
_: {transform: _.transform}, |
|
getBBox: function () { |
|
return element.getBBox(1); |
|
} |
|
}; |
|
from[attr] = [ |
|
m.a, |
|
m.b, |
|
m.c, |
|
m.d, |
|
m.e, |
|
m.f |
|
]; |
|
extractTransform(to2, to[attr]); |
|
to[attr] = to2._.transform; |
|
diff[attr] = [ |
|
(to2.matrix.a - m.a) / ms, |
|
(to2.matrix.b - m.b) / ms, |
|
(to2.matrix.c - m.c) / ms, |
|
(to2.matrix.d - m.d) / ms, |
|
(to2.matrix.e - m.e) / ms, |
|
(to2.matrix.f - m.f) / ms |
|
]; |
|
// from[attr] = [_.sx, _.sy, _.deg, _.dx, _.dy]; |
|
// var to2 = {_:{}, getBBox: function () { return element.getBBox(); }}; |
|
// extractTransform(to2, to[attr]); |
|
// diff[attr] = [ |
|
// (to2._.sx - _.sx) / ms, |
|
// (to2._.sy - _.sy) / ms, |
|
// (to2._.deg - _.deg) / ms, |
|
// (to2._.dx - _.dx) / ms, |
|
// (to2._.dy - _.dy) / ms |
|
// ]; |
|
} |
|
break; |
|
case "csv": |
|
var values = Str(params[attr])[split](separator), |
|
from2 = Str(from[attr])[split](separator); |
|
if (attr == "clip-rect") { |
|
from[attr] = from2; |
|
diff[attr] = []; |
|
i = from2.length; |
|
while (i--) { |
|
diff[attr][i] = (values[i] - from[attr][i]) / ms; |
|
} |
|
} |
|
to[attr] = values; |
|
break; |
|
default: |
|
values = [][concat](params[attr]); |
|
from2 = [][concat](from[attr]); |
|
diff[attr] = []; |
|
i = element.paper.customAttributes[attr].length; |
|
while (i--) { |
|
diff[attr][i] = ((values[i] || 0) - (from2[i] || 0)) / ms; |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
var easing = params.easing, |
|
easyeasy = R.easing_formulas[easing]; |
|
if (!easyeasy) { |
|
easyeasy = Str(easing).match(bezierrg); |
|
if (easyeasy && easyeasy.length == 5) { |
|
var curve = easyeasy; |
|
easyeasy = function (t) { |
|
return CubicBezierAtTime(t, +curve[1], +curve[2], +curve[3], +curve[4], ms); |
|
}; |
|
} else { |
|
easyeasy = pipe; |
|
} |
|
} |
|
timestamp = params.start || anim.start || +new Date; |
|
e = { |
|
anim: anim, |
|
percent: percent, |
|
timestamp: timestamp, |
|
start: timestamp + (anim.del || 0), |
|
status: 0, |
|
initstatus: status || 0, |
|
stop: false, |
|
ms: ms, |
|
easing: easyeasy, |
|
from: from, |
|
diff: diff, |
|
to: to, |
|
el: element, |
|
callback: params.callback, |
|
prev: prev, |
|
next: next, |
|
repeat: times || anim.times, |
|
origin: element.attr(), |
|
totalOrigin: totalOrigin |
|
}; |
|
animationElements.push(e); |
|
if (status && !isInAnim && !isInAnimSet) { |
|
e.stop = true; |
|
e.start = new Date - ms * status; |
|
if (animationElements.length == 1) { |
|
return animation(); |
|
} |
|
} |
|
if (isInAnimSet) { |
|
e.start = new Date - e.ms * status; |
|
} |
|
animationElements.length == 1 && requestAnimFrame(animation); |
|
} else { |
|
isInAnim.initstatus = status; |
|
isInAnim.start = new Date - isInAnim.ms * status; |
|
} |
|
eve("raphael.anim.start." + element.id, element, anim); |
|
} |
|
/*\ |
|
* Raphael.animation |
|
[ method ] |
|
** |
|
* Creates an animation object that can be passed to the @Element.animate or @Element.animateWith methods. |
|
* See also @Animation.delay and @Animation.repeat methods. |
|
** |
|
> Parameters |
|
** |
|
- params (object) final attributes for the element, see also @Element.attr |
|
- ms (number) number of milliseconds for animation to run |
|
- easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` |
|
- callback (function) #optional callback function. Will be called at the end of animation. |
|
** |
|
= (object) @Animation |
|
\*/ |
|
R.animation = function (params, ms, easing, callback) { |
|
if (params instanceof Animation) { |
|
return params; |
|
} |
|
if (R.is(easing, "function") || !easing) { |
|
callback = callback || easing || null; |
|
easing = null; |
|
} |
|
params = Object(params); |
|
ms = +ms || 0; |
|
var p = {}, |
|
json, |
|
attr; |
|
for (attr in params) if (params[has](attr) && toFloat(attr) != attr && toFloat(attr) + "%" != attr) { |
|
json = true; |
|
p[attr] = params[attr]; |
|
} |
|
if (!json) { |
|
// if percent-like syntax is used and end-of-all animation callback used |
|
if(callback){ |
|
// find the last one |
|
var lastKey = 0; |
|
for(var i in params){ |
|
var percent = toInt(i); |
|
if(params[has](i) && percent > lastKey){ |
|
lastKey = percent; |
|
} |
|
} |
|
lastKey += '%'; |
|
// if already defined callback in the last keyframe, skip |
|
!params[lastKey].callback && (params[lastKey].callback = callback); |
|
} |
|
return new Animation(params, ms); |
|
} else { |
|
easing && (p.easing = easing); |
|
callback && (p.callback = callback); |
|
return new Animation({100: p}, ms); |
|
} |
|
}; |
|
/*\ |
|
* Element.animate |
|
[ method ] |
|
** |
|
* Creates and starts animation for given element. |
|
** |
|
> Parameters |
|
** |
|
- params (object) final attributes for the element, see also @Element.attr |
|
- ms (number) number of milliseconds for animation to run |
|
- easing (string) #optional easing type. Accept one of @Raphael.easing_formulas or CSS format: `cubic‐bezier(XX, XX, XX, XX)` |
|
- callback (function) #optional callback function. Will be called at the end of animation. |
|
* or |
|
- animation (object) animation object, see @Raphael.animation |
|
** |
|
= (object) original element |
|
\*/ |
|
elproto.animate = function (params, ms, easing, callback) { |
|
var element = this; |
|
if (element.removed) { |
|
callback && callback.call(element); |
|
return element; |
|
} |
|
var anim = params instanceof Animation ? params : R.animation(params, ms, easing, callback); |
|
runAnimation(anim, element, anim.percents[0], null, element.attr()); |
|
return element; |
|
}; |
|
/*\ |
|
* Element.setTime |
|
[ method ] |
|
** |
|
* Sets the status of animation of the element in milliseconds. Similar to @Element.status method. |
|
** |
|
> Parameters |
|
** |
|
- anim (object) animation object |
|
- value (number) number of milliseconds from the beginning of the animation |
|
** |
|
= (object) original element if `value` is specified |
|
* Note, that during animation following events are triggered: |
|
* |
|
* On each animation frame event `anim.frame.<id>`, on start `anim.start.<id>` and on end `anim.finish.<id>`. |
|
\*/ |
|
elproto.setTime = function (anim, value) { |
|
if (anim && value != null) { |
|
this.status(anim, mmin(value, anim.ms) / anim.ms); |
|
} |
|
return this; |
|
}; |
|
/*\ |
|
* Element.status |
|
[ method ] |
|
** |
|
* Gets or sets the status of animation of the element. |
|
** |
|
> Parameters |
|
** |
|
- anim (object) #optional animation object |
|
- value (number) #optional 0 – 1. If specified, method works like a setter and sets the status of a given animation to the value. This will cause animation to jump to the given position. |
|
** |
|
= (number) status |
|
* or |
|
= (array) status if `anim` is not specified. Array of objects in format: |
|
o { |
|
o anim: (object) animation object |
|
o status: (number) status |
|
o } |
|
* or |
|
= (object) original element if `value` is specified |
|
\*/ |
|
elproto.status = function (anim, value) { |
|
var out = [], |
|
i = 0, |
|
len, |
|
e; |
|
if (value != null) { |
|
runAnimation(anim, this, -1, mmin(value, 1)); |
|
return this; |
|
} else { |
|
len = animationElements.length; |
|
for (; i < len; i++) { |
|
e = animationElements[i]; |
|
if (e.el.id == this.id && (!anim || e.anim == anim)) { |
|
if (anim) { |
|
return e.status; |
|
} |
|
out.push({ |
|
anim: e.anim, |
|
status: e.status |
|
}); |
|
} |
|
} |
|
if (anim) { |
|
return 0; |
|
} |
|
return out; |
|
} |
|
}; |
|
/*\ |
|
* Element.pause |
|
[ method ] |
|
** |
|
* Stops animation of the element with ability to resume it later on. |
|
** |
|
> Parameters |
|
** |
|
- anim (object) #optional animation object |
|
** |
|
= (object) original element |
|
\*/ |
|
elproto.pause = function (anim) { |
|
for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) { |
|
if (eve("raphael.anim.pause." + this.id, this, animationElements[i].anim) !== false) { |
|
animationElements[i].paused = true; |
|
} |
|
} |
|
return this; |
|
}; |
|
/*\ |
|
* Element.resume |
|
[ method ] |
|
** |
|
* Resumes animation if it was paused with @Element.pause method. |
|
** |
|
> Parameters |
|
** |
|
- anim (object) #optional animation object |
|
** |
|
= (object) original element |
|
\*/ |
|
elproto.resume = function (anim) { |
|
for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) { |
|
var e = animationElements[i]; |
|
if (eve("raphael.anim.resume." + this.id, this, e.anim) !== false) { |
|
delete e.paused; |
|
this.status(e.anim, e.status); |
|
} |
|
} |
|
return this; |
|
}; |
|
/*\ |
|
* Element.stop |
|
[ method ] |
|
** |
|
* Stops animation of the element. |
|
** |
|
> Parameters |
|
** |
|
- anim (object) #optional animation object |
|
** |
|
= (object) original element |
|
\*/ |
|
elproto.stop = function (anim) { |
|
for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.id == this.id && (!anim || animationElements[i].anim == anim)) { |
|
if (eve("raphael.anim.stop." + this.id, this, animationElements[i].anim) !== false) { |
|
animationElements.splice(i--, 1); |
|
} |
|
} |
|
return this; |
|
}; |
|
function stopAnimation(paper) { |
|
for (var i = 0; i < animationElements.length; i++) if (animationElements[i].el.paper == paper) { |
|
animationElements.splice(i--, 1); |
|
} |
|
} |
|
eve.on("raphael.remove", stopAnimation); |
|
eve.on("raphael.clear", stopAnimation); |
|
elproto.toString = function () { |
|
return "Rapha\xebl\u2019s object"; |
|
}; |
|
|
|
// Set |
|
var Set = function (items) { |
|
this.items = []; |
|
this.length = 0; |
|
this.type = "set"; |
|
if (items) { |
|
for (var i = 0, ii = items.length; i < ii; i++) { |
|
if (items[i] && (items[i].constructor == elproto.constructor || items[i].constructor == Set)) { |
|
this[this.items.length] = this.items[this.items.length] = items[i]; |
|
this.length++; |
|
} |
|
} |
|
} |
|
}, |
|
setproto = Set.prototype; |
|
/*\ |
|
* Set.push |
|
[ method ] |
|
** |
|
* Adds each argument to the current set. |
|
= (object) original element |
|
\*/ |
|
setproto.push = function () { |
|
var item, |
|
len; |
|
for (var i = 0, ii = arguments.length; i < ii; i++) { |
|
item = arguments[i]; |
|
if (item && (item.constructor == elproto.constructor || item.constructor == Set)) { |
|
len = this.items.length; |
|
this[len] = this.items[len] = item; |
|
this.length++; |
|
} |
|
} |
|
return this; |
|
}; |
|
/*\ |
|
* Set.pop |
|
[ method ] |
|
** |
|
* Removes last element and returns it. |
|
= (object) element |
|
\*/ |
|
setproto.pop = function () { |
|
this.length && delete this[this.length--]; |
|
return this.items.pop(); |
|
}; |
|
/*\ |
|
* Set.forEach |
|
[ method ] |
|
** |
|
* Executes given function for each element in the set. |
|
* |
|
* If function returns `false` it will stop loop running. |
|
** |
|
> Parameters |
|
** |
|
- callback (function) function to run |
|
- thisArg (object) context object for the callback |
|
= (object) Set object |
|
\*/ |
|
setproto.forEach = function (callback, thisArg) { |
|
for (var i = 0, ii = this.items.length; i < ii; i++) { |
|
if (callback.call(thisArg, this.items[i], i) === false) { |
|
return this; |
|
} |
|
} |
|
return this; |
|
}; |
|
for (var method in elproto) if (elproto[has](method)) { |
|
setproto[method] = (function (methodname) { |
|
return function () { |
|
var arg = arguments; |
|
return this.forEach(function (el) { |
|
el[methodname][apply](el, arg); |
|
}); |
|
}; |
|
})(method); |
|
} |
|
setproto.attr = function (name, value) { |
|
if (name && R.is(name, array) && R.is(name[0], "object")) { |
|
for (var j = 0, jj = name.length; j < jj; j++) { |
|
this.items[j].attr(name[j]); |
|
} |
|
} else { |
|
for (var i = 0, ii = this.items.length; i < ii; i++) { |
|
this.items[i].attr(name, value); |
|
} |
|
} |
|
return this; |
|
}; |
|
/*\ |
|
* Set.clear |
|
[ method ] |
|
** |
|
* Removes all elements from the set |
|
\*/ |
|
setproto.clear = function () { |
|
while (this.length) { |
|
this.pop(); |
|
} |
|
}; |
|
/*\ |
|
* Set.splice |
|
[ method ] |
|
** |
|
* Removes given element from the set |
|
** |
|
> Parameters |
|
** |
|
- index (number) position of the deletion |
|
- count (number) number of element to remove |
|
- insertion… (object) #optional elements to insert |
|
= (object) set elements that were deleted |
|
\*/ |
|
setproto.splice = function (index, count, insertion) { |
|
index = index < 0 ? mmax(this.length + index, 0) : index; |
|
count = mmax(0, mmin(this.length - index, count)); |
|
var tail = [], |
|
todel = [], |
|
args = [], |
|
i; |
|
for (i = 2; i < arguments.length; i++) { |
|
args.push(arguments[i]); |
|
} |
|
for (i = 0; i < count; i++) { |
|
todel.push(this[index + i]); |
|
} |
|
for (; i < this.length - index; i++) { |
|
tail.push(this[index + i]); |
|
} |
|
var arglen = args.length; |
|
for (i = 0; i < arglen + tail.length; i++) { |
|
this.items[index + i] = this[index + i] = i < arglen ? args[i] : tail[i - arglen]; |
|
} |
|
i = this.items.length = this.length -= count - arglen; |
|
while (this[i]) { |
|
delete this[i++]; |
|
} |
|
return new Set(todel); |
|
}; |
|
/*\ |
|
* Set.exclude |
|
[ method ] |
|
** |
|
* Removes given element from the set |
|
** |
|
> Parameters |
|
** |
|
- element (object) element to remove |
|
= (boolean) `true` if object was found & removed from the set |
|
\*/ |
|
setproto.exclude = function (el) { |
|
for (var i = 0, ii = this.length; i < ii; i++) if (this[i] == el) { |
|
this.splice(i, 1); |
|
return true; |
|
} |
|
}; |
|
setproto.animate = function (params, ms, easing, callback) { |
|
(R.is(easing, "function") || !easing) && (callback = easing || null); |
|
var len = this.items.length, |
|
i = len, |
|
item, |
|
set = this, |
|
collector; |
|
if (!len) { |
|
return this; |
|
} |
|
callback && (collector = function () { |
|
!--len && callback.call(set); |
|
}); |
|
easing = R.is(easing, string) ? easing : collector; |
|
var anim = R.animation(params, ms, easing, collector); |
|
item = this.items[--i].animate(anim); |
|
while (i--) { |
|
this.items[i] && !this.items[i].removed && this.items[i].animateWith(item, anim, anim); |
|
(this.items[i] && !this.items[i].removed) || len--; |
|
} |
|
return this; |
|
}; |
|
setproto.insertAfter = function (el) { |
|
var i = this.items.length; |
|
while (i--) { |
|
this.items[i].insertAfter(el); |
|
} |
|
return this; |
|
}; |
|
setproto.getBBox = function () { |
|
var x = [], |
|
y = [], |
|
x2 = [], |
|
y2 = []; |
|
for (var i = this.items.length; i--;) if (!this.items[i].removed) { |
|
var box = this.items[i].getBBox(); |
|
x.push(box.x); |
|
y.push(box.y); |
|
x2.push(box.x + box.width); |
|
y2.push(box.y + box.height); |
|
} |
|
x = mmin[apply](0, x); |
|
y = mmin[apply](0, y); |
|
x2 = mmax[apply](0, x2); |
|
y2 = mmax[apply](0, y2); |
|
return { |
|
x: x, |
|
y: y, |
|
x2: x2, |
|
y2: y2, |
|
width: x2 - x, |
|
height: y2 - y |
|
}; |
|
}; |
|
setproto.clone = function (s) { |
|
s = this.paper.set(); |
|
for (var i = 0, ii = this.items.length; i < ii; i++) { |
|
s.push(this.items[i].clone()); |
|
} |
|
return s; |
|
}; |
|
setproto.toString = function () { |
|
return "Rapha\xebl\u2018s set"; |
|
}; |
|
|
|
setproto.glow = function(glowConfig) { |
|
var ret = this.paper.set(); |
|
this.forEach(function(shape, index){ |
|
var g = shape.glow(glowConfig); |
|
if(g != null){ |
|
g.forEach(function(shape2, index2){ |
|
ret.push(shape2); |
|
}); |
|
} |
|
}); |
|
return ret; |
|
}; |
|
|
|
|
|
/*\ |
|
* Set.isPointInside |
|
[ method ] |
|
** |
|
* Determine if given point is inside this set’s elements |
|
** |
|
> Parameters |
|
** |
|
- x (number) x coordinate of the point |
|
- y (number) y coordinate of the point |
|
= (boolean) `true` if point is inside any of the set's elements |
|
\*/ |
|
setproto.isPointInside = function (x, y) { |
|
var isPointInside = false; |
|
this.forEach(function (el) { |
|
if (el.isPointInside(x, y)) { |
|
isPointInside = true; |
|
return false; // stop loop |
|
} |
|
}); |
|
return isPointInside; |
|
}; |
|
|
|
/*\ |
|
* Raphael.registerFont |
|
[ method ] |
|
** |
|
* Adds given font to the registered set of font for Raphaël. Should be used as an internal call from within Cufón’s font file. |
|
* Returns original parameter, so it could be used with chaining. |
|
# <a href="http://wiki.github.com/sorccu/cufon/about">More about Cufón and how to convert your font form TTF, OTF, etc to JavaScript file.</a> |
|
** |
|
> Parameters |
|
** |
|
- font (object) the font to register |
|
= (object) the font you passed in |
|
> Usage |
|
| Cufon.registerFont(Raphael.registerFont({…})); |
|
\*/ |
|
R.registerFont = function (font) { |
|
if (!font.face) { |
|
return font; |
|
} |
|
this.fonts = this.fonts || {}; |
|
var fontcopy = { |
|
w: font.w, |
|
face: {}, |
|
glyphs: {} |
|
}, |
|
family = font.face["font-family"]; |
|
for (var prop in font.face) if (font.face[has](prop)) { |
|
fontcopy.face[prop] = font.face[prop]; |
|
} |
|
if (this.fonts[family]) { |
|
this.fonts[family].push(fontcopy); |
|
} else { |
|
this.fonts[family] = [fontcopy]; |
|
} |
|
if (!font.svg) { |
|
fontcopy.face["units-per-em"] = toInt(font.face["units-per-em"], 10); |
|
for (var glyph in font.glyphs) if (font.glyphs[has](glyph)) { |
|
var path = font.glyphs[glyph]; |
|
fontcopy.glyphs[glyph] = { |
|
w: path.w, |
|
k: {}, |
|
d: path.d && "M" + path.d.replace(/[mlcxtrv]/g, function (command) { |
|
return {l: "L", c: "C", x: "z", t: "m", r: "l", v: "c"}[command] || "M"; |
|
}) + "z" |
|
}; |
|
if (path.k) { |
|
for (var k in path.k) if (path[has](k)) { |
|
fontcopy.glyphs[glyph].k[k] = path.k[k]; |
|
} |
|
} |
|
} |
|
} |
|
return font; |
|
}; |
|
/*\ |
|
* Paper.getFont |
|
[ method ] |
|
** |
|
* Finds font object in the registered font by given parameters. You could specify only one word from the font name, like “Myriad” for “Myriad Pro”. |
|
** |
|
> Parameters |
|
** |
|
- family (string) font family name or any word from it |
|
- weight (string) #optional font weight |
|
- style (string) #optional font style |
|
- stretch (string) #optional font stretch |
|
= (object) the font object |
|
> Usage |
|
| paper.print(100, 100, "Test string", paper.getFont("Times", 800), 30); |
|
\*/ |
|
paperproto.getFont = function (family, weight, style, stretch) { |
|
stretch = stretch || "normal"; |
|
style = style || "normal"; |
|
weight = +weight || {normal: 400, bold: 700, lighter: 300, bolder: 800}[weight] || 400; |
|
if (!R.fonts) { |
|
return; |
|
} |
|
var font = R.fonts[family]; |
|
if (!font) { |
|
var name = new RegExp("(^|\\s)" + family.replace(/[^\w\d\s+!~.:_-]/g, E) + "(\\s|$)", "i"); |
|
for (var fontName in R.fonts) if (R.fonts[has](fontName)) { |
|
if (name.test(fontName)) { |
|
font = R.fonts[fontName]; |
|
break; |
|
} |
|
} |
|
} |
|
var thefont; |
|
if (font) { |
|
for (var i = 0, ii = font.length; i < ii; i++) { |
|
thefont = font[i]; |
|
if (thefont.face["font-weight"] == weight && (thefont.face["font-style"] == style || !thefont.face["font-style"]) && thefont.face["font-stretch"] == stretch) { |
|
break; |
|
} |
|
} |
|
} |
|
return thefont; |
|
}; |
|
/*\ |
|
* Paper.print |
|
[ method ] |
|
** |
|
* Creates path that represent given text written using given font at given position with given size. |
|
* Result of the method is path element that contains whole text as a separate path. |
|
** |
|
> Parameters |
|
** |
|
- x (number) x position of the text |
|
- y (number) y position of the text |
|
- string (string) text to print |
|
- font (object) font object, see @Paper.getFont |
|
- size (number) #optional size of the font, default is `16` |
|
- origin (string) #optional could be `"baseline"` or `"middle"`, default is `"middle"` |
|
- letter_spacing (number) #optional number in range `-1..1`, default is `0` |
|
- line_spacing (number) #optional number in range `1..3`, default is `1` |
|
= (object) resulting path element, which consist of all letters |
|
> Usage |
|
| var txt = r.print(10, 50, "print", r.getFont("Museo"), 30).attr({fill: "#fff"}); |
|
\*/ |
|
paperproto.print = function (x, y, string, font, size, origin, letter_spacing, line_spacing) { |
|
origin = origin || "middle"; // baseline|middle |
|
letter_spacing = mmax(mmin(letter_spacing || 0, 1), -1); |
|
line_spacing = mmax(mmin(line_spacing || 1, 3), 1); |
|
var letters = Str(string)[split](E), |
|
shift = 0, |
|
notfirst = 0, |
|
path = E, |
|
scale; |
|
R.is(font, "string") && (font = this.getFont(font)); |
|
if (font) { |
|
scale = (size || 16) / font.face["units-per-em"]; |
|
var bb = font.face.bbox[split](separator), |
|
top = +bb[0], |
|
lineHeight = bb[3] - bb[1], |
|
shifty = 0, |
|
height = +bb[1] + (origin == "baseline" ? lineHeight + (+font.face.descent) : lineHeight / 2); |
|
for (var i = 0, ii = letters.length; i < ii; i++) { |
|
if (letters[i] == "\n") { |
|
shift = 0; |
|
curr = 0; |
|
notfirst = 0; |
|
shifty += lineHeight * line_spacing; |
|
} else { |
|
var prev = notfirst && font.glyphs[letters[i - 1]] || {}, |
|
curr = font.glyphs[letters[i]]; |
|
shift += notfirst ? (prev.w || font.w) + (prev.k && prev.k[letters[i]] || 0) + (font.w * letter_spacing) : 0; |
|
notfirst = 1; |
|
} |
|
if (curr && curr.d) { |
|
path += R.transformPath(curr.d, ["t", shift * scale, shifty * scale, "s", scale, scale, top, height, "t", (x - top) / scale, (y - height) / scale]); |
|
} |
|
} |
|
} |
|
return this.path(path).attr({ |
|
fill: "#000", |
|
stroke: "none" |
|
}); |
|
}; |
|
|
|
/*\ |
|
* Paper.add |
|
[ method ] |
|
** |
|
* Imports elements in JSON array in format `{type: type, <attributes>}` |
|
** |
|
> Parameters |
|
** |
|
- json (array) |
|
= (object) resulting set of imported elements |
|
> Usage |
|
| paper.add([ |
|
| { |
|
| type: "circle", |
|
| cx: 10, |
|
| cy: 10, |
|
| r: 5 |
|
| }, |
|
| { |
|
| type: "rect", |
|
| x: 10, |
|
| y: 10, |
|
| width: 10, |
|
| height: 10, |
|
| fill: "#fc0" |
|
| } |
|
| ]); |
|
\*/ |
|
paperproto.add = function (json) { |
|
if (R.is(json, "array")) { |
|
var res = this.set(), |
|
i = 0, |
|
ii = json.length, |
|
j; |
|
for (; i < ii; i++) { |
|
j = json[i] || {}; |
|
elements[has](j.type) && res.push(this[j.type]().attr(j)); |
|
} |
|
} |
|
return res; |
|
}; |
|
|
|
/*\ |
|
* Raphael.format |
|
[ method ] |
|
** |
|
* Simple format function. Replaces construction of type “`{<number>}`” to the corresponding argument. |
|
** |
|
> Parameters |
|
** |
|
- token (string) string to format |
|
- … (string) rest of arguments will be treated as parameters for replacement |
|
= (string) formated string |
|
> Usage |
|
| var x = 10, |
|
| y = 20, |
|
| width = 40, |
|
| height = 50; |
|
| // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z" |
|
| paper.path(Raphael.format("M{0},{1}h{2}v{3}h{4}z", x, y, width, height, -width)); |
|
\*/ |
|
R.format = function (token, params) { |
|
var args = R.is(params, array) ? [0][concat](params) : arguments; |
|
token && R.is(token, string) && args.length - 1 && (token = token.replace(formatrg, function (str, i) { |
|
return args[++i] == null ? E : args[i]; |
|
})); |
|
return token || E; |
|
}; |
|
/*\ |
|
* Raphael.fullfill |
|
[ method ] |
|
** |
|
* A little bit more advanced format function than @Raphael.format. Replaces construction of type “`{<name>}`” to the corresponding argument. |
|
** |
|
> Parameters |
|
** |
|
- token (string) string to format |
|
- json (object) object which properties will be used as a replacement |
|
= (string) formated string |
|
> Usage |
|
| // this will draw a rectangular shape equivalent to "M10,20h40v50h-40z" |
|
| paper.path(Raphael.fullfill("M{x},{y}h{dim.width}v{dim.height}h{dim['negative width']}z", { |
|
| x: 10, |
|
| y: 20, |
|
| dim: { |
|
| width: 40, |
|
| height: 50, |
|
| "negative width": -40 |
|
| } |
|
| })); |
|
\*/ |
|
R.fullfill = (function () { |
|
var tokenRegex = /\{([^\}]+)\}/g, |
|
objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g, // matches .xxxxx or ["xxxxx"] to run over object properties |
|
replacer = function (all, key, obj) { |
|
var res = obj; |
|
key.replace(objNotationRegex, function (all, name, quote, quotedName, isFunc) { |
|
name = name || quotedName; |
|
if (res) { |
|
if (name in res) { |
|
res = res[name]; |
|
} |
|
typeof res == "function" && isFunc && (res = res()); |
|
} |
|
}); |
|
res = (res == null || res == obj ? all : res) + ""; |
|
return res; |
|
}; |
|
return function (str, obj) { |
|
return String(str).replace(tokenRegex, function (all, key) { |
|
return replacer(all, key, obj); |
|
}); |
|
}; |
|
})(); |
|
/*\ |
|
* Raphael.ninja |
|
[ method ] |
|
** |
|
* If you want to leave no trace of Raphaël (Well, Raphaël creates only one global variable `Raphael`, but anyway.) You can use `ninja` method. |
|
* Beware, that in this case plugins could stop working, because they are depending on global variable existence. |
|
** |
|
= (object) Raphael object |
|
> Usage |
|
| (function (local_raphael) { |
|
| var paper = local_raphael(10, 10, 320, 200); |
|
| … |
|
| })(Raphael.ninja()); |
|
\*/ |
|
R.ninja = function () { |
|
oldRaphael.was ? (g.win.Raphael = oldRaphael.is) : delete Raphael; |
|
return R; |
|
}; |
|
/*\ |
|
* Raphael.st |
|
[ property (object) ] |
|
** |
|
* You can add your own method to elements and sets. It is wise to add a set method for each element method |
|
* you added, so you will be able to call the same method on sets too. |
|
** |
|
* See also @Raphael.el. |
|
> Usage |
|
| Raphael.el.red = function () { |
|
| this.attr({fill: "#f00"}); |
|
| }; |
|
| Raphael.st.red = function () { |
|
| this.forEach(function (el) { |
|
| el.red(); |
|
| }); |
|
| }; |
|
| // then use it |
|
| paper.set(paper.circle(100, 100, 20), paper.circle(110, 100, 20)).red(); |
|
\*/ |
|
R.st = setproto; |
|
|
|
eve.on("raphael.DOMload", function () { |
|
loaded = true; |
|
}); |
|
|
|
// Firefox <3.6 fix: http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html |
|
(function (doc, loaded, f) { |
|
if (doc.readyState == null && doc.addEventListener){ |
|
doc.addEventListener(loaded, f = function () { |
|
doc.removeEventListener(loaded, f, false); |
|
doc.readyState = "complete"; |
|
}, false); |
|
doc.readyState = "loading"; |
|
} |
|
function isLoaded() { |
|
(/in/).test(doc.readyState) ? setTimeout(isLoaded, 9) : R.eve("raphael.DOMload"); |
|
} |
|
isLoaded(); |
|
})(document, "DOMContentLoaded"); |
|
|
|
return R; |
|
})); |
|
|
|
// ┌─────────────────────────────────────────────────────────────────────┐ \\ |
|
// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\ |
|
// ├─────────────────────────────────────────────────────────────────────┤ \\ |
|
// │ SVG Module │ \\ |
|
// ├─────────────────────────────────────────────────────────────────────┤ \\ |
|
// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ |
|
// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\ |
|
// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\ |
|
// └─────────────────────────────────────────────────────────────────────┘ \\ |
|
|
|
(function (glob, factory) { |
|
if (typeof define === "function" && define.amd) { |
|
define("raphael.svg", ["raphael.core"], function(raphael) { |
|
return factory(raphael); |
|
}); |
|
} else if (typeof exports === "object") { |
|
factory(require("raphael.core")); |
|
} else { |
|
factory(glob.Raphael); |
|
} |
|
}(this, function(R) { |
|
if (R && !R.svg) { |
|
return; |
|
} |
|
|
|
var has = "hasOwnProperty", |
|
Str = String, |
|
toFloat = parseFloat, |
|
toInt = parseInt, |
|
math = Math, |
|
mmax = math.max, |
|
abs = math.abs, |
|
pow = math.pow, |
|
separator = /[, ]+/, |
|
eve = R.eve, |
|
E = "", |
|
S = " "; |
|
var xlink = "http://www.w3.org/1999/xlink", |
|
markers = { |
|
block: "M5,0 0,2.5 5,5z", |
|
classic: "M5,0 0,2.5 5,5 3.5,3 3.5,2z", |
|
diamond: "M2.5,0 5,2.5 2.5,5 0,2.5z", |
|
open: "M6,1 1,3.5 6,6", |
|
oval: "M2.5,0A2.5,2.5,0,0,1,2.5,5 2.5,2.5,0,0,1,2.5,0z" |
|
}, |
|
markerCounter = {}; |
|
R.toString = function () { |
|
return "Your browser supports SVG.\nYou are running Rapha\xebl " + this.version; |
|
}; |
|
var $ = function (el, attr) { |
|
if (attr) { |
|
if (typeof el == "string") { |
|
el = $(el); |
|
} |
|
for (var key in attr) if (attr[has](key)) { |
|
if (key.substring(0, 6) == "xlink:") { |
|
el.setAttributeNS(xlink, key.substring(6), Str(attr[key])); |
|
} else { |
|
el.setAttribute(key, Str(attr[key])); |
|
} |
|
} |
|
} else { |
|
el = R._g.doc.createElementNS("http://www.w3.org/2000/svg", el); |
|
el.style && (el.style.webkitTapHighlightColor = "rgba(0,0,0,0)"); |
|
} |
|
return el; |
|
}, |
|
addGradientFill = function (element, gradient) { |
|
var type = "linear", |
|
id = element.id + gradient, |
|
fx = .5, fy = .5, |
|
o = element.node, |
|
SVG = element.paper, |
|
s = o.style, |
|
el = R._g.doc.getElementById(id); |
|
if (!el) { |
|
gradient = Str(gradient).replace(R._radial_gradient, function (all, _fx, _fy) { |
|
type = "radial"; |
|
if (_fx && _fy) { |
|
fx = toFloat(_fx); |
|
fy = toFloat(_fy); |
|
var dir = ((fy > .5) * 2 - 1); |
|
pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && |
|
(fy = math.sqrt(.25 - pow(fx - .5, 2)) * dir + .5) && |
|
fy != .5 && |
|
(fy = fy.toFixed(5) - 1e-5 * dir); |
|
} |
|
return E; |
|
}); |
|
gradient = gradient.split(/\s*\-\s*/); |
|
if (type == "linear") { |
|
var angle = gradient.shift(); |
|
angle = -toFloat(angle); |
|
if (isNaN(angle)) { |
|
return null; |
|
} |
|
var vector = [0, 0, math.cos(R.rad(angle)), math.sin(R.rad(angle))], |
|
max = 1 / (mmax(abs(vector[2]), abs(vector[3])) || 1); |
|
vector[2] *= max; |
|
vector[3] *= max; |
|
if (vector[2] < 0) { |
|
vector[0] = -vector[2]; |
|
vector[2] = 0; |
|
} |
|
if (vector[3] < 0) { |
|
vector[1] = -vector[3]; |
|
vector[3] = 0; |
|
} |
|
} |
|
var dots = R._parseDots(gradient); |
|
if (!dots) { |
|
return null; |
|
} |
|
id = id.replace(/[\(\)\s,\xb0#]/g, "_"); |
|
|
|
if (element.gradient && id != element.gradient.id) { |
|
SVG.defs.removeChild(element.gradient); |
|
delete element.gradient; |
|
} |
|
|
|
if (!element.gradient) { |
|
el = $(type + "Gradient", {id: id}); |
|
element.gradient = el; |
|
$(el, type == "radial" ? { |
|
fx: fx, |
|
fy: fy |
|
} : { |
|
x1: vector[0], |
|
y1: vector[1], |
|
x2: vector[2], |
|
y2: vector[3], |
|
gradientTransform: element.matrix.invert() |
|
}); |
|
SVG.defs.appendChild(el); |
|
for (var i = 0, ii = dots.length; i < ii; i++) { |
|
el.appendChild($("stop", { |
|
offset: dots[i].offset ? dots[i].offset : i ? "100%" : "0%", |
|
"stop-color": dots[i].color || "#fff", |
|
"stop-opacity": isFinite(dots[i].opacity) ? dots[i].opacity : 1 |
|
})); |
|
} |
|
} |
|
} |
|
$(o, { |
|
fill: "url('" + document.location.origin + document.location.pathname + "#" + id + "')", |
|
opacity: 1, |
|
"fill-opacity": 1 |
|
}); |
|
s.fill = E; |
|
s.opacity = 1; |
|
s.fillOpacity = 1; |
|
return 1; |
|
}, |
|
updatePosition = function (o) { |
|
var bbox = o.getBBox(1); |
|
$(o.pattern, {patternTransform: o.matrix.invert() + " translate(" + bbox.x + "," + bbox.y + ")"}); |
|
}, |
|
addArrow = function (o, value, isEnd) { |
|
if (o.type == "path") { |
|
var values = Str(value).toLowerCase().split("-"), |
|
p = o.paper, |
|
se = isEnd ? "end" : "start", |
|
node = o.node, |
|
attrs = o.attrs, |
|
stroke = attrs["stroke-width"], |
|
i = values.length, |
|
type = "classic", |
|
from, |
|
to, |
|
dx, |
|
refX, |
|
attr, |
|
w = 3, |
|
h = 3, |
|
t = 5; |
|
while (i--) { |
|
switch (values[i]) { |
|
case "block": |
|
case "classic": |
|
case "oval": |
|
case "diamond": |
|
case "open": |
|
case "none": |
|
type = values[i]; |
|
break; |
|
case "wide": h = 5; break; |
|
case "narrow": h = 2; break; |
|
case "long": w = 5; break; |
|
case "short": w = 2; break; |
|
} |
|
} |
|
if (type == "open") { |
|
w += 2; |
|
h += 2; |
|
t += 2; |
|
dx = 1; |
|
refX = isEnd ? 4 : 1; |
|
attr = { |
|
fill: "none", |
|
stroke: attrs.stroke |
|
}; |
|
} else { |
|
refX = dx = w / 2; |
|
attr = { |
|
fill: attrs.stroke, |
|
stroke: "none" |
|
}; |
|
} |
|
if (o._.arrows) { |
|
if (isEnd) { |
|
o._.arrows.endPath && markerCounter[o._.arrows.endPath]--; |
|
o._.arrows.endMarker && markerCounter[o._.arrows.endMarker]--; |
|
} else { |
|
o._.arrows.startPath && markerCounter[o._.arrows.startPath]--; |
|
o._.arrows.startMarker && markerCounter[o._.arrows.startMarker]--; |
|
} |
|
} else { |
|
o._.arrows = {}; |
|
} |
|
if (type != "none") { |
|
var pathId = "raphael-marker-" + type, |
|
markerId = "raphael-marker-" + se + type + w + h + "-obj" + o.id; |
|
if (!R._g.doc.getElementById(pathId)) { |
|
p.defs.appendChild($($("path"), { |
|
"stroke-linecap": "round", |
|
d: markers[type], |
|
id: pathId |
|
})); |
|
markerCounter[pathId] = 1; |
|
} else { |
|
markerCounter[pathId]++; |
|
} |
|
var marker = R._g.doc.getElementById(markerId), |
|
use; |
|
if (!marker) { |
|
marker = $($("marker"), { |
|
id: markerId, |
|
markerHeight: h, |
|
markerWidth: w, |
|
orient: "auto", |
|
refX: refX, |
|
refY: h / 2 |
|
}); |
|
use = $($("use"), { |
|
"xlink:href": "#" + pathId, |
|
transform: (isEnd ? "rotate(180 " + w / 2 + " " + h / 2 + ") " : E) + "scale(" + w / t + "," + h / t + ")", |
|
"stroke-width": (1 / ((w / t + h / t) / 2)).toFixed(4) |
|
}); |
|
marker.appendChild(use); |
|
p.defs.appendChild(marker); |
|
markerCounter[markerId] = 1; |
|
} else { |
|
markerCounter[markerId]++; |
|
use = marker.getElementsByTagName("use")[0]; |
|
} |
|
$(use, attr); |
|
var delta = dx * (type != "diamond" && type != "oval"); |
|
if (isEnd) { |
|
from = o._.arrows.startdx * stroke || 0; |
|
to = R.getTotalLength(attrs.path) - delta * stroke; |
|
} else { |
|
from = delta * stroke; |
|
to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0); |
|
} |
|
attr = {}; |
|
attr["marker-" + se] = "url(#" + markerId + ")"; |
|
if (to || from) { |
|
attr.d = R.getSubpath(attrs.path, from, to); |
|
} |
|
$(node, attr); |
|
o._.arrows[se + "Path"] = pathId; |
|
o._.arrows[se + "Marker"] = markerId; |
|
o._.arrows[se + "dx"] = delta; |
|
o._.arrows[se + "Type"] = type; |
|
o._.arrows[se + "String"] = value; |
|
} else { |
|
if (isEnd) { |
|
from = o._.arrows.startdx * stroke || 0; |
|
to = R.getTotalLength(attrs.path) - from; |
|
} else { |
|
from = 0; |
|
to = R.getTotalLength(attrs.path) - (o._.arrows.enddx * stroke || 0); |
|
} |
|
o._.arrows[se + "Path"] && $(node, {d: R.getSubpath(attrs.path, from, to)}); |
|
delete o._.arrows[se + "Path"]; |
|
delete o._.arrows[se + "Marker"]; |
|
delete o._.arrows[se + "dx"]; |
|
delete o._.arrows[se + "Type"]; |
|
delete o._.arrows[se + "String"]; |
|
} |
|
for (attr in markerCounter) if (markerCounter[has](attr) && !markerCounter[attr]) { |
|
var item = R._g.doc.getElementById(attr); |
|
item && item.parentNode.removeChild(item); |
|
} |
|
} |
|
}, |
|
dasharray = { |
|
"-": [3, 1], |
|
".": [1, 1], |
|
"-.": [3, 1, 1, 1], |
|
"-..": [3, 1, 1, 1, 1, 1], |
|
". ": [1, 3], |
|
"- ": [4, 3], |
|
"--": [8, 3], |
|
"- .": [4, 3, 1, 3], |
|
"--.": [8, 3, 1, 3], |
|
"--..": [8, 3, 1, 3, 1, 3] |
|
}, |
|
addDashes = function (o, value, params) { |
|
value = dasharray[Str(value).toLowerCase()]; |
|
if (value) { |
|
var width = o.attrs["stroke-width"] || "1", |
|
butt = {round: width, square: width, butt: 0}[o.attrs["stroke-linecap"] || params["stroke-linecap"]] || 0, |
|
dashes = [], |
|
i = value.length; |
|
while (i--) { |
|
dashes[i] = value[i] * width + ((i % 2) ? 1 : -1) * butt; |
|
} |
|
$(o.node, {"stroke-dasharray": dashes.join(",")}); |
|
} |
|
else { |
|
$(o.node, {"stroke-dasharray": "none"}); |
|
} |
|
}, |
|
setFillAndStroke = function (o, params) { |
|
var node = o.node, |
|
attrs = o.attrs, |
|
vis = node.style.visibility; |
|
node.style.visibility = "hidden"; |
|
for (var att in params) { |
|
if (params[has](att)) { |
|
if (!R._availableAttrs[has](att)) { |
|
continue; |
|
} |
|
var value = params[att]; |
|
attrs[att] = value; |
|
switch (att) { |
|
case "blur": |
|
o.blur(value); |
|
break; |
|
case "title": |
|
var title = node.getElementsByTagName("title"); |
|
|
|
// Use the existing <title>. |
|
if (title.length && (title = title[0])) { |
|
title.firstChild.nodeValue = value; |
|
} else { |
|
title = $("title"); |
|
var val = R._g.doc.createTextNode(value); |
|
title.appendChild(val); |
|
node.appendChild(title); |
|
} |
|
break; |
|
case "href": |
|
case "target": |
|
var pn = node.parentNode; |
|
if (pn.tagName.toLowerCase() != "a") { |
|
var hl = $("a"); |
|
pn.insertBefore(hl, node); |
|
hl.appendChild(node); |
|
pn = hl; |
|
} |
|
if (att == "target") { |
|
pn.setAttributeNS(xlink, "show", value == "blank" ? "new" : value); |
|
} else { |
|
pn.setAttributeNS(xlink, att, value); |
|
} |
|
break; |
|
case "cursor": |
|
node.style.cursor = value; |
|
break; |
|
case "transform": |
|
o.transform(value); |
|
break; |
|
case "arrow-start": |
|
addArrow(o, value); |
|
break; |
|
case "arrow-end": |
|
addArrow(o, value, 1); |
|
break; |
|
case "clip-rect": |
|
var rect = Str(value).split(separator); |
|
if (rect.length == 4) { |
|
o.clip && o.clip.parentNode.parentNode.removeChild(o.clip.parentNode); |
|
var el = $("clipPath"), |
|
rc = $("rect"); |
|
el.id = R.createUUID(); |
|
$(rc, { |
|
x: rect[0], |
|
y: rect[1], |
|
width: rect[2], |
|
height: rect[3] |
|
}); |
|
el.appendChild(rc); |
|
o.paper.defs.appendChild(el); |
|
$(node, {"clip-path": "url(#" + el.id + ")"}); |
|
o.clip = rc; |
|
} |
|
if (!value) { |
|
var path = node.getAttribute("clip-path"); |
|
if (path) { |
|
var clip = R._g.doc.getElementById(path.replace(/(^url\(#|\)$)/g, E)); |
|
clip && clip.parentNode.removeChild(clip); |
|
$(node, {"clip-path": E}); |
|
delete o.clip; |
|
} |
|
} |
|
break; |
|
case "path": |
|
if (o.type == "path") { |
|
$(node, {d: value ? attrs.path = R._pathToAbsolute(value) : "M0,0"}); |
|
o._.dirty = 1; |
|
if (o._.arrows) { |
|
"startString" in o._.arrows && addArrow(o, o._.arrows.startString); |
|
"endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1); |
|
} |
|
} |
|
break; |
|
case "width": |
|
node.setAttribute(att, value); |
|
o._.dirty = 1; |
|
if (attrs.fx) { |
|
att = "x"; |
|
value = attrs.x; |
|
} else { |
|
break; |
|
} |
|
case "x": |
|
if (attrs.fx) { |
|
value = -attrs.x - (attrs.width || 0); |
|
} |
|
case "rx": |
|
if (att == "rx" && o.type == "rect") { |
|
break; |
|
} |
|
case "cx": |
|
node.setAttribute(att, value); |
|
o.pattern && updatePosition(o); |
|
o._.dirty = 1; |
|
break; |
|
case "height": |
|
node.setAttribute(att, value); |
|
o._.dirty = 1; |
|
if (attrs.fy) { |
|
att = "y"; |
|
value = attrs.y; |
|
} else { |
|
break; |
|
} |
|
case "y": |
|
if (attrs.fy) { |
|
value = -attrs.y - (attrs.height || 0); |
|
} |
|
case "ry": |
|
if (att == "ry" && o.type == "rect") { |
|
break; |
|
} |
|
case "cy": |
|
node.setAttribute(att, value); |
|
o.pattern && updatePosition(o); |
|
o._.dirty = 1; |
|
break; |
|
case "r": |
|
if (o.type == "rect") { |
|
$(node, {rx: value, ry: value}); |
|
} else { |
|
node.setAttribute(att, value); |
|
} |
|
o._.dirty = 1; |
|
break; |
|
case "src": |
|
if (o.type == "image") { |
|
node.setAttributeNS(xlink, "href", value); |
|
} |
|
break; |
|
case "stroke-width": |
|
if (o._.sx != 1 || o._.sy != 1) { |
|
value /= mmax(abs(o._.sx), abs(o._.sy)) || 1; |
|
} |
|
node.setAttribute(att, value); |
|
if (attrs["stroke-dasharray"]) { |
|
addDashes(o, attrs["stroke-dasharray"], params); |
|
} |
|
if (o._.arrows) { |
|
"startString" in o._.arrows && addArrow(o, o._.arrows.startString); |
|
"endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1); |
|
} |
|
break; |
|
case "stroke-dasharray": |
|
addDashes(o, value, params); |
|
break; |
|
case "fill": |
|
var isURL = Str(value).match(R._ISURL); |
|
if (isURL) { |
|
el = $("pattern"); |
|
var ig = $("image"); |
|
el.id = R.createUUID(); |
|
$(el, {x: 0, y: 0, patternUnits: "userSpaceOnUse", height: 1, width: 1}); |
|
$(ig, {x: 0, y: 0, "xlink:href": isURL[1]}); |
|
el.appendChild(ig); |
|
|
|
(function (el) { |
|
R._preload(isURL[1], function () { |
|
var w = this.offsetWidth, |
|
h = this.offsetHeight; |
|
$(el, {width: w, height: h}); |
|
$(ig, {width: w, height: h}); |
|
}); |
|
})(el); |
|
o.paper.defs.appendChild(el); |
|
$(node, {fill: "url(#" + el.id + ")"}); |
|
o.pattern = el; |
|
o.pattern && updatePosition(o); |
|
break; |
|
} |
|
var clr = R.getRGB(value); |
|
if (!clr.error) { |
|
delete params.gradient; |
|
delete attrs.gradient; |
|
!R.is(attrs.opacity, "undefined") && |
|
R.is(params.opacity, "undefined") && |
|
$(node, {opacity: attrs.opacity}); |
|
!R.is(attrs["fill-opacity"], "undefined") && |
|
R.is(params["fill-opacity"], "undefined") && |
|
$(node, {"fill-opacity": attrs["fill-opacity"]}); |
|
} else if ((o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value)) { |
|
if ("opacity" in attrs || "fill-opacity" in attrs) { |
|
var gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E)); |
|
if (gradient) { |
|
var stops = gradient.getElementsByTagName("stop"); |
|
$(stops[stops.length - 1], {"stop-opacity": ("opacity" in attrs ? attrs.opacity : 1) * ("fill-opacity" in attrs ? attrs["fill-opacity"] : 1)}); |
|
} |
|
} |
|
attrs.gradient = value; |
|
attrs.fill = "none"; |
|
break; |
|
} |
|
clr[has]("opacity") && $(node, {"fill-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity}); |
|
case "stroke": |
|
clr = R.getRGB(value); |
|
node.setAttribute(att, clr.hex); |
|
att == "stroke" && clr[has]("opacity") && $(node, {"stroke-opacity": clr.opacity > 1 ? clr.opacity / 100 : clr.opacity}); |
|
if (att == "stroke" && o._.arrows) { |
|
"startString" in o._.arrows && addArrow(o, o._.arrows.startString); |
|
"endString" in o._.arrows && addArrow(o, o._.arrows.endString, 1); |
|
} |
|
break; |
|
case "gradient": |
|
(o.type == "circle" || o.type == "ellipse" || Str(value).charAt() != "r") && addGradientFill(o, value); |
|
break; |
|
case "opacity": |
|
if (attrs.gradient && !attrs[has]("stroke-opacity")) { |
|
$(node, {"stroke-opacity": value > 1 ? value / 100 : value}); |
|
} |
|
// fall |
|
case "fill-opacity": |
|
if (attrs.gradient) { |
|
gradient = R._g.doc.getElementById(node.getAttribute("fill").replace(/^url\(#|\)$/g, E)); |
|
if (gradient) { |
|
stops = gradient.getElementsByTagName("stop"); |
|
$(stops[stops.length - 1], {"stop-opacity": value}); |
|
} |
|
break; |
|
} |
|
default: |
|
att == "font-size" && (value = toInt(value, 10) + "px"); |
|
var cssrule = att.replace(/(\-.)/g, function (w) { |
|
return w.substring(1).toUpperCase(); |
|
}); |
|
node.style[cssrule] = value; |
|
o._.dirty = 1; |
|
node.setAttribute(att, value); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
tuneText(o, params); |
|
node.style.visibility = vis; |
|
}, |
|
leading = 1.2, |
|
tuneText = function (el, params) { |
|
if (el.type != "text" || !(params[has]("text") || params[has]("font") || params[has]("font-size") || params[has]("x") || params[has]("y"))) { |
|
return; |
|
} |
|
var a = el.attrs, |
|
node = el.node, |
|
fontSize = node.firstChild ? toInt(R._g.doc.defaultView.getComputedStyle(node.firstChild, E).getPropertyValue("font-size"), 10) : 10; |
|
|
|
if (params[has]("text")) { |
|
a.text = params.text; |
|
while (node.firstChild) { |
|
node.removeChild(node.firstChild); |
|
} |
|
var texts = Str(params.text).split("\n"), |
|
tspans = [], |
|
tspan; |
|
for (var i = 0, ii = texts.length; i < ii; i++) { |
|
tspan = $("tspan"); |
|
i && $(tspan, {dy: fontSize * leading, x: a.x}); |
|
tspan.appendChild(R._g.doc.createTextNode(texts[i])); |
|
node.appendChild(tspan); |
|
tspans[i] = tspan; |
|
} |
|
} else { |
|
tspans = node.getElementsByTagName("tspan"); |
|
for (i = 0, ii = tspans.length; i < ii; i++) if (i) { |
|
$(tspans[i], {dy: fontSize * leading, x: a.x}); |
|
} else { |
|
$(tspans[0], {dy: 0}); |
|
} |
|
} |
|
$(node, {x: a.x, y: a.y}); |
|
el._.dirty = 1; |
|
var bb = el._getBBox(), |
|
dif = a.y - (bb.y + bb.height / 2); |
|
dif && R.is(dif, "finite") && $(tspans[0], {dy: dif}); |
|
}, |
|
getRealNode = function (node) { |
|
if (node.parentNode && node.parentNode.tagName.toLowerCase() === "a") { |
|
return node.parentNode; |
|
} else { |
|
return node; |
|
} |
|
}, |
|
Element = function (node, svg) { |
|
var X = 0, |
|
Y = 0; |
|
/*\ |
|
* Element.node |
|
[ property (object) ] |
|
** |
|
* Gives you a reference to the DOM object, so you can assign event handlers or just mess around. |
|
** |
|
* Note: Don’t mess with it. |
|
> Usage |
|
| // draw a circle at coordinate 10,10 with radius of 10 |
|
| var c = paper.circle(10, 10, 10); |
|
| c.node.onclick = function () { |
|
| c.attr("fill", "red"); |
|
| }; |
|
\*/ |
|
this[0] = this.node = node; |
|
/*\ |
|
* Element.raphael |
|
[ property (object) ] |
|
** |
|
* Internal reference to @Raphael object. In case it is not available. |
|
> Usage |
|
| Raphael.el.red = function () { |
|
| var hsb = this.paper.raphael.rgb2hsb(this.attr("fill")); |
|
| hsb.h = 1; |
|
| this.attr({fill: this.paper.raphael.hsb2rgb(hsb).hex}); |
|
| } |
|
\*/ |
|
node.raphael = true; |
|
/*\ |
|
* Element.id |
|
[ property (number) ] |
|
** |
|
* Unique id of the element. Especially useful when you want to listen to events of the element, |
|
* because all events are fired in format `<module>.<action>.<id>`. Also useful for @Paper.getById method. |
|
\*/ |
|
this.id = R._oid++; |
|
node.raphaelid = this.id; |
|
this.matrix = R.matrix(); |
|
this.realPath = null; |
|
/*\ |
|
* Element.paper |
|
[ property (object) ] |
|
** |
|
* Internal reference to “paper” where object drawn. Mainly for use in plugins and element extensions. |
|
> Usage |
|
| Raphael.el.cross = function () { |
|
| this.attr({fill: "red"}); |
|
| this.paper.path("M10,10L50,50M50,10L10,50") |
|
| .attr({stroke: "red"}); |
|
| } |
|
\*/ |
|
this.paper = svg; |
|
this.attrs = this.attrs || {}; |
|
this._ = { |
|
transform: [], |
|
sx: 1, |
|
sy: 1, |
|
deg: 0, |
|
dx: 0, |
|
dy: 0, |
|
dirty: 1 |
|
}; |
|
!svg.bottom && (svg.bottom = this); |
|
/*\ |
|
* Element.prev |
|
[ property (object) ] |
|
** |
|
* Reference to the previous element in the hierarchy. |
|
\*/ |
|
this.prev = svg.top; |
|
svg.top && (svg.top.next = this); |
|
svg.top = this; |
|
/*\ |
|
* Element.next |
|
[ property (object) ] |
|
** |
|
* Reference to the next element in the hierarchy. |
|
\*/ |
|
this.next = null; |
|
}, |
|
elproto = R.el; |
|
|
|
Element.prototype = elproto; |
|
elproto.constructor = Element; |
|
|
|
R._engine.path = function (pathString, SVG) { |
|
var el = $("path"); |
|
SVG.canvas && SVG.canvas.appendChild(el); |
|
var p = new Element(el, SVG); |
|
p.type = "path"; |
|
setFillAndStroke(p, { |
|
fill: "none", |
|
stroke: "#000", |
|
path: pathString |
|
}); |
|
return p; |
|
}; |
|
/*\ |
|
* Element.rotate |
|
[ method ] |
|
** |
|
* Deprecated! Use @Element.transform instead. |
|
* Adds rotation by given angle around given point to the list of |
|
* transformations of the element. |
|
> Parameters |
|
- deg (number) angle in degrees |
|
- cx (number) #optional x coordinate of the centre of rotation |
|
- cy (number) #optional y coordinate of the centre of rotation |
|
* If cx & cy aren’t specified centre of the shape is used as a point of rotation. |
|
= (object) @Element |
|
\*/ |
|
elproto.rotate = function (deg, cx, cy) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
deg = Str(deg).split(separator); |
|
if (deg.length - 1) { |
|
cx = toFloat(deg[1]); |
|
cy = toFloat(deg[2]); |
|
} |
|
deg = toFloat(deg[0]); |
|
(cy == null) && (cx = cy); |
|
if (cx == null || cy == null) { |
|
var bbox = this.getBBox(1); |
|
cx = bbox.x + bbox.width / 2; |
|
cy = bbox.y + bbox.height / 2; |
|
} |
|
this.transform(this._.transform.concat([["r", deg, cx, cy]])); |
|
return this; |
|
}; |
|
/*\ |
|
* Element.scale |
|
[ method ] |
|
** |
|
* Deprecated! Use @Element.transform instead. |
|
* Adds scale by given amount relative to given point to the list of |
|
* transformations of the element. |
|
> Parameters |
|
- sx (number) horisontal scale amount |
|
- sy (number) vertical scale amount |
|
- cx (number) #optional x coordinate of the centre of scale |
|
- cy (number) #optional y coordinate of the centre of scale |
|
* If cx & cy aren’t specified centre of the shape is used instead. |
|
= (object) @Element |
|
\*/ |
|
elproto.scale = function (sx, sy, cx, cy) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
sx = Str(sx).split(separator); |
|
if (sx.length - 1) { |
|
sy = toFloat(sx[1]); |
|
cx = toFloat(sx[2]); |
|
cy = toFloat(sx[3]); |
|
} |
|
sx = toFloat(sx[0]); |
|
(sy == null) && (sy = sx); |
|
(cy == null) && (cx = cy); |
|
if (cx == null || cy == null) { |
|
var bbox = this.getBBox(1); |
|
} |
|
cx = cx == null ? bbox.x + bbox.width / 2 : cx; |
|
cy = cy == null ? bbox.y + bbox.height / 2 : cy; |
|
this.transform(this._.transform.concat([["s", sx, sy, cx, cy]])); |
|
return this; |
|
}; |
|
/*\ |
|
* Element.translate |
|
[ method ] |
|
** |
|
* Deprecated! Use @Element.transform instead. |
|
* Adds translation by given amount to the list of transformations of the element. |
|
> Parameters |
|
- dx (number) horisontal shift |
|
- dy (number) vertical shift |
|
= (object) @Element |
|
\*/ |
|
elproto.translate = function (dx, dy) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
dx = Str(dx).split(separator); |
|
if (dx.length - 1) { |
|
dy = toFloat(dx[1]); |
|
} |
|
dx = toFloat(dx[0]) || 0; |
|
dy = +dy || 0; |
|
this.transform(this._.transform.concat([["t", dx, dy]])); |
|
return this; |
|
}; |
|
/*\ |
|
* Element.transform |
|
[ method ] |
|
** |
|
* Adds transformation to the element which is separate to other attributes, |
|
* i.e. translation doesn’t change `x` or `y` of the rectange. The format |
|
* of transformation string is similar to the path string syntax: |
|
| "t100,100r30,100,100s2,2,100,100r45s1.5" |
|
* Each letter is a command. There are four commands: `t` is for translate, `r` is for rotate, `s` is for |
|
* scale and `m` is for matrix. |
|
* |
|
* There are also alternative “absolute” translation, rotation and scale: `T`, `R` and `S`. They will not take previous transformation into account. For example, `...T100,0` will always move element 100 px horisontally, while `...t100,0` could move it vertically if there is `r90` before. Just compare results of `r90t100,0` and `r90T100,0`. |
|
* |
|
* So, the example line above could be read like “translate by 100, 100; rotate 30° around 100, 100; scale twice around 100, 100; |
|
* rotate 45° around centre; scale 1.5 times relative to centre”. As you can see rotate and scale commands have origin |
|
* coordinates as optional parameters, the default is the centre point of the element. |
|
* Matrix accepts six parameters. |
|
> Usage |
|
| var el = paper.rect(10, 20, 300, 200); |
|
| // translate 100, 100, rotate 45°, translate -100, 0 |
|
| el.transform("t100,100r45t-100,0"); |
|
| // if you want you can append or prepend transformations |
|
| el.transform("...t50,50"); |
|
| el.transform("s2..."); |
|
| // or even wrap |
|
| el.transform("t50,50...t-50-50"); |
|
| // to reset transformation call method with empty string |
|
| el.transform(""); |
|
| // to get current value call it without parameters |
|
| console.log(el.transform()); |
|
> Parameters |
|
- tstr (string) #optional transformation string |
|
* If tstr isn’t specified |
|
= (string) current transformation string |
|
* else |
|
= (object) @Element |
|
\*/ |
|
elproto.transform = function (tstr) { |
|
var _ = this._; |
|
if (tstr == null) { |
|
return _.transform; |
|
} |
|
R._extractTransform(this, tstr); |
|
|
|
this.clip && $(this.clip, {transform: this.matrix.invert()}); |
|
this.pattern && updatePosition(this); |
|
this.node && $(this.node, {transform: this.matrix}); |
|
|
|
if (_.sx != 1 || _.sy != 1) { |
|
var sw = this.attrs[has]("stroke-width") ? this.attrs["stroke-width"] : 1; |
|
this.attr({"stroke-width": sw}); |
|
} |
|
|
|
return this; |
|
}; |
|
/*\ |
|
* Element.hide |
|
[ method ] |
|
** |
|
* Makes element invisible. See @Element.show. |
|
= (object) @Element |
|
\*/ |
|
elproto.hide = function () { |
|
if(!this.removed) this.node.style.display = "none"; |
|
return this; |
|
}; |
|
/*\ |
|
* Element.show |
|
[ method ] |
|
** |
|
* Makes element visible. See @Element.hide. |
|
= (object) @Element |
|
\*/ |
|
elproto.show = function () { |
|
if(!this.removed) this.node.style.display = ""; |
|
return this; |
|
}; |
|
/*\ |
|
* Element.remove |
|
[ method ] |
|
** |
|
* Removes element from the paper. |
|
\*/ |
|
elproto.remove = function () { |
|
var node = getRealNode(this.node); |
|
if (this.removed || !node.parentNode) { |
|
return; |
|
} |
|
var paper = this.paper; |
|
paper.__set__ && paper.__set__.exclude(this); |
|
eve.unbind("raphael.*.*." + this.id); |
|
if (this.gradient) { |
|
paper.defs.removeChild(this.gradient); |
|
} |
|
R._tear(this, paper); |
|
|
|
node.parentNode.removeChild(node); |
|
|
|
// Remove custom data for element |
|
this.removeData(); |
|
|
|
for (var i in this) { |
|
this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null; |
|
} |
|
this.removed = true; |
|
}; |
|
elproto._getBBox = function () { |
|
if (this.node.style.display == "none") { |
|
this.show(); |
|
var hide = true; |
|
} |
|
var canvasHidden = false, |
|
containerStyle; |
|
if (this.paper.canvas.parentElement) { |
|
containerStyle = this.paper.canvas.parentElement.style; |
|
} //IE10+ can't find parentElement |
|
else if (this.paper.canvas.parentNode) { |
|
containerStyle = this.paper.canvas.parentNode.style; |
|
} |
|
|
|
if(containerStyle && containerStyle.display == "none") { |
|
canvasHidden = true; |
|
containerStyle.display = ""; |
|
} |
|
var bbox = {}; |
|
try { |
|
bbox = this.node.getBBox(); |
|
} catch(e) { |
|
// Firefox 3.0.x, 25.0.1 (probably more versions affected) play badly here - possible fix |
|
bbox = { |
|
x: this.node.clientLeft, |
|
y: this.node.clientTop, |
|
width: this.node.clientWidth, |
|
height: this.node.clientHeight |
|
} |
|
} finally { |
|
bbox = bbox || {}; |
|
if(canvasHidden){ |
|
containerStyle.display = "none"; |
|
} |
|
} |
|
hide && this.hide(); |
|
return bbox; |
|
}; |
|
/*\ |
|
* Element.attr |
|
[ method ] |
|
** |
|
* Sets the attributes of the element. |
|
> Parameters |
|
- attrName (string) attribute’s name |
|
- value (string) value |
|
* or |
|
- params (object) object of name/value pairs |
|
* or |
|
- attrName (string) attribute’s name |
|
* or |
|
- attrNames (array) in this case method returns array of current values for given attribute names |
|
= (object) @Element if attrsName & value or params are passed in. |
|
= (...) value of the attribute if only attrsName is passed in. |
|
= (array) array of values of the attribute if attrsNames is passed in. |
|
= (object) object of attributes if nothing is passed in. |
|
> Possible parameters |
|
# <p>Please refer to the <a href="http://www.w3.org/TR/SVG/" title="The W3C Recommendation for the SVG language describes these properties in detail.">SVG specification</a> for an explanation of these parameters.</p> |
|
o arrow-end (string) arrowhead on the end of the path. The format for string is `<type>[-<width>[-<length>]]`. Possible types: `classic`, `block`, `open`, `oval`, `diamond`, `none`, width: `wide`, `narrow`, `medium`, length: `long`, `short`, `midium`. |
|
o clip-rect (string) comma or space separated values: x, y, width and height |
|
o cursor (string) CSS type of the cursor |
|
o cx (number) the x-axis coordinate of the center of the circle, or ellipse |
|
o cy (number) the y-axis coordinate of the center of the circle, or ellipse |
|
o fill (string) colour, gradient or image |
|
o fill-opacity (number) |
|
o font (string) |
|
o font-family (string) |
|
o font-size (number) font size in pixels |
|
o font-weight (string) |
|
o height (number) |
|
o href (string) URL, if specified element behaves as hyperlink |
|
o opacity (number) |
|
o path (string) SVG path string format |
|
o r (number) radius of the circle, ellipse or rounded corner on the rect |
|
o rx (number) horisontal radius of the ellipse |
|
o ry (number) vertical radius of the ellipse |
|
o src (string) image URL, only works for @Element.image element |
|
o stroke (string) stroke colour |
|
o stroke-dasharray (string) [“”, “none”, “`-`”, “`.`”, “`-.`”, “`-..`”, “`. `”, “`- `”, “`--`”, “`- .`”, “`--.`”, “`--..`”] |
|
o stroke-linecap (string) [“`butt`”, “`square`”, “`round`”] |
|
o stroke-linejoin (string) [“`bevel`”, “`round`”, “`miter`”] |
|
o stroke-miterlimit (number) |
|
o stroke-opacity (number) |
|
o stroke-width (number) stroke width in pixels, default is '1' |
|
o target (string) used with href |
|
o text (string) contents of the text element. Use `\n` for multiline text |
|
o text-anchor (string) [“`start`”, “`middle`”, “`end`”], default is “`middle`” |
|
o title (string) will create tooltip with a given text |
|
o transform (string) see @Element.transform |
|
o width (number) |
|
o x (number) |
|
o y (number) |
|
> Gradients |
|
* Linear gradient format: “`‹angle›-‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`90-#fff-#000`” – 90° |
|
* gradient from white to black or “`0-#fff-#f00:20-#000`” – 0° gradient from white via red (at 20%) to black. |
|
* |
|
* radial gradient: “`r[(‹fx›, ‹fy›)]‹colour›[-‹colour›[:‹offset›]]*-‹colour›`”, example: “`r#fff-#000`” – |
|
* gradient from white to black or “`r(0.25, 0.75)#fff-#000`” – gradient from white to black with focus point |
|
* at 0.25, 0.75. Focus point coordinates are in 0..1 range. Radial gradients can only be applied to circles and ellipses. |
|
> Path String |
|
# <p>Please refer to <a href="http://www.w3.org/TR/SVG/paths.html#PathData" title="Details of a path’s data attribute’s format are described in the SVG specification.">SVG documentation regarding path string</a>. Raphaël fully supports it.</p> |
|
> Colour Parsing |
|
# <ul> |
|
# <li>Colour name (“<code>red</code>”, “<code>green</code>”, “<code>cornflowerblue</code>”, etc)</li> |
|
# <li>#••• — shortened HTML colour: (“<code>#000</code>”, “<code>#fc0</code>”, etc)</li> |
|
# <li>#•••••• — full length HTML colour: (“<code>#000000</code>”, “<code>#bd2300</code>”)</li> |
|
# <li>rgb(•••, •••, •••) — red, green and blue channels’ values: (“<code>rgb(200, 100, 0)</code>”)</li> |
|
# <li>rgb(•••%, •••%, •••%) — same as above, but in %: (“<code>rgb(100%, 175%, 0%)</code>”)</li> |
|
# <li>rgba(•••, •••, •••, •••) — red, green and blue channels’ values: (“<code>rgba(200, 100, 0, .5)</code>”)</li> |
|
# <li>rgba(•••%, •••%, •••%, •••%) — same as above, but in %: (“<code>rgba(100%, 175%, 0%, 50%)</code>”)</li> |
|
# <li>hsb(•••, •••, •••) — hue, saturation and brightness values: (“<code>hsb(0.5, 0.25, 1)</code>”)</li> |
|
# <li>hsb(•••%, •••%, •••%) — same as above, but in %</li> |
|
# <li>hsba(•••, •••, •••, •••) — same as above, but with opacity</li> |
|
# <li>hsl(•••, •••, •••) — almost the same as hsb, see <a href="http://en.wikipedia.org/wiki/HSL_and_HSV" title="HSL and HSV - Wikipedia, the free encyclopedia">Wikipedia page</a></li> |
|
# <li>hsl(•••%, •••%, •••%) — same as above, but in %</li> |
|
# <li>hsla(•••, •••, •••, •••) — same as above, but with opacity</li> |
|
# <li>Optionally for hsb and hsl you could specify hue as a degree: “<code>hsl(240deg, 1, .5)</code>” or, if you want to go fancy, “<code>hsl(240°, 1, .5)</code>”</li> |
|
# </ul> |
|
\*/ |
|
elproto.attr = function (name, value) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
if (name == null) { |
|
var res = {}; |
|
for (var a in this.attrs) if (this.attrs[has](a)) { |
|
res[a] = this.attrs[a]; |
|
} |
|
res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient; |
|
res.transform = this._.transform; |
|
return res; |
|
} |
|
if (value == null && R.is(name, "string")) { |
|
if (name == "fill" && this.attrs.fill == "none" && this.attrs.gradient) { |
|
return this.attrs.gradient; |
|
} |
|
if (name == "transform") { |
|
return this._.transform; |
|
} |
|
var names = name.split(separator), |
|
out = {}; |
|
for (var i = 0, ii = names.length; i < ii; i++) { |
|
name = names[i]; |
|
if (name in this.attrs) { |
|
out[name] = this.attrs[name]; |
|
} else if (R.is(this.paper.customAttributes[name], "function")) { |
|
out[name] = this.paper.customAttributes[name].def; |
|
} else { |
|
out[name] = R._availableAttrs[name]; |
|
} |
|
} |
|
return ii - 1 ? out : out[names[0]]; |
|
} |
|
if (value == null && R.is(name, "array")) { |
|
out = {}; |
|
for (i = 0, ii = name.length; i < ii; i++) { |
|
out[name[i]] = this.attr(name[i]); |
|
} |
|
return out; |
|
} |
|
if (value != null) { |
|
var params = {}; |
|
params[name] = value; |
|
} else if (name != null && R.is(name, "object")) { |
|
params = name; |
|
} |
|
for (var key in params) { |
|
eve("raphael.attr." + key + "." + this.id, this, params[key]); |
|
} |
|
for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) { |
|
var par = this.paper.customAttributes[key].apply(this, [].concat(params[key])); |
|
this.attrs[key] = params[key]; |
|
for (var subkey in par) if (par[has](subkey)) { |
|
params[subkey] = par[subkey]; |
|
} |
|
} |
|
setFillAndStroke(this, params); |
|
return this; |
|
}; |
|
/*\ |
|
* Element.toFront |
|
[ method ] |
|
** |
|
* Moves the element so it is the closest to the viewer’s eyes, on top of other elements. |
|
= (object) @Element |
|
\*/ |
|
elproto.toFront = function () { |
|
if (this.removed) { |
|
return this; |
|
} |
|
var node = getRealNode(this.node); |
|
node.parentNode.appendChild(node); |
|
var svg = this.paper; |
|
svg.top != this && R._tofront(this, svg); |
|
return this; |
|
}; |
|
/*\ |
|
* Element.toBack |
|
[ method ] |
|
** |
|
* Moves the element so it is the furthest from the viewer’s eyes, behind other elements. |
|
= (object) @Element |
|
\*/ |
|
elproto.toBack = function () { |
|
if (this.removed) { |
|
return this; |
|
} |
|
var node = getRealNode(this.node); |
|
var parentNode = node.parentNode; |
|
parentNode.insertBefore(node, parentNode.firstChild); |
|
R._toback(this, this.paper); |
|
var svg = this.paper; |
|
return this; |
|
}; |
|
/*\ |
|
* Element.insertAfter |
|
[ method ] |
|
** |
|
* Inserts current object after the given one. |
|
= (object) @Element |
|
\*/ |
|
elproto.insertAfter = function (element) { |
|
if (this.removed || !element) { |
|
return this; |
|
} |
|
|
|
var node = getRealNode(this.node); |
|
var afterNode = getRealNode(element.node || element[element.length - 1].node); |
|
if (afterNode.nextSibling) { |
|
afterNode.parentNode.insertBefore(node, afterNode.nextSibling); |
|
} else { |
|
afterNode.parentNode.appendChild(node); |
|
} |
|
R._insertafter(this, element, this.paper); |
|
return this; |
|
}; |
|
/*\ |
|
* Element.insertBefore |
|
[ method ] |
|
** |
|
* Inserts current object before the given one. |
|
= (object) @Element |
|
\*/ |
|
elproto.insertBefore = function (element) { |
|
if (this.removed || !element) { |
|
return this; |
|
} |
|
|
|
var node = getRealNode(this.node); |
|
var beforeNode = getRealNode(element.node || element[0].node); |
|
beforeNode.parentNode.insertBefore(node, beforeNode); |
|
R._insertbefore(this, element, this.paper); |
|
return this; |
|
}; |
|
elproto.blur = function (size) { |
|
// Experimental. No Safari support. Use it on your own risk. |
|
var t = this; |
|
if (+size !== 0) { |
|
var fltr = $("filter"), |
|
blur = $("feGaussianBlur"); |
|
t.attrs.blur = size; |
|
fltr.id = R.createUUID(); |
|
$(blur, {stdDeviation: +size || 1.5}); |
|
fltr.appendChild(blur); |
|
t.paper.defs.appendChild(fltr); |
|
t._blur = fltr; |
|
$(t.node, {filter: "url(#" + fltr.id + ")"}); |
|
} else { |
|
if (t._blur) { |
|
t._blur.parentNode.removeChild(t._blur); |
|
delete t._blur; |
|
delete t.attrs.blur; |
|
} |
|
t.node.removeAttribute("filter"); |
|
} |
|
return t; |
|
}; |
|
R._engine.circle = function (svg, x, y, r) { |
|
var el = $("circle"); |
|
svg.canvas && svg.canvas.appendChild(el); |
|
var res = new Element(el, svg); |
|
res.attrs = {cx: x, cy: y, r: r, fill: "none", stroke: "#000"}; |
|
res.type = "circle"; |
|
$(el, res.attrs); |
|
return res; |
|
}; |
|
R._engine.rect = function (svg, x, y, w, h, r) { |
|
var el = $("rect"); |
|
svg.canvas && svg.canvas.appendChild(el); |
|
var res = new Element(el, svg); |
|
res.attrs = {x: x, y: y, width: w, height: h, rx: r || 0, ry: r || 0, fill: "none", stroke: "#000"}; |
|
res.type = "rect"; |
|
$(el, res.attrs); |
|
return res; |
|
}; |
|
R._engine.ellipse = function (svg, x, y, rx, ry) { |
|
var el = $("ellipse"); |
|
svg.canvas && svg.canvas.appendChild(el); |
|
var res = new Element(el, svg); |
|
res.attrs = {cx: x, cy: y, rx: rx, ry: ry, fill: "none", stroke: "#000"}; |
|
res.type = "ellipse"; |
|
$(el, res.attrs); |
|
return res; |
|
}; |
|
R._engine.image = function (svg, src, x, y, w, h) { |
|
var el = $("image"); |
|
$(el, {x: x, y: y, width: w, height: h, preserveAspectRatio: "none"}); |
|
el.setAttributeNS(xlink, "href", src); |
|
svg.canvas && svg.canvas.appendChild(el); |
|
var res = new Element(el, svg); |
|
res.attrs = {x: x, y: y, width: w, height: h, src: src}; |
|
res.type = "image"; |
|
return res; |
|
}; |
|
R._engine.text = function (svg, x, y, text) { |
|
var el = $("text"); |
|
svg.canvas && svg.canvas.appendChild(el); |
|
var res = new Element(el, svg); |
|
res.attrs = { |
|
x: x, |
|
y: y, |
|
"text-anchor": "middle", |
|
text: text, |
|
"font-family": R._availableAttrs["font-family"], |
|
"font-size": R._availableAttrs["font-size"], |
|
stroke: "none", |
|
fill: "#000" |
|
}; |
|
res.type = "text"; |
|
setFillAndStroke(res, res.attrs); |
|
return res; |
|
}; |
|
R._engine.setSize = function (width, height) { |
|
this.width = width || this.width; |
|
this.height = height || this.height; |
|
this.canvas.setAttribute("width", this.width); |
|
this.canvas.setAttribute("height", this.height); |
|
if (this._viewBox) { |
|
this.setViewBox.apply(this, this._viewBox); |
|
} |
|
return this; |
|
}; |
|
R._engine.create = function () { |
|
var con = R._getContainer.apply(0, arguments), |
|
container = con && con.container, |
|
x = con.x, |
|
y = con.y, |
|
width = con.width, |
|
height = con.height; |
|
if (!container) { |
|
throw new Error("SVG container not found."); |
|
} |
|
var cnvs = $("svg"), |
|
css = "overflow:hidden;", |
|
isFloating; |
|
x = x || 0; |
|
y = y || 0; |
|
width = width || 512; |
|
height = height || 342; |
|
$(cnvs, { |
|
height: height, |
|
version: 1.1, |
|
width: width, |
|
xmlns: "http://www.w3.org/2000/svg", |
|
"xmlns:xlink": "http://www.w3.org/1999/xlink" |
|
}); |
|
if (container == 1) { |
|
cnvs.style.cssText = css + "position:absolute;left:" + x + "px;top:" + y + "px"; |
|
R._g.doc.body.appendChild(cnvs); |
|
isFloating = 1; |
|
} else { |
|
cnvs.style.cssText = css + "position:relative"; |
|
if (container.firstChild) { |
|
container.insertBefore(cnvs, container.firstChild); |
|
} else { |
|
container.appendChild(cnvs); |
|
} |
|
} |
|
container = new R._Paper; |
|
container.width = width; |
|
container.height = height; |
|
container.canvas = cnvs; |
|
container.clear(); |
|
container._left = container._top = 0; |
|
isFloating && (container.renderfix = function () {}); |
|
container.renderfix(); |
|
return container; |
|
}; |
|
R._engine.setViewBox = function (x, y, w, h, fit) { |
|
eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]); |
|
var paperSize = this.getSize(), |
|
size = mmax(w / paperSize.width, h / paperSize.height), |
|
top = this.top, |
|
aspectRatio = fit ? "xMidYMid meet" : "xMinYMin", |
|
vb, |
|
sw; |
|
if (x == null) { |
|
if (this._vbSize) { |
|
size = 1; |
|
} |
|
delete this._vbSize; |
|
vb = "0 0 " + this.width + S + this.height; |
|
} else { |
|
this._vbSize = size; |
|
vb = x + S + y + S + w + S + h; |
|
} |
|
$(this.canvas, { |
|
viewBox: vb, |
|
preserveAspectRatio: aspectRatio |
|
}); |
|
while (size && top) { |
|
sw = "stroke-width" in top.attrs ? top.attrs["stroke-width"] : 1; |
|
top.attr({"stroke-width": sw}); |
|
top._.dirty = 1; |
|
top._.dirtyT = 1; |
|
top = top.prev; |
|
} |
|
this._viewBox = [x, y, w, h, !!fit]; |
|
return this; |
|
}; |
|
/*\ |
|
* Paper.renderfix |
|
[ method ] |
|
** |
|
* Fixes the issue of Firefox and IE9 regarding subpixel rendering. If paper is dependant |
|
* on other elements after reflow it could shift half pixel which cause for lines to lost their crispness. |
|
* This method fixes the issue. |
|
** |
|
Special thanks to Mariusz Nowak (http://www.medikoo.com/) for this method. |
|
\*/ |
|
R.prototype.renderfix = function () { |
|
var cnvs = this.canvas, |
|
s = cnvs.style, |
|
pos; |
|
try { |
|
pos = cnvs.getScreenCTM() || cnvs.createSVGMatrix(); |
|
} catch (e) { |
|
pos = cnvs.createSVGMatrix(); |
|
} |
|
var left = -pos.e % 1, |
|
top = -pos.f % 1; |
|
if (left || top) { |
|
if (left) { |
|
this._left = (this._left + left) % 1; |
|
s.left = this._left + "px"; |
|
} |
|
if (top) { |
|
this._top = (this._top + top) % 1; |
|
s.top = this._top + "px"; |
|
} |
|
} |
|
}; |
|
/*\ |
|
* Paper.clear |
|
[ method ] |
|
** |
|
* Clears the paper, i.e. removes all the elements. |
|
\*/ |
|
R.prototype.clear = function () { |
|
R.eve("raphael.clear", this); |
|
var c = this.canvas; |
|
while (c.firstChild) { |
|
c.removeChild(c.firstChild); |
|
} |
|
this.bottom = this.top = null; |
|
(this.desc = $("desc")).appendChild(R._g.doc.createTextNode("Created with Rapha\xebl " + R.version)); |
|
c.appendChild(this.desc); |
|
c.appendChild(this.defs = $("defs")); |
|
}; |
|
/*\ |
|
* Paper.remove |
|
[ method ] |
|
** |
|
* Removes the paper from the DOM. |
|
\*/ |
|
R.prototype.remove = function () { |
|
eve("raphael.remove", this); |
|
this.canvas.parentNode && this.canvas.parentNode.removeChild(this.canvas); |
|
for (var i in this) { |
|
this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null; |
|
} |
|
}; |
|
var setproto = R.st; |
|
for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) { |
|
setproto[method] = (function (methodname) { |
|
return function () { |
|
var arg = arguments; |
|
return this.forEach(function (el) { |
|
el[methodname].apply(el, arg); |
|
}); |
|
}; |
|
})(method); |
|
} |
|
})); |
|
|
|
// ┌─────────────────────────────────────────────────────────────────────┐ \\ |
|
// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\ |
|
// ├─────────────────────────────────────────────────────────────────────┤ \\ |
|
// │ VML Module │ \\ |
|
// ├─────────────────────────────────────────────────────────────────────┤ \\ |
|
// │ Copyright (c) 2008-2011 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ |
|
// │ Copyright (c) 2008-2011 Sencha Labs (http://sencha.com) │ \\ |
|
// │ Licensed under the MIT (http://raphaeljs.com/license.html) license. │ \\ |
|
// └─────────────────────────────────────────────────────────────────────┘ \\ |
|
|
|
(function (glob, factory) { |
|
if (typeof define === "function" && define.amd) { |
|
define("raphael.vml", ["raphael.core"], function(raphael) { |
|
return factory(raphael); |
|
}); |
|
} else if (typeof exports === "object") { |
|
factory(require("raphael")); |
|
} else { |
|
factory(glob.Raphael); |
|
} |
|
}(this, function(R) { |
|
if (R && !R.vml) { |
|
return; |
|
} |
|
|
|
var has = "hasOwnProperty", |
|
Str = String, |
|
toFloat = parseFloat, |
|
math = Math, |
|
round = math.round, |
|
mmax = math.max, |
|
mmin = math.min, |
|
abs = math.abs, |
|
fillString = "fill", |
|
separator = /[, ]+/, |
|
eve = R.eve, |
|
ms = " progid:DXImageTransform.Microsoft", |
|
S = " ", |
|
E = "", |
|
map = {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"}, |
|
bites = /([clmz]),?([^clmz]*)/gi, |
|
blurregexp = / progid:\S+Blur\([^\)]+\)/g, |
|
val = /-?[^,\s-]+/g, |
|
cssDot = "position:absolute;left:0;top:0;width:1px;height:1px;behavior:url(#default#VML)", |
|
zoom = 21600, |
|
pathTypes = {path: 1, rect: 1, image: 1}, |
|
ovalTypes = {circle: 1, ellipse: 1}, |
|
path2vml = function (path) { |
|
var total = /[ahqstv]/ig, |
|
command = R._pathToAbsolute; |
|
Str(path).match(total) && (command = R._path2curve); |
|
total = /[clmz]/g; |
|
if (command == R._pathToAbsolute && !Str(path).match(total)) { |
|
var res = Str(path).replace(bites, function (all, command, args) { |
|
var vals = [], |
|
isMove = command.toLowerCase() == "m", |
|
res = map[command]; |
|
args.replace(val, function (value) { |
|
if (isMove && vals.length == 2) { |
|
res += vals + map[command == "m" ? "l" : "L"]; |
|
vals = []; |
|
} |
|
vals.push(round(value * zoom)); |
|
}); |
|
return res + vals; |
|
}); |
|
return res; |
|
} |
|
var pa = command(path), p, r; |
|
res = []; |
|
for (var i = 0, ii = pa.length; i < ii; i++) { |
|
p = pa[i]; |
|
r = pa[i][0].toLowerCase(); |
|
r == "z" && (r = "x"); |
|
for (var j = 1, jj = p.length; j < jj; j++) { |
|
r += round(p[j] * zoom) + (j != jj - 1 ? "," : E); |
|
} |
|
res.push(r); |
|
} |
|
return res.join(S); |
|
}, |
|
compensation = function (deg, dx, dy) { |
|
var m = R.matrix(); |
|
m.rotate(-deg, .5, .5); |
|
return { |
|
dx: m.x(dx, dy), |
|
dy: m.y(dx, dy) |
|
}; |
|
}, |
|
setCoords = function (p, sx, sy, dx, dy, deg) { |
|
var _ = p._, |
|
m = p.matrix, |
|
fillpos = _.fillpos, |
|
o = p.node, |
|
s = o.style, |
|
y = 1, |
|
flip = "", |
|
dxdy, |
|
kx = zoom / sx, |
|
ky = zoom / sy; |
|
s.visibility = "hidden"; |
|
if (!sx || !sy) { |
|
return; |
|
} |
|
o.coordsize = abs(kx) + S + abs(ky); |
|
s.rotation = deg * (sx * sy < 0 ? -1 : 1); |
|
if (deg) { |
|
var c = compensation(deg, dx, dy); |
|
dx = c.dx; |
|
dy = c.dy; |
|
} |
|
sx < 0 && (flip += "x"); |
|
sy < 0 && (flip += " y") && (y = -1); |
|
s.flip = flip; |
|
o.coordorigin = (dx * -kx) + S + (dy * -ky); |
|
if (fillpos || _.fillsize) { |
|
var fill = o.getElementsByTagName(fillString); |
|
fill = fill && fill[0]; |
|
o.removeChild(fill); |
|
if (fillpos) { |
|
c = compensation(deg, m.x(fillpos[0], fillpos[1]), m.y(fillpos[0], fillpos[1])); |
|
fill.position = c.dx * y + S + c.dy * y; |
|
} |
|
if (_.fillsize) { |
|
fill.size = _.fillsize[0] * abs(sx) + S + _.fillsize[1] * abs(sy); |
|
} |
|
o.appendChild(fill); |
|
} |
|
s.visibility = "visible"; |
|
}; |
|
R.toString = function () { |
|
return "Your browser doesn\u2019t support SVG. Falling down to VML.\nYou are running Rapha\xebl " + this.version; |
|
}; |
|
var addArrow = function (o, value, isEnd) { |
|
var values = Str(value).toLowerCase().split("-"), |
|
se = isEnd ? "end" : "start", |
|
i = values.length, |
|
type = "classic", |
|
w = "medium", |
|
h = "medium"; |
|
while (i--) { |
|
switch (values[i]) { |
|
case "block": |
|
case "classic": |
|
case "oval": |
|
case "diamond": |
|
case "open": |
|
case "none": |
|
type = values[i]; |
|
break; |
|
case "wide": |
|
case "narrow": h = values[i]; break; |
|
case "long": |
|
case "short": w = values[i]; break; |
|
} |
|
} |
|
var stroke = o.node.getElementsByTagName("stroke")[0]; |
|
stroke[se + "arrow"] = type; |
|
stroke[se + "arrowlength"] = w; |
|
stroke[se + "arrowwidth"] = h; |
|
}, |
|
setFillAndStroke = function (o, params) { |
|
// o.paper.canvas.style.display = "none"; |
|
o.attrs = o.attrs || {}; |
|
var node = o.node, |
|
a = o.attrs, |
|
s = node.style, |
|
xy, |
|
newpath = pathTypes[o.type] && (params.x != a.x || params.y != a.y || params.width != a.width || params.height != a.height || params.cx != a.cx || params.cy != a.cy || params.rx != a.rx || params.ry != a.ry || params.r != a.r), |
|
isOval = ovalTypes[o.type] && (a.cx != params.cx || a.cy != params.cy || a.r != params.r || a.rx != params.rx || a.ry != params.ry), |
|
res = o; |
|
|
|
|
|
for (var par in params) if (params[has](par)) { |
|
a[par] = params[par]; |
|
} |
|
if (newpath) { |
|
a.path = R._getPath[o.type](o); |
|
o._.dirty = 1; |
|
} |
|
params.href && (node.href = params.href); |
|
params.title && (node.title = params.title); |
|
params.target && (node.target = params.target); |
|
params.cursor && (s.cursor = params.cursor); |
|
"blur" in params && o.blur(params.blur); |
|
if (params.path && o.type == "path" || newpath) { |
|
node.path = path2vml(~Str(a.path).toLowerCase().indexOf("r") ? R._pathToAbsolute(a.path) : a.path); |
|
o._.dirty = 1; |
|
if (o.type == "image") { |
|
o._.fillpos = [a.x, a.y]; |
|
o._.fillsize = [a.width, a.height]; |
|
setCoords(o, 1, 1, 0, 0, 0); |
|
} |
|
} |
|
"transform" in params && o.transform(params.transform); |
|
if (isOval) { |
|
var cx = +a.cx, |
|
cy = +a.cy, |
|
rx = +a.rx || +a.r || 0, |
|
ry = +a.ry || +a.r || 0; |
|
node.path = R.format("ar{0},{1},{2},{3},{4},{1},{4},{1}x", round((cx - rx) * zoom), round((cy - ry) * zoom), round((cx + rx) * zoom), round((cy + ry) * zoom), round(cx * zoom)); |
|
o._.dirty = 1; |
|
} |
|
if ("clip-rect" in params) { |
|
var rect = Str(params["clip-rect"]).split(separator); |
|
if (rect.length == 4) { |
|
rect[2] = +rect[2] + (+rect[0]); |
|
rect[3] = +rect[3] + (+rect[1]); |
|
var div = node.clipRect || R._g.doc.createElement("div"), |
|
dstyle = div.style; |
|
dstyle.clip = R.format("rect({1}px {2}px {3}px {0}px)", rect); |
|
if (!node.clipRect) { |
|
dstyle.position = "absolute"; |
|
dstyle.top = 0; |
|
dstyle.left = 0; |
|
dstyle.width = o.paper.width + "px"; |
|
dstyle.height = o.paper.height + "px"; |
|
node.parentNode.insertBefore(div, node); |
|
div.appendChild(node); |
|
node.clipRect = div; |
|
} |
|
} |
|
if (!params["clip-rect"]) { |
|
node.clipRect && (node.clipRect.style.clip = "auto"); |
|
} |
|
} |
|
if (o.textpath) { |
|
var textpathStyle = o.textpath.style; |
|
params.font && (textpathStyle.font = params.font); |
|
params["font-family"] && (textpathStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(/^['"]+|['"]+$/g, E) + '"'); |
|
params["font-size"] && (textpathStyle.fontSize = params["font-size"]); |
|
params["font-weight"] && (textpathStyle.fontWeight = params["font-weight"]); |
|
params["font-style"] && (textpathStyle.fontStyle = params["font-style"]); |
|
} |
|
if ("arrow-start" in params) { |
|
addArrow(res, params["arrow-start"]); |
|
} |
|
if ("arrow-end" in params) { |
|
addArrow(res, params["arrow-end"], 1); |
|
} |
|
if (params.opacity != null || |
|
params["stroke-width"] != null || |
|
params.fill != null || |
|
params.src != null || |
|
params.stroke != null || |
|
params["stroke-width"] != null || |
|
params["stroke-opacity"] != null || |
|
params["fill-opacity"] != null || |
|
params["stroke-dasharray"] != null || |
|
params["stroke-miterlimit"] != null || |
|
params["stroke-linejoin"] != null || |
|
params["stroke-linecap"] != null) { |
|
var fill = node.getElementsByTagName(fillString), |
|
newfill = false; |
|
fill = fill && fill[0]; |
|
!fill && (newfill = fill = createNode(fillString)); |
|
if (o.type == "image" && params.src) { |
|
fill.src = params.src; |
|
} |
|
params.fill && (fill.on = true); |
|
if (fill.on == null || params.fill == "none" || params.fill === null) { |
|
fill.on = false; |
|
} |
|
if (fill.on && params.fill) { |
|
var isURL = Str(params.fill).match(R._ISURL); |
|
if (isURL) { |
|
fill.parentNode == node && node.removeChild(fill); |
|
fill.rotate = true; |
|
fill.src = isURL[1]; |
|
fill.type = "tile"; |
|
var bbox = o.getBBox(1); |
|
fill.position = bbox.x + S + bbox.y; |
|
o._.fillpos = [bbox.x, bbox.y]; |
|
|
|
R._preload(isURL[1], function () { |
|
o._.fillsize = [this.offsetWidth, this.offsetHeight]; |
|
}); |
|
} else { |
|
fill.color = R.getRGB(params.fill).hex; |
|
fill.src = E; |
|
fill.type = "solid"; |
|
if (R.getRGB(params.fill).error && (res.type in {circle: 1, ellipse: 1} || Str(params.fill).charAt() != "r") && addGradientFill(res, params.fill, fill)) { |
|
a.fill = "none"; |
|
a.gradient = params.fill; |
|
fill.rotate = false; |
|
} |
|
} |
|
} |
|
if ("fill-opacity" in params || "opacity" in params) { |
|
var opacity = ((+a["fill-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+R.getRGB(params.fill).o + 1 || 2) - 1); |
|
opacity = mmin(mmax(opacity, 0), 1); |
|
fill.opacity = opacity; |
|
if (fill.src) { |
|
fill.color = "none"; |
|
} |
|
} |
|
node.appendChild(fill); |
|
var stroke = (node.getElementsByTagName("stroke") && node.getElementsByTagName("stroke")[0]), |
|
newstroke = false; |
|
!stroke && (newstroke = stroke = createNode("stroke")); |
|
if ((params.stroke && params.stroke != "none") || |
|
params["stroke-width"] || |
|
params["stroke-opacity"] != null || |
|
params["stroke-dasharray"] || |
|
params["stroke-miterlimit"] || |
|
params["stroke-linejoin"] || |
|
params["stroke-linecap"]) { |
|
stroke.on = true; |
|
} |
|
(params.stroke == "none" || params.stroke === null || stroke.on == null || params.stroke == 0 || params["stroke-width"] == 0) && (stroke.on = false); |
|
var strokeColor = R.getRGB(params.stroke); |
|
stroke.on && params.stroke && (stroke.color = strokeColor.hex); |
|
opacity = ((+a["stroke-opacity"] + 1 || 2) - 1) * ((+a.opacity + 1 || 2) - 1) * ((+strokeColor.o + 1 || 2) - 1); |
|
var width = (toFloat(params["stroke-width"]) || 1) * .75; |
|
opacity = mmin(mmax(opacity, 0), 1); |
|
params["stroke-width"] == null && (width = a["stroke-width"]); |
|
params["stroke-width"] && (stroke.weight = width); |
|
width && width < 1 && (opacity *= width) && (stroke.weight = 1); |
|
stroke.opacity = opacity; |
|
|
|
params["stroke-linejoin"] && (stroke.joinstyle = params["stroke-linejoin"] || "miter"); |
|
stroke.miterlimit = params["stroke-miterlimit"] || 8; |
|
params["stroke-linecap"] && (stroke.endcap = params["stroke-linecap"] == "butt" ? "flat" : params["stroke-linecap"] == "square" ? "square" : "round"); |
|
if ("stroke-dasharray" in params) { |
|
var dasharray = { |
|
"-": "shortdash", |
|
".": "shortdot", |
|
"-.": "shortdashdot", |
|
"-..": "shortdashdotdot", |
|
". ": "dot", |
|
"- ": "dash", |
|
"--": "longdash", |
|
"- .": "dashdot", |
|
"--.": "longdashdot", |
|
"--..": "longdashdotdot" |
|
}; |
|
stroke.dashstyle = dasharray[has](params["stroke-dasharray"]) ? dasharray[params["stroke-dasharray"]] : E; |
|
} |
|
newstroke && node.appendChild(stroke); |
|
} |
|
if (res.type == "text") { |
|
res.paper.canvas.style.display = E; |
|
var span = res.paper.span, |
|
m = 100, |
|
fontSize = a.font && a.font.match(/\d+(?:\.\d*)?(?=px)/); |
|
s = span.style; |
|
a.font && (s.font = a.font); |
|
a["font-family"] && (s.fontFamily = a["font-family"]); |
|
a["font-weight"] && (s.fontWeight = a["font-weight"]); |
|
a["font-style"] && (s.fontStyle = a["font-style"]); |
|
fontSize = toFloat(a["font-size"] || fontSize && fontSize[0]) || 10; |
|
s.fontSize = fontSize * m + "px"; |
|
res.textpath.string && (span.innerHTML = Str(res.textpath.string).replace(/</g, "<").replace(/&/g, "&").replace(/\n/g, "<br>")); |
|
var brect = span.getBoundingClientRect(); |
|
res.W = a.w = (brect.right - brect.left) / m; |
|
res.H = a.h = (brect.bottom - brect.top) / m; |
|
// res.paper.canvas.style.display = "none"; |
|
res.X = a.x; |
|
res.Y = a.y + res.H / 2; |
|
|
|
("x" in params || "y" in params) && (res.path.v = R.format("m{0},{1}l{2},{1}", round(a.x * zoom), round(a.y * zoom), round(a.x * zoom) + 1)); |
|
var dirtyattrs = ["x", "y", "text", "font", "font-family", "font-weight", "font-style", "font-size"]; |
|
for (var d = 0, dd = dirtyattrs.length; d < dd; d++) if (dirtyattrs[d] in params) { |
|
res._.dirty = 1; |
|
break; |
|
} |
|
|
|
// text-anchor emulation |
|
switch (a["text-anchor"]) { |
|
case "start": |
|
res.textpath.style["v-text-align"] = "left"; |
|
res.bbx = res.W / 2; |
|
break; |
|
case "end": |
|
res.textpath.style["v-text-align"] = "right"; |
|
res.bbx = -res.W / 2; |
|
break; |
|
default: |
|
res.textpath.style["v-text-align"] = "center"; |
|
res.bbx = 0; |
|
break; |
|
} |
|
res.textpath.style["v-text-kern"] = true; |
|
} |
|
// res.paper.canvas.style.display = E; |
|
}, |
|
addGradientFill = function (o, gradient, fill) { |
|
o.attrs = o.attrs || {}; |
|
var attrs = o.attrs, |
|
pow = Math.pow, |
|
opacity, |
|
oindex, |
|
type = "linear", |
|
fxfy = ".5 .5"; |
|
o.attrs.gradient = gradient; |
|
gradient = Str(gradient).replace(R._radial_gradient, function (all, fx, fy) { |
|
type = "radial"; |
|
if (fx && fy) { |
|
fx = toFloat(fx); |
|
fy = toFloat(fy); |
|
pow(fx - .5, 2) + pow(fy - .5, 2) > .25 && (fy = math.sqrt(.25 - pow(fx - .5, 2)) * ((fy > .5) * 2 - 1) + .5); |
|
fxfy = fx + S + fy; |
|
} |
|
return E; |
|
}); |
|
gradient = gradient.split(/\s*\-\s*/); |
|
if (type == "linear") { |
|
var angle = gradient.shift(); |
|
angle = -toFloat(angle); |
|
if (isNaN(angle)) { |
|
return null; |
|
} |
|
} |
|
var dots = R._parseDots(gradient); |
|
if (!dots) { |
|
return null; |
|
} |
|
o = o.shape || o.node; |
|
if (dots.length) { |
|
o.removeChild(fill); |
|
fill.on = true; |
|
fill.method = "none"; |
|
fill.color = dots[0].color; |
|
fill.color2 = dots[dots.length - 1].color; |
|
var clrs = []; |
|
for (var i = 0, ii = dots.length; i < ii; i++) { |
|
dots[i].offset && clrs.push(dots[i].offset + S + dots[i].color); |
|
} |
|
fill.colors = clrs.length ? clrs.join() : "0% " + fill.color; |
|
if (type == "radial") { |
|
fill.type = "gradientTitle"; |
|
fill.focus = "100%"; |
|
fill.focussize = "0 0"; |
|
fill.focusposition = fxfy; |
|
fill.angle = 0; |
|
} else { |
|
// fill.rotate= true; |
|
fill.type = "gradient"; |
|
fill.angle = (270 - angle) % 360; |
|
} |
|
o.appendChild(fill); |
|
} |
|
return 1; |
|
}, |
|
Element = function (node, vml) { |
|
this[0] = this.node = node; |
|
node.raphael = true; |
|
this.id = R._oid++; |
|
node.raphaelid = this.id; |
|
this.X = 0; |
|
this.Y = 0; |
|
this.attrs = {}; |
|
this.paper = vml; |
|
this.matrix = R.matrix(); |
|
this._ = { |
|
transform: [], |
|
sx: 1, |
|
sy: 1, |
|
dx: 0, |
|
dy: 0, |
|
deg: 0, |
|
dirty: 1, |
|
dirtyT: 1 |
|
}; |
|
!vml.bottom && (vml.bottom = this); |
|
this.prev = vml.top; |
|
vml.top && (vml.top.next = this); |
|
vml.top = this; |
|
this.next = null; |
|
}; |
|
var elproto = R.el; |
|
|
|
Element.prototype = elproto; |
|
elproto.constructor = Element; |
|
elproto.transform = function (tstr) { |
|
if (tstr == null) { |
|
return this._.transform; |
|
} |
|
var vbs = this.paper._viewBoxShift, |
|
vbt = vbs ? "s" + [vbs.scale, vbs.scale] + "-1-1t" + [vbs.dx, vbs.dy] : E, |
|
oldt; |
|
if (vbs) { |
|
oldt = tstr = Str(tstr).replace(/\.{3}|\u2026/g, this._.transform || E); |
|
} |
|
R._extractTransform(this, vbt + tstr); |
|
var matrix = this.matrix.clone(), |
|
skew = this.skew, |
|
o = this.node, |
|
split, |
|
isGrad = ~Str(this.attrs.fill).indexOf("-"), |
|
isPatt = !Str(this.attrs.fill).indexOf("url("); |
|
matrix.translate(1, 1); |
|
if (isPatt || isGrad || this.type == "image") { |
|
skew.matrix = "1 0 0 1"; |
|
skew.offset = "0 0"; |
|
split = matrix.split(); |
|
if ((isGrad && split.noRotation) || !split.isSimple) { |
|
o.style.filter = matrix.toFilter(); |
|
var bb = this.getBBox(), |
|
bbt = this.getBBox(1), |
|
dx = bb.x - bbt.x, |
|
dy = bb.y - bbt.y; |
|
o.coordorigin = (dx * -zoom) + S + (dy * -zoom); |
|
setCoords(this, 1, 1, dx, dy, 0); |
|
} else { |
|
o.style.filter = E; |
|
setCoords(this, split.scalex, split.scaley, split.dx, split.dy, split.rotate); |
|
} |
|
} else { |
|
o.style.filter = E; |
|
skew.matrix = Str(matrix); |
|
skew.offset = matrix.offset(); |
|
} |
|
if (oldt !== null) { // empty string value is true as well |
|
this._.transform = oldt; |
|
R._extractTransform(this, oldt); |
|
} |
|
return this; |
|
}; |
|
elproto.rotate = function (deg, cx, cy) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
if (deg == null) { |
|
return; |
|
} |
|
deg = Str(deg).split(separator); |
|
if (deg.length - 1) { |
|
cx = toFloat(deg[1]); |
|
cy = toFloat(deg[2]); |
|
} |
|
deg = toFloat(deg[0]); |
|
(cy == null) && (cx = cy); |
|
if (cx == null || cy == null) { |
|
var bbox = this.getBBox(1); |
|
cx = bbox.x + bbox.width / 2; |
|
cy = bbox.y + bbox.height / 2; |
|
} |
|
this._.dirtyT = 1; |
|
this.transform(this._.transform.concat([["r", deg, cx, cy]])); |
|
return this; |
|
}; |
|
elproto.translate = function (dx, dy) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
dx = Str(dx).split(separator); |
|
if (dx.length - 1) { |
|
dy = toFloat(dx[1]); |
|
} |
|
dx = toFloat(dx[0]) || 0; |
|
dy = +dy || 0; |
|
if (this._.bbox) { |
|
this._.bbox.x += dx; |
|
this._.bbox.y += dy; |
|
} |
|
this.transform(this._.transform.concat([["t", dx, dy]])); |
|
return this; |
|
}; |
|
elproto.scale = function (sx, sy, cx, cy) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
sx = Str(sx).split(separator); |
|
if (sx.length - 1) { |
|
sy = toFloat(sx[1]); |
|
cx = toFloat(sx[2]); |
|
cy = toFloat(sx[3]); |
|
isNaN(cx) && (cx = null); |
|
isNaN(cy) && (cy = null); |
|
} |
|
sx = toFloat(sx[0]); |
|
(sy == null) && (sy = sx); |
|
(cy == null) && (cx = cy); |
|
if (cx == null || cy == null) { |
|
var bbox = this.getBBox(1); |
|
} |
|
cx = cx == null ? bbox.x + bbox.width / 2 : cx; |
|
cy = cy == null ? bbox.y + bbox.height / 2 : cy; |
|
|
|
this.transform(this._.transform.concat([["s", sx, sy, cx, cy]])); |
|
this._.dirtyT = 1; |
|
return this; |
|
}; |
|
elproto.hide = function () { |
|
!this.removed && (this.node.style.display = "none"); |
|
return this; |
|
}; |
|
elproto.show = function () { |
|
!this.removed && (this.node.style.display = E); |
|
return this; |
|
}; |
|
// Needed to fix the vml setViewBox issues |
|
elproto.auxGetBBox = R.el.getBBox; |
|
elproto.getBBox = function(){ |
|
var b = this.auxGetBBox(); |
|
if (this.paper && this.paper._viewBoxShift) |
|
{ |
|
var c = {}; |
|
var z = 1/this.paper._viewBoxShift.scale; |
|
c.x = b.x - this.paper._viewBoxShift.dx; |
|
c.x *= z; |
|
c.y = b.y - this.paper._viewBoxShift.dy; |
|
c.y *= z; |
|
c.width = b.width * z; |
|
c.height = b.height * z; |
|
c.x2 = c.x + c.width; |
|
c.y2 = c.y + c.height; |
|
return c; |
|
} |
|
return b; |
|
}; |
|
elproto._getBBox = function () { |
|
if (this.removed) { |
|
return {}; |
|
} |
|
return { |
|
x: this.X + (this.bbx || 0) - this.W / 2, |
|
y: this.Y - this.H, |
|
width: this.W, |
|
height: this.H |
|
}; |
|
}; |
|
elproto.remove = function () { |
|
if (this.removed || !this.node.parentNode) { |
|
return; |
|
} |
|
this.paper.__set__ && this.paper.__set__.exclude(this); |
|
R.eve.unbind("raphael.*.*." + this.id); |
|
R._tear(this, this.paper); |
|
this.node.parentNode.removeChild(this.node); |
|
this.shape && this.shape.parentNode.removeChild(this.shape); |
|
for (var i in this) { |
|
this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null; |
|
} |
|
this.removed = true; |
|
}; |
|
elproto.attr = function (name, value) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
if (name == null) { |
|
var res = {}; |
|
for (var a in this.attrs) if (this.attrs[has](a)) { |
|
res[a] = this.attrs[a]; |
|
} |
|
res.gradient && res.fill == "none" && (res.fill = res.gradient) && delete res.gradient; |
|
res.transform = this._.transform; |
|
return res; |
|
} |
|
if (value == null && R.is(name, "string")) { |
|
if (name == fillString && this.attrs.fill == "none" && this.attrs.gradient) { |
|
return this.attrs.gradient; |
|
} |
|
var names = name.split(separator), |
|
out = {}; |
|
for (var i = 0, ii = names.length; i < ii; i++) { |
|
name = names[i]; |
|
if (name in this.attrs) { |
|
out[name] = this.attrs[name]; |
|
} else if (R.is(this.paper.customAttributes[name], "function")) { |
|
out[name] = this.paper.customAttributes[name].def; |
|
} else { |
|
out[name] = R._availableAttrs[name]; |
|
} |
|
} |
|
return ii - 1 ? out : out[names[0]]; |
|
} |
|
if (this.attrs && value == null && R.is(name, "array")) { |
|
out = {}; |
|
for (i = 0, ii = name.length; i < ii; i++) { |
|
out[name[i]] = this.attr(name[i]); |
|
} |
|
return out; |
|
} |
|
var params; |
|
if (value != null) { |
|
params = {}; |
|
params[name] = value; |
|
} |
|
value == null && R.is(name, "object") && (params = name); |
|
for (var key in params) { |
|
eve("raphael.attr." + key + "." + this.id, this, params[key]); |
|
} |
|
if (params) { |
|
for (key in this.paper.customAttributes) if (this.paper.customAttributes[has](key) && params[has](key) && R.is(this.paper.customAttributes[key], "function")) { |
|
var par = this.paper.customAttributes[key].apply(this, [].concat(params[key])); |
|
this.attrs[key] = params[key]; |
|
for (var subkey in par) if (par[has](subkey)) { |
|
params[subkey] = par[subkey]; |
|
} |
|
} |
|
// this.paper.canvas.style.display = "none"; |
|
if (params.text && this.type == "text") { |
|
this.textpath.string = params.text; |
|
} |
|
setFillAndStroke(this, params); |
|
// this.paper.canvas.style.display = E; |
|
} |
|
return this; |
|
}; |
|
elproto.toFront = function () { |
|
!this.removed && this.node.parentNode.appendChild(this.node); |
|
this.paper && this.paper.top != this && R._tofront(this, this.paper); |
|
return this; |
|
}; |
|
elproto.toBack = function () { |
|
if (this.removed) { |
|
return this; |
|
} |
|
if (this.node.parentNode.firstChild != this.node) { |
|
this.node.parentNode.insertBefore(this.node, this.node.parentNode.firstChild); |
|
R._toback(this, this.paper); |
|
} |
|
return this; |
|
}; |
|
elproto.insertAfter = function (element) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
if (element.constructor == R.st.constructor) { |
|
element = element[element.length - 1]; |
|
} |
|
if (element.node.nextSibling) { |
|
element.node.parentNode.insertBefore(this.node, element.node.nextSibling); |
|
} else { |
|
element.node.parentNode.appendChild(this.node); |
|
} |
|
R._insertafter(this, element, this.paper); |
|
return this; |
|
}; |
|
elproto.insertBefore = function (element) { |
|
if (this.removed) { |
|
return this; |
|
} |
|
if (element.constructor == R.st.constructor) { |
|
element = element[0]; |
|
} |
|
element.node.parentNode.insertBefore(this.node, element.node); |
|
R._insertbefore(this, element, this.paper); |
|
return this; |
|
}; |
|
elproto.blur = function (size) { |
|
var s = this.node.runtimeStyle, |
|
f = s.filter; |
|
f = f.replace(blurregexp, E); |
|
if (+size !== 0) { |
|
this.attrs.blur = size; |
|
s.filter = f + S + ms + ".Blur(pixelradius=" + (+size || 1.5) + ")"; |
|
s.margin = R.format("-{0}px 0 0 -{0}px", round(+size || 1.5)); |
|
} else { |
|
s.filter = f; |
|
s.margin = 0; |
|
delete this.attrs.blur; |
|
} |
|
return this; |
|
}; |
|
|
|
R._engine.path = function (pathString, vml) { |
|
var el = createNode("shape"); |
|
el.style.cssText = cssDot; |
|
el.coordsize = zoom + S + zoom; |
|
el.coordorigin = vml.coordorigin; |
|
var p = new Element(el, vml), |
|
attr = {fill: "none", stroke: "#000"}; |
|
pathString && (attr.path = pathString); |
|
p.type = "path"; |
|
p.path = []; |
|
p.Path = E; |
|
setFillAndStroke(p, attr); |
|
vml.canvas.appendChild(el); |
|
var skew = createNode("skew"); |
|
skew.on = true; |
|
el.appendChild(skew); |
|
p.skew = skew; |
|
p.transform(E); |
|
return p; |
|
}; |
|
R._engine.rect = function (vml, x, y, w, h, r) { |
|
var path = R._rectPath(x, y, w, h, r), |
|
res = vml.path(path), |
|
a = res.attrs; |
|
res.X = a.x = x; |
|
res.Y = a.y = y; |
|
res.W = a.width = w; |
|
res.H = a.height = h; |
|
a.r = r; |
|
a.path = path; |
|
res.type = "rect"; |
|
return res; |
|
}; |
|
R._engine.ellipse = function (vml, x, y, rx, ry) { |
|
var res = vml.path(), |
|
a = res.attrs; |
|
res.X = x - rx; |
|
res.Y = y - ry; |
|
res.W = rx * 2; |
|
res.H = ry * 2; |
|
res.type = "ellipse"; |
|
setFillAndStroke(res, { |
|
cx: x, |
|
cy: y, |
|
rx: rx, |
|
ry: ry |
|
}); |
|
return res; |
|
}; |
|
R._engine.circle = function (vml, x, y, r) { |
|
var res = vml.path(), |
|
a = res.attrs; |
|
res.X = x - r; |
|
res.Y = y - r; |
|
res.W = res.H = r * 2; |
|
res.type = "circle"; |
|
setFillAndStroke(res, { |
|
cx: x, |
|
cy: y, |
|
r: r |
|
}); |
|
return res; |
|
}; |
|
R._engine.image = function (vml, src, x, y, w, h) { |
|
var path = R._rectPath(x, y, w, h), |
|
res = vml.path(path).attr({stroke: "none"}), |
|
a = res.attrs, |
|
node = res.node, |
|
fill = node.getElementsByTagName(fillString)[0]; |
|
a.src = src; |
|
res.X = a.x = x; |
|
res.Y = a.y = y; |
|
res.W = a.width = w; |
|
res.H = a.height = h; |
|
a.path = path; |
|
res.type = "image"; |
|
fill.parentNode == node && node.removeChild(fill); |
|
fill.rotate = true; |
|
fill.src = src; |
|
fill.type = "tile"; |
|
res._.fillpos = [x, y]; |
|
res._.fillsize = [w, h]; |
|
node.appendChild(fill); |
|
setCoords(res, 1, 1, 0, 0, 0); |
|
return res; |
|
}; |
|
R._engine.text = function (vml, x, y, text) { |
|
var el = createNode("shape"), |
|
path = createNode("path"), |
|
o = createNode("textpath"); |
|
x = x || 0; |
|
y = y || 0; |
|
text = text || ""; |
|
path.v = R.format("m{0},{1}l{2},{1}", round(x * zoom), round(y * zoom), round(x * zoom) + 1); |
|
path.textpathok = true; |
|
o.string = Str(text); |
|
o.on = true; |
|
el.style.cssText = cssDot; |
|
el.coordsize = zoom + S + zoom; |
|
el.coordorigin = "0 0"; |
|
var p = new Element(el, vml), |
|
attr = { |
|
fill: "#000", |
|
stroke: "none", |
|
font: R._availableAttrs.font, |
|
text: text |
|
}; |
|
p.shape = el; |
|
p.path = path; |
|
p.textpath = o; |
|
p.type = "text"; |
|
p.attrs.text = Str(text); |
|
p.attrs.x = x; |
|
p.attrs.y = y; |
|
p.attrs.w = 1; |
|
p.attrs.h = 1; |
|
setFillAndStroke(p, attr); |
|
el.appendChild(o); |
|
el.appendChild(path); |
|
vml.canvas.appendChild(el); |
|
var skew = createNode("skew"); |
|
skew.on = true; |
|
el.appendChild(skew); |
|
p.skew = skew; |
|
p.transform(E); |
|
return p; |
|
}; |
|
R._engine.setSize = function (width, height) { |
|
var cs = this.canvas.style; |
|
this.width = width; |
|
this.height = height; |
|
width == +width && (width += "px"); |
|
height == +height && (height += "px"); |
|
cs.width = width; |
|
cs.height = height; |
|
cs.clip = "rect(0 " + width + " " + height + " 0)"; |
|
if (this._viewBox) { |
|
R._engine.setViewBox.apply(this, this._viewBox); |
|
} |
|
return this; |
|
}; |
|
R._engine.setViewBox = function (x, y, w, h, fit) { |
|
R.eve("raphael.setViewBox", this, this._viewBox, [x, y, w, h, fit]); |
|
var paperSize = this.getSize(), |
|
width = paperSize.width, |
|
height = paperSize.height, |
|
H, W; |
|
if (fit) { |
|
H = height / h; |
|
W = width / w; |
|
if (w * H < width) { |
|
x -= (width - w * H) / 2 / H; |
|
} |
|
if (h * W < height) { |
|
y -= (height - h * W) / 2 / W; |
|
} |
|
} |
|
this._viewBox = [x, y, w, h, !!fit]; |
|
this._viewBoxShift = { |
|
dx: -x, |
|
dy: -y, |
|
scale: paperSize |
|
}; |
|
this.forEach(function (el) { |
|
el.transform("..."); |
|
}); |
|
return this; |
|
}; |
|
var createNode; |
|
R._engine.initWin = function (win) { |
|
var doc = win.document; |
|
if (doc.styleSheets.length < 31) { |
|
doc.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)"); |
|
} else { |
|
// no more room, add to the existing one |
|
// http://msdn.microsoft.com/en-us/library/ms531194%28VS.85%29.aspx |
|
doc.styleSheets[0].addRule(".rvml", "behavior:url(#default#VML)"); |
|
} |
|
try { |
|
!doc.namespaces.rvml && doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml"); |
|
createNode = function (tagName) { |
|
return doc.createElement('<rvml:' + tagName + ' class="rvml">'); |
|
}; |
|
} catch (e) { |
|
createNode = function (tagName) { |
|
return doc.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">'); |
|
}; |
|
} |
|
}; |
|
R._engine.initWin(R._g.win); |
|
R._engine.create = function () { |
|
var con = R._getContainer.apply(0, arguments), |
|
container = con.container, |
|
height = con.height, |
|
s, |
|
width = con.width, |
|
x = con.x, |
|
y = con.y; |
|
if (!container) { |
|
throw new Error("VML container not found."); |
|
} |
|
var res = new R._Paper, |
|
c = res.canvas = R._g.doc.createElement("div"), |
|
cs = c.style; |
|
x = x || 0; |
|
y = y || 0; |
|
width = width || 512; |
|
height = height || 342; |
|
res.width = width; |
|
res.height = height; |
|
width == +width && (width += "px"); |
|
height == +height && (height += "px"); |
|
res.coordsize = zoom * 1e3 + S + zoom * 1e3; |
|
res.coordorigin = "0 0"; |
|
res.span = R._g.doc.createElement("span"); |
|
res.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;"; |
|
c.appendChild(res.span); |
|
cs.cssText = R.format("top:0;left:0;display:inline-block;position:absolute;clip:rect(0 {0} {1} 0);", width, height); |
|
if (container == 1) { |
|
R._g.doc.body.appendChild(c); |
|
cs.left = x + "px"; |
|
cs.top = y + "px"; |
|
cs.position = "absolute"; |
|
} else { |
|
if (container.firstChild) { |
|
container.insertBefore(c, container.firstChild); |
|
} else { |
|
container.appendChild(c); |
|
} |
|
} |
|
res.renderfix = function () {}; |
|
return res; |
|
}; |
|
R.prototype.clear = function () { |
|
R.eve("raphael.clear", this); |
|
this.canvas.innerHTML = E; |
|
this.span = R._g.doc.createElement("span"); |
|
this.span.style.cssText = "position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline;"; |
|
this.canvas.appendChild(this.span); |
|
this.bottom = this.top = null; |
|
}; |
|
R.prototype.remove = function () { |
|
R.eve("raphael.remove", this); |
|
this.canvas.parentNode.removeChild(this.canvas); |
|
for (var i in this) { |
|
this[i] = typeof this[i] == "function" ? R._removedFactory(i) : null; |
|
} |
|
return true; |
|
}; |
|
|
|
var setproto = R.st; |
|
for (var method in elproto) if (elproto[has](method) && !setproto[has](method)) { |
|
setproto[method] = (function (methodname) { |
|
return function () { |
|
var arg = arguments; |
|
return this.forEach(function (el) { |
|
el[methodname].apply(el, arg); |
|
}); |
|
}; |
|
})(method); |
|
} |
|
})); |
|
|
|
// ┌────────────────────────────────────────────────────────────────────┐ \\ |
|
// │ Raphaël 2.1.4 - JavaScript Vector Library │ \\ |
|
// ├────────────────────────────────────────────────────────────────────┤ \\ |
|
// │ Copyright © 2008-2012 Dmitry Baranovskiy (http://raphaeljs.com) │ \\ |
|
// │ Copyright © 2008-2012 Sencha Labs (http://sencha.com) │ \\ |
|
// ├────────────────────────────────────────────────────────────────────┤ \\ |
|
// │ Licensed under the MIT (http://raphaeljs.com/license.html) license.│ \\ |
|
// └────────────────────────────────────────────────────────────────────┘ \\ |
|
|
|
(function (glob, factory) { |
|
if (typeof define === "function" && define.amd) { |
|
define("raphael", ["raphael.core", "raphael.svg", "raphael.vml"], function(Raphael) { |
|
return factory(Raphael); |
|
}); |
|
} else if (typeof exports === "object") { |
|
var raphael = require("raphael.core"); |
|
|
|
require("raphael.svg"); |
|
require("raphael.vml"); |
|
|
|
module.exports = factory(raphael); |
|
} else { |
|
//glob.Raphael = factory(glob.Raphael); |
|
} |
|
}(this, function (Raphael) { |
|
return Raphael.ninja(); |
|
}));/* |
|
* JQuery zTree core v3.5.18 |
|
* http://zTree.me/ |
|
* |
|
* Copyright (c) 2010 Hunter.z |
|
* |
|
* Licensed same as jquery - MIT License |
|
* http://www.opensource.org/licenses/mit-license.php |
|
* |
|
* email: hunter.z@263.net |
|
* Date: 2015-06-18 |
|
*/ |
|
(function($){ |
|
var settings = {}, roots = {}, caches = {}, |
|
//default consts of core |
|
_consts = { |
|
className: { |
|
BUTTON: "button", |
|
LEVEL: "level", |
|
ICO_LOADING: "ico_loading", |
|
SWITCH: "switch" |
|
}, |
|
event: { |
|
NODECREATED: "ztree_nodeCreated", |
|
CLICK: "ztree_click", |
|
EXPAND: "ztree_expand", |
|
COLLAPSE: "ztree_collapse", |
|
ASYNC_SUCCESS: "ztree_async_success", |
|
ASYNC_ERROR: "ztree_async_error", |
|
REMOVE: "ztree_remove", |
|
SELECTED: "ztree_selected", |
|
UNSELECTED: "ztree_unselected" |
|
}, |
|
id: { |
|
A: "_a", |
|
ICON: "_ico", |
|
SPAN: "_span", |
|
SWITCH: "_switch", |
|
UL: "_ul" |
|
}, |
|
line: { |
|
ROOT: "root", |
|
ROOTS: "roots", |
|
CENTER: "center", |
|
BOTTOM: "bottom", |
|
NOLINE: "noline", |
|
LINE: "line" |
|
}, |
|
folder: { |
|
OPEN: "open", |
|
CLOSE: "close", |
|
DOCU: "docu" |
|
}, |
|
node: { |
|
CURSELECTED: "curSelectedNode" |
|
} |
|
}, |
|
//default setting of core |
|
_setting = { |
|
treeId: "", |
|
treeObj: null, |
|
view: { |
|
addDiyDom: null, |
|
autoCancelSelected: true, |
|
dblClickExpand: true, |
|
expandSpeed: "fast", |
|
fontCss: {}, |
|
nameIsHTML: false, |
|
selectedMulti: true, |
|
showIcon: true, |
|
showLine: true, |
|
showTitle: true, |
|
txtSelectedEnable: false |
|
}, |
|
data: { |
|
key: { |
|
children: "children", |
|
name: "name", |
|
title: "", |
|
url: "url" |
|
}, |
|
simpleData: { |
|
enable: false, |
|
idKey: "id", |
|
pIdKey: "pId", |
|
rootPId: null |
|
}, |
|
keep: { |
|
parent: false, |
|
leaf: false |
|
} |
|
}, |
|
async: { |
|
enable: false, |
|
contentType: "application/x-www-form-urlencoded", |
|
type: "post", |
|
dataType: "text", |
|
url: "", |
|
autoParam: [], |
|
otherParam: [], |
|
dataFilter: null |
|
}, |
|
callback: { |
|
beforeAsync:null, |
|
beforeClick:null, |
|
beforeDblClick:null, |
|
beforeRightClick:null, |
|
beforeMouseDown:null, |
|
beforeMouseUp:null, |
|
beforeExpand:null, |
|
beforeCollapse:null, |
|
beforeRemove:null, |
|
|
|
onAsyncError:null, |
|
onAsyncSuccess:null, |
|
onNodeCreated:null, |
|
onClick:null, |
|
onDblClick:null, |
|
onRightClick:null, |
|
onMouseDown:null, |
|
onMouseUp:null, |
|
onExpand:null, |
|
onCollapse:null, |
|
onRemove:null |
|
} |
|
}, |
|
//default root of core |
|
//zTree use root to save full data |
|
_initRoot = function (setting) { |
|
var r = data.getRoot(setting); |
|
if (!r) { |
|
r = {}; |
|
data.setRoot(setting, r); |
|
} |
|
r[setting.data.key.children] = []; |
|
r.expandTriggerFlag = false; |
|
r.curSelectedList = []; |
|
r.noSelection = true; |
|
r.createdNodes = []; |
|
r.zId = 0; |
|
r._ver = (new Date()).getTime(); |
|
}, |
|
//default cache of core |
|
_initCache = function(setting) { |
|
var c = data.getCache(setting); |
|
if (!c) { |
|
c = {}; |
|
data.setCache(setting, c); |
|
} |
|
c.nodes = []; |
|
c.doms = []; |
|
}, |
|
//default bindEvent of core |
|
_bindEvent = function(setting) { |
|
var o = setting.treeObj, |
|
c = consts.event; |
|
o.bind(c.NODECREATED, function (event, treeId, node) { |
|
tools.apply(setting.callback.onNodeCreated, [event, treeId, node]); |
|
}); |
|
|
|
o.bind(c.CLICK, function (event, srcEvent, treeId, node, clickFlag) { |
|
tools.apply(setting.callback.onClick, [srcEvent, treeId, node, clickFlag]); |
|
}); |
|
|
|
o.bind(c.EXPAND, function (event, treeId, node) { |
|
tools.apply(setting.callback.onExpand, [event, treeId, node]); |
|
}); |
|
|
|
o.bind(c.COLLAPSE, function (event, treeId, node) { |
|
tools.apply(setting.callback.onCollapse, [event, treeId, node]); |
|
}); |
|
|
|
o.bind(c.ASYNC_SUCCESS, function (event, treeId, node, msg) { |
|
tools.apply(setting.callback.onAsyncSuccess, [event, treeId, node, msg]); |
|
}); |
|
|
|
o.bind(c.ASYNC_ERROR, function (event, treeId, node, XMLHttpRequest, textStatus, errorThrown) { |
|
tools.apply(setting.callback.onAsyncError, [event, treeId, node, XMLHttpRequest, textStatus, errorThrown]); |
|
}); |
|
|
|
o.bind(c.REMOVE, function (event, treeId, treeNode) { |
|
tools.apply(setting.callback.onRemove, [event, treeId, treeNode]); |
|
}); |
|
|
|
o.bind(c.SELECTED, function (event, srcEvent, treeId, node) { |
|
tools.apply(setting.callback.onSelected, [srcEvent, treeId, node]); |
|
}); |
|
o.bind(c.UNSELECTED, function (event, srcEvent, treeId, node) { |
|
tools.apply(setting.callback.onUnSelected, [srcEvent, treeId, node]); |
|
}); |
|
}, |
|
_unbindEvent = function(setting) { |
|
var o = setting.treeObj, |
|
c = consts.event; |
|
o.unbind(c.NODECREATED) |
|
.unbind(c.CLICK) |
|
.unbind(c.EXPAND) |
|
.unbind(c.COLLAPSE) |
|
.unbind(c.ASYNC_SUCCESS) |
|
.unbind(c.ASYNC_ERROR) |
|
.unbind(c.REMOVE) |
|
.unbind(c.SELECTED) |
|
.unbind(c.UNSELECTED); |
|
}, |
|
//default event proxy of core |
|
_eventProxy = function(event) { |
|
var target = event.target, |
|
setting = data.getSetting(event.data.treeId), |
|
tId = "", node = null, |
|
nodeEventType = "", treeEventType = "", |
|
nodeEventCallback = null, treeEventCallback = null, |
|
tmp = null; |
|
|
|
if (tools.eqs(event.type, "mousedown")) { |
|
treeEventType = "mousedown"; |
|
} else if (tools.eqs(event.type, "mouseup")) { |
|
treeEventType = "mouseup"; |
|
} else if (tools.eqs(event.type, "contextmenu")) { |
|
treeEventType = "contextmenu"; |
|
} else if (tools.eqs(event.type, "click")) { |
|
if (tools.eqs(target.tagName, "span") && target.getAttribute("treeNode"+ consts.id.SWITCH) !== null) { |
|
tId = tools.getNodeMainDom(target).id; |
|
nodeEventType = "switchNode"; |
|
} else { |
|
tmp = tools.getMDom(setting, target, [{tagName:"a", attrName:"treeNode"+consts.id.A}]); |
|
if (tmp) { |
|
tId = tools.getNodeMainDom(tmp).id; |
|
nodeEventType = "clickNode"; |
|
} |
|
} |
|
} else if (tools.eqs(event.type, "dblclick")) { |
|
treeEventType = "dblclick"; |
|
tmp = tools.getMDom(setting, target, [{tagName:"a", attrName:"treeNode"+consts.id.A}]); |
|
if (tmp) { |
|
tId = tools.getNodeMainDom(tmp).id; |
|
nodeEventType = "switchNode"; |
|
} |
|
} |
|
if (treeEventType.length > 0 && tId.length == 0) { |
|
tmp = tools.getMDom(setting, target, [{tagName:"a", attrName:"treeNode"+consts.id.A}]); |
|
if (tmp) {tId = tools.getNodeMainDom(tmp).id;} |
|
} |
|
// event to node |
|
if (tId.length>0) { |
|
node = data.getNodeCache(setting, tId); |
|
switch (nodeEventType) { |
|
case "switchNode" : |
|
if (!node.isParent) { |
|
nodeEventType = ""; |
|
} else if (tools.eqs(event.type, "click") |
|
|| (tools.eqs(event.type, "dblclick") && tools.apply(setting.view.dblClickExpand, [setting.treeId, node], setting.view.dblClickExpand))) { |
|
nodeEventCallback = handler.onSwitchNode; |
|
} else { |
|
nodeEventType = ""; |
|
} |
|
break; |
|
case "clickNode" : |
|
nodeEventCallback = handler.onClickNode; |
|
break; |
|
} |
|
} |
|
// event to zTree |
|
switch (treeEventType) { |
|
case "mousedown" : |
|
treeEventCallback = handler.onZTreeMousedown; |
|
break; |
|
case "mouseup" : |
|
treeEventCallback = handler.onZTreeMouseup; |
|
break; |
|
case "dblclick" : |
|
treeEventCallback = handler.onZTreeDblclick; |
|
break; |
|
case "contextmenu" : |
|
treeEventCallback = handler.onZTreeContextmenu; |
|
break; |
|
} |
|
var proxyResult = { |
|
stop: false, |
|
node: node, |
|
nodeEventType: nodeEventType, |
|
nodeEventCallback: nodeEventCallback, |
|
treeEventType: treeEventType, |
|
treeEventCallback: treeEventCallback |
|
}; |
|
return proxyResult |
|
}, |
|
//default init node of core |
|
_initNode = function(setting, level, n, parentNode, isFirstNode, isLastNode, openFlag) { |
|
if (!n) return; |
|
var r = data.getRoot(setting), |
|
childKey = setting.data.key.children; |
|
n.level = level; |
|
n.tId = setting.treeId + "_" + (++r.zId); |
|
n.parentTId = parentNode ? parentNode.tId : null; |
|
n.open = (typeof n.open == "string") ? tools.eqs(n.open, "true") : !!n.open; |
|
if (n[childKey] && n[childKey].length > 0) { |
|
n.isParent = true; |
|
n.zAsync = true; |
|
} else { |
|
n.isParent = (typeof n.isParent == "string") ? tools.eqs(n.isParent, "true") : !!n.isParent; |
|
n.open = (n.isParent && !setting.async.enable) ? n.open : false; |
|
n.zAsync = !n.isParent; |
|
} |
|
n.isFirstNode = isFirstNode; |
|
n.isLastNode = isLastNode; |
|
n.getParentNode = function() {return data.getNodeCache(setting, n.parentTId);}; |
|
n.getPreNode = function() {return data.getPreNode(setting, n);}; |
|
n.getNextNode = function() {return data.getNextNode(setting, n);}; |
|
n.isAjaxing = false; |
|
data.fixPIdKeyValue(setting, n); |
|
}, |
|
_init = { |
|
bind: [_bindEvent], |
|
unbind: [_unbindEvent], |
|
caches: [_initCache], |
|
nodes: [_initNode], |
|
proxys: [_eventProxy], |
|
roots: [_initRoot], |
|
beforeA: [], |
|
afterA: [], |
|
innerBeforeA: [], |
|
innerAfterA: [], |
|
zTreeTools: [] |
|
}, |
|
//method of operate data |
|
data = { |
|
addNodeCache: function(setting, node) { |
|
data.getCache(setting).nodes[data.getNodeCacheId(node.tId)] = node; |
|
}, |
|
getNodeCacheId: function(tId) { |
|
return tId.substring(tId.lastIndexOf("_")+1); |
|
}, |
|
addAfterA: function(afterA) { |
|
_init.afterA.push(afterA); |
|
}, |
|
addBeforeA: function(beforeA) { |
|
_init.beforeA.push(beforeA); |
|
}, |
|
addInnerAfterA: function(innerAfterA) { |
|
_init.innerAfterA.push(innerAfterA); |
|
}, |
|
addInnerBeforeA: function(innerBeforeA) { |
|
_init.innerBeforeA.push(innerBeforeA); |
|
}, |
|
addInitBind: function(bindEvent) { |
|
_init.bind.push(bindEvent); |
|
}, |
|
addInitUnBind: function(unbindEvent) { |
|
_init.unbind.push(unbindEvent); |
|
}, |
|
addInitCache: function(initCache) { |
|
_init.caches.push(initCache); |
|
}, |
|
addInitNode: function(initNode) { |
|
_init.nodes.push(initNode); |
|
}, |
|
addInitProxy: function(initProxy, isFirst) { |
|
if (!!isFirst) { |
|
_init.proxys.splice(0,0,initProxy); |
|
} else { |
|
_init.proxys.push(initProxy); |
|
} |
|
}, |
|
addInitRoot: function(initRoot) { |
|
_init.roots.push(initRoot); |
|
}, |
|
addNodesData: function(setting, parentNode, nodes) { |
|
var childKey = setting.data.key.children; |
|
if (!parentNode[childKey]) parentNode[childKey] = []; |
|
if (parentNode[childKey].length > 0) { |
|
parentNode[childKey][parentNode[childKey].length - 1].isLastNode = false; |
|
view.setNodeLineIcos(setting, parentNode[childKey][parentNode[childKey].length - 1]); |
|
} |
|
parentNode.isParent = true; |
|
parentNode[childKey] = parentNode[childKey].concat(nodes); |
|
}, |
|
addSelectedNode: function(setting, node) { |
|
var root = data.getRoot(setting); |
|
if (!data.isSelectedNode(setting, node)) { |
|
root.curSelectedList.push(node); |
|
} |
|
}, |
|
addCreatedNode: function(setting, node) { |
|
if (!!setting.callback.onNodeCreated || !!setting.view.addDiyDom) { |
|
var root = data.getRoot(setting); |
|
root.createdNodes.push(node); |
|
} |
|
}, |
|
addZTreeTools: function(zTreeTools) { |
|
_init.zTreeTools.push(zTreeTools); |
|
}, |
|
exSetting: function(s) { |
|
$.extend(true, _setting, s); |
|
}, |
|
fixPIdKeyValue: function(setting, node) { |
|
if (setting.data.simpleData.enable) { |
|
node[setting.data.simpleData.pIdKey] = node.parentTId ? node.getParentNode()[setting.data.simpleData.idKey] : setting.data.simpleData.rootPId; |
|
} |
|
}, |
|
getAfterA: function(setting, node, array) { |
|
for (var i=0, j=_init.afterA.length; i<j; i++) { |
|
_init.afterA[i].apply(this, arguments); |
|
} |
|
}, |
|
getBeforeA: function(setting, node, array) { |
|
for (var i=0, j=_init.beforeA.length; i<j; i++) { |
|
_init.beforeA[i].apply(this, arguments); |
|
} |
|
}, |
|
getInnerAfterA: function(setting, node, array) { |
|
for (var i=0, j=_init.innerAfterA.length; i<j; i++) { |
|
_init.innerAfterA[i].apply(this, arguments); |
|
} |
|
}, |
|
getInnerBeforeA: function(setting, node, array) { |
|
for (var i=0, j=_init.innerBeforeA.length; i<j; i++) { |
|
_init.innerBeforeA[i].apply(this, arguments); |
|
} |
|
}, |
|
getCache: function(setting) { |
|
return caches[setting.treeId]; |
|
}, |
|
getNextNode: function(setting, node) { |
|
if (!node) return null; |
|
var childKey = setting.data.key.children, |
|
p = node.parentTId ? node.getParentNode() : data.getRoot(setting); |
|
for (var i=0, l=p[childKey].length-1; i<=l; i++) { |
|
if (p[childKey][i] === node) { |
|
return (i==l ? null : p[childKey][i+1]); |
|
} |
|
} |
|
return null; |
|
}, |
|
getNodeByParam: function(setting, nodes, key, value) { |
|
if (!nodes || !key) return null; |
|
var childKey = setting.data.key.children; |
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
if (nodes[i][key] == value) { |
|
return nodes[i]; |
|
} |
|
var tmp = data.getNodeByParam(setting, nodes[i][childKey], key, value); |
|
if (tmp) return tmp; |
|
} |
|
return null; |
|
}, |
|
getNodeCache: function(setting, tId) { |
|
if (!tId) return null; |
|
var n = caches[setting.treeId].nodes[data.getNodeCacheId(tId)]; |
|
return n ? n : null; |
|
}, |
|
getNodeName: function(setting, node) { |
|
var nameKey = setting.data.key.name; |
|
return "" + node[nameKey]; |
|
}, |
|
getNodeTitle: function(setting, node) { |
|
var t = setting.data.key.title === "" ? setting.data.key.name : setting.data.key.title; |
|
return "" + node[t]; |
|
}, |
|
getNodes: function(setting) { |
|
return data.getRoot(setting)[setting.data.key.children]; |
|
}, |
|
getNodesByParam: function(setting, nodes, key, value) { |
|
if (!nodes || !key) return []; |
|
var childKey = setting.data.key.children, |
|
result = []; |
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
if (nodes[i][key] == value) { |
|
result.push(nodes[i]); |
|
} |
|
result = result.concat(data.getNodesByParam(setting, nodes[i][childKey], key, value)); |
|
} |
|
return result; |
|
}, |
|
getNodesByParamFuzzy: function(setting, nodes, key, value) { |
|
if (!nodes || !key) return []; |
|
var childKey = setting.data.key.children, |
|
result = []; |
|
value = value.toLowerCase(); |
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
if (typeof nodes[i][key] == "string" && nodes[i][key].toLowerCase().indexOf(value)>-1) { |
|
result.push(nodes[i]); |
|
} |
|
result = result.concat(data.getNodesByParamFuzzy(setting, nodes[i][childKey], key, value)); |
|
} |
|
return result; |
|
}, |
|
getNodesByFilter: function(setting, nodes, filter, isSingle, invokeParam) { |
|
if (!nodes) return (isSingle ? null : []); |
|
var childKey = setting.data.key.children, |
|
result = isSingle ? null : []; |
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
if (tools.apply(filter, [nodes[i], invokeParam], false)) { |
|
if (isSingle) {return nodes[i];} |
|
result.push(nodes[i]); |
|
} |
|
var tmpResult = data.getNodesByFilter(setting, nodes[i][childKey], filter, isSingle, invokeParam); |
|
if (isSingle && !!tmpResult) {return tmpResult;} |
|
result = isSingle ? tmpResult : result.concat(tmpResult); |
|
} |
|
return result; |
|
}, |
|
getPreNode: function(setting, node) { |
|
if (!node) return null; |
|
var childKey = setting.data.key.children, |
|
p = node.parentTId ? node.getParentNode() : data.getRoot(setting); |
|
for (var i=0, l=p[childKey].length; i<l; i++) { |
|
if (p[childKey][i] === node) { |
|
return (i==0 ? null : p[childKey][i-1]); |
|
} |
|
} |
|
return null; |
|
}, |
|
getRoot: function(setting) { |
|
return setting ? roots[setting.treeId] : null; |
|
}, |
|
getRoots: function() { |
|
return roots; |
|
}, |
|
getSetting: function(treeId) { |
|
return settings[treeId]; |
|
}, |
|
getSettings: function() { |
|
return settings; |
|
}, |
|
getZTreeTools: function(treeId) { |
|
var r = this.getRoot(this.getSetting(treeId)); |
|
return r ? r.treeTools : null; |
|
}, |
|
initCache: function(setting) { |
|
for (var i=0, j=_init.caches.length; i<j; i++) { |
|
_init.caches[i].apply(this, arguments); |
|
} |
|
}, |
|
initNode: function(setting, level, node, parentNode, preNode, nextNode) { |
|
for (var i=0, j=_init.nodes.length; i<j; i++) { |
|
_init.nodes[i].apply(this, arguments); |
|
} |
|
}, |
|
initRoot: function(setting) { |
|
for (var i=0, j=_init.roots.length; i<j; i++) { |
|
_init.roots[i].apply(this, arguments); |
|
} |
|
}, |
|
isSelectedNode: function(setting, node) { |
|
var root = data.getRoot(setting); |
|
for (var i=0, j=root.curSelectedList.length; i<j; i++) { |
|
if(node === root.curSelectedList[i]) return true; |
|
} |
|
return false; |
|
}, |
|
removeNodeCache: function(setting, node) { |
|
var childKey = setting.data.key.children; |
|
if (node[childKey]) { |
|
for (var i=0, l=node[childKey].length; i<l; i++) { |
|
arguments.callee(setting, node[childKey][i]); |
|
} |
|
} |
|
data.getCache(setting).nodes[data.getNodeCacheId(node.tId)] = null; |
|
}, |
|
removeSelectedNode: function(setting, node) { |
|
var root = data.getRoot(setting); |
|
for (var i=0, j=root.curSelectedList.length; i<j; i++) { |
|
if(node === root.curSelectedList[i] || !data.getNodeCache(setting, root.curSelectedList[i].tId)) { |
|
root.curSelectedList.splice(i, 1); |
|
i--;j--; |
|
} |
|
} |
|
}, |
|
setCache: function(setting, cache) { |
|
caches[setting.treeId] = cache; |
|
}, |
|
setRoot: function(setting, root) { |
|
roots[setting.treeId] = root; |
|
}, |
|
setZTreeTools: function(setting, zTreeTools) { |
|
for (var i=0, j=_init.zTreeTools.length; i<j; i++) { |
|
_init.zTreeTools[i].apply(this, arguments); |
|
} |
|
}, |
|
transformToArrayFormat: function (setting, nodes) { |
|
if (!nodes) return []; |
|
var childKey = setting.data.key.children, |
|
r = []; |
|
if (tools.isArray(nodes)) { |
|
for (var i=0, l=nodes.length; i<l; i++) { |
|
r.push(nodes[i]); |
|
if (nodes[i][childKey]) |
|
r = r.concat(data.transformToArrayFormat(setting, nodes[i][childKey])); |
|
} |
|
} else { |
|
r.push(nodes); |
|
if (nodes[childKey]) |
|
r = r.concat(data.transformToArrayFormat(setting, nodes[childKey])); |
|
} |
|
return r; |
|
}, |
|
transformTozTreeFormat: function(setting, sNodes) { |
|
var i,l, |
|
key = setting.data.simpleData.idKey, |
|
parentKey = setting.data.simpleData.pIdKey, |
|
childKey = setting.data.key.children; |
|
if (!key || key=="" || !sNodes) return []; |
|
|
|
if (tools.isArray(sNodes)) { |
|
var r = []; |
|
var tmpMap = []; |
|
for (i=0, l=sNodes.length; i<l; i++) { |
|
tmpMap[sNodes[i][key]] = sNodes[i]; |
|
} |
|
for (i=0, l=sNodes.length; i<l; i++) { |
|
if (tmpMap[sNodes[i][parentKey]] && sNodes[i][key] != sNodes[i][parentKey]) { |
|
if (!tmpMap[sNodes[i][parentKey]][childKey]) |
|
tmpMap[sNodes[i][parentKey]][childKey] = []; |
|
tmpMap[sNodes[i][parentKey]][childKey].push(sNodes[i]); |
|
} else { |
|
r.push(sNodes[i]); |
|
} |
|
} |
|
return r; |
|
}else { |
|
return [sNodes]; |
|
} |
|
} |
|
}, |
|
//method of event proxy |
|
event = { |
|
bindEvent: function(setting) { |
|
for (var i=0, j=_init.bind.length; i<j; i++) { |
|
_init.bind[i].apply(this, arguments); |
|
} |
|
}, |
|
unbindEvent: function(setting) { |
|
for (var i=0, j=_init.unbind.length; i<j; i++) { |
|
_init.unbind[i].apply(this, arguments); |
|
} |
|
}, |
|
bindTree: function(setting) { |
|
var eventParam = { |
|
treeId: setting.treeId |
|
}, |
|
o = setting.treeObj; |
|
if (!setting.view.txtSelectedEnable) { |
|
// for can't select text |
|
o.bind('selectstart', function(e){ |
|
var node |
|
var n = e.originalEvent.srcElement.nodeName.toLowerCase(); |
|
return (n === "input" || n === "textarea" ); |
|
}).css({ |
|
"-moz-user-select":"-moz-none" |
|
}); |
|
} |
|
o.bind('click', eventParam, event.proxy); |
|
o.bind('dblclick', eventParam, event.proxy); |
|
o.bind('mouseover', eventParam, event.proxy); |
|
o.bind('mouseout', eventParam, event.proxy); |
|
o.bind('mousedown', eventParam, event.proxy); |
|
o.bind('mouseup', eventParam, event.proxy); |
|
o.bind('contextmenu', eventParam, event.proxy); |
|
}, |
|
unbindTree: function(setting) { |
|
var o = setting.treeObj; |
|
o.unbind('click', event.proxy) |
|
.unbind('dblclick', event.proxy) |
|
.unbind('mouseover', event.proxy) |
|
.unbind('mouseout', event.proxy) |
|
.unbind('mousedown', event.proxy) |
|
.unbind('mouseup', event.proxy) |
|
.unbind('contextmenu', event.proxy); |
|
}, |
|
doProxy: function(e) { |
|
var results = []; |
|
for (var i=0, j=_init.proxys.length; i<j; i++) { |
|
var proxyResult = _init.proxys[i].apply(this, arguments); |
|
results.push(proxyResult); |
|
if (proxyResult.stop) { |
|
break; |
|
} |
|
} |
|
return results; |
|
}, |
|
proxy: function(e) { |
|
var setting = data.getSetting(e.data.treeId); |
|
if (!tools.uCanDo(setting, e)) return true; |
|
var results = event.doProxy(e), |
|
r = true, x = false; |
|
for (var i=0, l=results.length; i<l; i++) { |
|
var proxyResult = results[i]; |
|
if (proxyResult.nodeEventCallback) { |
|
x = true; |
|
r = proxyResult.nodeEventCallback.apply(proxyResult, [e, proxyResult.node]) && r; |
|
} |
|
if (proxyResult.treeEventCallback) { |
|
x = true; |
|
r = proxyResult.treeEventCallback.apply(proxyResult, [e, proxyResult.node]) && r; |
|
} |
|
} |
|
return r; |
|
} |
|
}, |
|
//method of event handler |
|
handler = { |
|
onSwitchNode: function (event, node) { |
|
var setting = data.getSetting(event.data.treeId); |
|
if (node.open) { |
|
if (tools.apply(setting.callback.beforeCollapse, [setting.treeId, node], true) == false) return true; |
|
data.getRoot(setting).expandTriggerFlag = true; |
|
view.switchNode(setting, node); |
|
} else { |
|
if (tools.apply(setting.callback.beforeExpand, [setting.treeId, node], true) == false) return true; |
|
data.getRoot(setting).expandTriggerFlag = true; |
|
view.switchNode(setting, node); |
|
} |
|
return true; |
|
}, |
|
onClickNode: function (event, node) { |
|
var setting = data.getSetting(event.data.treeId), |
|
clickFlag = ( (setting.view.autoCancelSelected && (event.ctrlKey || event.metaKey)) && data.isSelectedNode(setting, node)) ? 0 : (setting.view.autoCancelSelected && (event.ctrlKey || event.metaKey) && setting.view.selectedMulti) ? 2 : 1; |
|
if (tools.apply(setting.callback.beforeClick, [setting.treeId, node, clickFlag], true) == false) return true; |
|
if (clickFlag === 0) { |
|
view.cancelPreSelectedNode(setting, node); |
|
} else { |
|
view.selectNode(setting, node, clickFlag === 2); |
|
} |
|
setting.treeObj.trigger(consts.event.CLICK, [event, setting.treeId, node, clickFlag]); |
|
return true; |
|
}, |
|
onZTreeMousedown: function(event, node) { |
|
var setting = data.getSetting(event.data.treeId); |
|
if (tools.apply(setting.callback.beforeMouseDown, [setting.treeId, node], true)) { |
|
tools.apply(setting.callback.onMouseDown, [event, setting.treeId, node]); |
|
} |
|
return true; |
|
}, |
|
onZTreeMouseup: function(event, node) { |
|
var setting = data.getSetting(event.data.treeId); |
|
if (tools.apply(setting.callback.beforeMouseUp, [setting.treeId, node], true)) { |
|
tools.apply(setting.callback.onMouseUp, [event, setting.treeId, node]); |
|
} |
|
return true; |
|
}, |
|
onZTreeDblclick: function(event, node) { |
|
var setting = data.getSetting(event.data.treeId); |
|
if (tools.apply(setting.callback.beforeDblClick, [setting.treeId, node], true)) { |
|
tools.apply(setting.callback.onDblClick, [event, setting.treeId, node]); |
|
} |
|
return true; |
|
}, |
|
onZTreeContextmenu: function(event, node) { |
|
var setting = data.getSetting(event.data.treeId); |
|
if (tools.apply(setting.callback.beforeRightClick, [setting.treeId, node], true)) { |
|
tools.apply(setting.callback.onRightClick, [event, setting.treeId, node]); |
|
} |
|
return (typeof setting.callback.onRightClick) != "function"; |
|
} |
|
}, |
|
//method of tools for zTree |
|
tools = { |
|
apply: function(fun, param, defaultValue) { |
|
if ((typeof fun) == "function") { |
|
return fun.apply(zt, param?param:[]); |
|
} |
|
return defaultValue; |
|
}, |
|
canAsync: function(setting, node) { |
|
var childKey = setting.data.key.children; |
|
return (setting.async.enable && node && node.isParent && !(node.zAsync || (node[childKey] && node[childKey].length > 0))); |
|
}, |
|
clone: function (obj){ |
|
if (obj === null) return null; |
|
var o = tools.isArray(obj) ? [] : {}; |
|
for(var i in obj){ |
|
o[i] = (obj[i] instanceof Date) ? new Date(obj[i].getTime()) : (typeof obj[i] === "object" ? arguments.callee(obj[i]) : obj[i]); |
|
} |
|
return o; |
|
}, |
|
eqs: function(str1, str2) { |
|
return str1.toLowerCase() === str2.toLowerCase(); |
|
}, |
|
isArray: function(arr) { |
|
return Object.prototype.toString.apply(arr) === "[object Array]"; |
|
}, |
|
$: function(node, exp, setting) { |
|
if (!!exp && typeof exp != "string") { |
|
setting = exp; |
|
exp = ""; |
|
} |
|
if (typeof node == "string") { |
|
return $(node, setting ? setting.treeObj.get(0).ownerDocument : null); |
|
} else { |
|
return $("#" + node.tId + exp, setting ? setting.treeObj : null); |
|
} |
|
}, |
|
getMDom: function (setting, curDom, targetExpr) { |
|
if (!curDom) return null; |
|
while (curDom && curDom.id !== setting.treeId) { |
|
for (var i=0, l=targetExpr.length; curDom.tagName && i<l; i++) { |
|
if (tools.eqs(curDom.tagName, targetExpr[i].tagName) && curDom.getAttribute(targetExpr[i].attrName) !== null) { |
|
return curDom; |
|
} |
|
} |
|
curDom = curDom.parentNode; |
|
} |
|
return null; |
|
}, |
|
getNodeMainDom:function(target) { |
|
return ($(target).parent("li").get(0) || $(target).parentsUntil("li").parent().get(0)); |
|
}, |
|
isChildOrSelf: function(dom, parentId) { |
|
return ( $(dom).closest("#" + parentId).length> 0 ); |
|
}, |
|
uCanDo: function(setting, e) { |
|
return true; |
|
} |
|
}, |
|
//method of operate ztree dom |
|
view = { |
|
addNodes: function(setting, parentNode, newNodes, isSilent) { |
|
if (setting.data.keep.leaf && parentNode && !parentNode.isParent) { |
|
return; |
|
} |
|
if (!tools.isArray(newNodes)) { |
|
newNodes = [newNodes]; |
|
} |
|
if (setting.data.simpleData.enable) { |
|
newNodes = data.transformTozTreeFormat(setting, newNodes); |
|
} |
|
if (parentNode) { |
|
var target_switchObj = $$(parentNode, consts.id.SWITCH, setting), |
|
target_icoObj = $$(parentNode, consts.id.ICON, setting), |
|
target_ulObj = $$(parentNode, consts.id.UL, setting); |
|
|
|
if (!parentNode.open) { |
|
view.replaceSwitchClass(parentNode, target_switchObj, consts.folder.CLOSE); |
|
view.replaceIcoClass(parentNode, target_icoObj, consts.folder.CLOSE); |
|
parentNode.open = false; |
|
target_ulObj.css({ |
|
"display": "none" |
|
}); |
|
} |
|
|
|
data.addNodesData(setting, parentNode, newNodes); |
|
view.createNodes(setting, parentNode.level + 1, newNodes, parentNode); |
|
if (!isSilent) { |
|
view.expandCollapseParentNode(setting, parentNode, true); |
|
} |
|
} else { |
|
data.addNodesData(setting, data.getRoot(setting), newNodes); |
|
view.createNodes(setting, 0, newNodes, null); |
|
} |
|
}, |
|
appendNodes: function(setting, level, nodes, parentNode, initFlag, openFlag) { |
|
if (!nodes) return []; |
|
var html = [], |
|
childKey = setting.data.key.children; |
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
var node = nodes[i]; |
|
if (initFlag) { |
|
var tmpPNode = (parentNode) ? parentNode: data.getRoot(setting), |
|
tmpPChild = tmpPNode[childKey], |
|
isFirstNode = ((tmpPChild.length == nodes.length) && (i == 0)), |
|
isLastNode = (i == (nodes.length - 1)); |
|
data.initNode(setting, level, node, parentNode, isFirstNode, isLastNode, openFlag); |
|
data.addNodeCache(setting, node); |
|
} |
|
|
|
var childHtml = []; |
|
if (node[childKey] && node[childKey].length > 0) { |
|
//make child html first, because checkType |
|
childHtml = view.appendNodes(setting, level + 1, node[childKey], node, initFlag, openFlag && node.open); |
|
} |
|
if (openFlag) { |
|
|
|
view.makeDOMNodeMainBefore(html, setting, node); |
|
view.makeDOMNodeLine(html, setting, node); |
|
data.getBeforeA(setting, node, html); |
|
view.makeDOMNodeNameBefore(html, setting, node); |
|
data.getInnerBeforeA(setting, node, html); |
|
view.makeDOMNodeIcon(html, setting, node); |
|
data.getInnerAfterA(setting, node, html); |
|
view.makeDOMNodeNameAfter(html, setting, node); |
|
data.getAfterA(setting, node, html); |
|
if (node.isParent && node.open) { |
|
view.makeUlHtml(setting, node, html, childHtml.join('')); |
|
} |
|
view.makeDOMNodeMainAfter(html, setting, node); |
|
data.addCreatedNode(setting, node); |
|
} |
|
} |
|
return html; |
|
}, |
|
appendParentULDom: function(setting, node) { |
|
var html = [], |
|
nObj = $$(node, setting); |
|
if (!nObj.get(0) && !!node.parentTId) { |
|
view.appendParentULDom(setting, node.getParentNode()); |
|
nObj = $$(node, setting); |
|
} |
|
var ulObj = $$(node, consts.id.UL, setting); |
|
if (ulObj.get(0)) { |
|
ulObj.remove(); |
|
} |
|
var childKey = setting.data.key.children, |
|
childHtml = view.appendNodes(setting, node.level+1, node[childKey], node, false, true); |
|
view.makeUlHtml(setting, node, html, childHtml.join('')); |
|
nObj.append(html.join('')); |
|
}, |
|
asyncNode: function(setting, node, isSilent, callback) { |
|
var i, l; |
|
if (node && !node.isParent) { |
|
tools.apply(callback); |
|
return false; |
|
} else if (node && node.isAjaxing) { |
|
return false; |
|
} else if (tools.apply(setting.callback.beforeAsync, [setting.treeId, node], true) == false) { |
|
tools.apply(callback); |
|
return false; |
|
} |
|
if (node) { |
|
node.isAjaxing = true; |
|
var icoObj = $$(node, consts.id.ICON, setting); |
|
icoObj.attr({"style":"", "class":consts.className.BUTTON + " " + consts.className.ICO_LOADING}); |
|
} |
|
|
|
var tmpParam = {}; |
|
for (i = 0, l = setting.async.autoParam.length; node && i < l; i++) { |
|
var pKey = setting.async.autoParam[i].split("="), spKey = pKey; |
|
if (pKey.length>1) { |
|
spKey = pKey[1]; |
|
pKey = pKey[0]; |
|
} |
|
tmpParam[spKey] = node[pKey]; |
|
} |
|
if (tools.isArray(setting.async.otherParam)) { |
|
for (i = 0, l = setting.async.otherParam.length; i < l; i += 2) { |
|
tmpParam[setting.async.otherParam[i]] = setting.async.otherParam[i + 1]; |
|
} |
|
} else { |
|
for (var p in setting.async.otherParam) { |
|
tmpParam[p] = setting.async.otherParam[p]; |
|
} |
|
} |
|
|
|
var _tmpV = data.getRoot(setting)._ver; |
|
$.ajax({ |
|
contentType: setting.async.contentType, |
|
cache: false, |
|
type: setting.async.type, |
|
url: tools.apply(setting.async.url, [setting.treeId, node], setting.async.url), |
|
data: tmpParam, |
|
dataType: setting.async.dataType, |
|
success: function(msg) { |
|
if (_tmpV != data.getRoot(setting)._ver) { |
|
return; |
|
} |
|
var newNodes = []; |
|
try { |
|
if (!msg || msg.length == 0) { |
|
newNodes = []; |
|
} else if (typeof msg == "string") { |
|
newNodes = eval("(" + msg + ")"); |
|
} else { |
|
newNodes = msg; |
|
} |
|
} catch(err) { |
|
newNodes = msg; |
|
} |
|
|
|
if (node) { |
|
node.isAjaxing = null; |
|
node.zAsync = true; |
|
} |
|
view.setNodeLineIcos(setting, node); |
|
if (newNodes && newNodes !== "") { |
|
newNodes = tools.apply(setting.async.dataFilter, [setting.treeId, node, newNodes], newNodes); |
|
view.addNodes(setting, node, !!newNodes ? tools.clone(newNodes) : [], !!isSilent); |
|
} else { |
|
view.addNodes(setting, node, [], !!isSilent); |
|
} |
|
setting.treeObj.trigger(consts.event.ASYNC_SUCCESS, [setting.treeId, node, msg]); |
|
tools.apply(callback); |
|
}, |
|
error: function(XMLHttpRequest, textStatus, errorThrown) { |
|
if (_tmpV != data.getRoot(setting)._ver) { |
|
return; |
|
} |
|
if (node) node.isAjaxing = null; |
|
view.setNodeLineIcos(setting, node); |
|
setting.treeObj.trigger(consts.event.ASYNC_ERROR, [setting.treeId, node, XMLHttpRequest, textStatus, errorThrown]); |
|
} |
|
}); |
|
return true; |
|
}, |
|
cancelPreSelectedNode: function (setting, node, excludeNode) { |
|
var list = data.getRoot(setting).curSelectedList, |
|
i, n; |
|
for (i=list.length-1; i>=0; i--) { |
|
n = list[i]; |
|
if (node === n || (!node && (!excludeNode || excludeNode !== n))) { |
|
$$(n, consts.id.A, setting).removeClass(consts.node.CURSELECTED); |
|
if (node) { |
|
data.removeSelectedNode(setting, node); |
|
setting.treeObj.trigger(consts.event.UNSELECTED, [event, setting.treeId, n]); |
|
break; |
|
} else { |
|
list.splice(i, 1); |
|
setting.treeObj.trigger(consts.event.UNSELECTED, [event, setting.treeId, n]); |
|
} |
|
} |
|
} |
|
}, |
|
createNodeCallback: function(setting) { |
|
if (!!setting.callback.onNodeCreated || !!setting.view.addDiyDom) { |
|
var root = data.getRoot(setting); |
|
while (root.createdNodes.length>0) { |
|
var node = root.createdNodes.shift(); |
|
tools.apply(setting.view.addDiyDom, [setting.treeId, node]); |
|
if (!!setting.callback.onNodeCreated) { |
|
setting.treeObj.trigger(consts.event.NODECREATED, [setting.treeId, node]); |
|
} |
|
} |
|
} |
|
}, |
|
createNodes: function(setting, level, nodes, parentNode) { |
|
if (!nodes || nodes.length == 0) return; |
|
var root = data.getRoot(setting), |
|
childKey = setting.data.key.children, |
|
openFlag = !parentNode || parentNode.open || !!$$(parentNode[childKey][0], setting).get(0); |
|
root.createdNodes = []; |
|
var zTreeHtml = view.appendNodes(setting, level, nodes, parentNode, true, openFlag); |
|
if (!parentNode) { |
|
setting.treeObj.append(zTreeHtml.join('')); |
|
} else { |
|
var ulObj = $$(parentNode, consts.id.UL, setting); |
|
if (ulObj.get(0)) { |
|
ulObj.append(zTreeHtml.join('')); |
|
} |
|
} |
|
view.createNodeCallback(setting); |
|
}, |
|
destroy: function(setting) { |
|
if (!setting) return; |
|
data.initCache(setting); |
|
data.initRoot(setting); |
|
event.unbindTree(setting); |
|
event.unbindEvent(setting); |
|
setting.treeObj.empty(); |
|
delete settings[setting.treeId]; |
|
}, |
|
expandCollapseNode: function(setting, node, expandFlag, animateFlag, callback) { |
|
var root = data.getRoot(setting), |
|
childKey = setting.data.key.children; |
|
if (!node) { |
|
tools.apply(callback, []); |
|
return; |
|
} |
|
if (root.expandTriggerFlag) { |
|
var _callback = callback; |
|
callback = function(){ |
|
if (_callback) _callback(); |
|
if (node.open) { |
|
setting.treeObj.trigger(consts.event.EXPAND, [setting.treeId, node]); |
|
} else { |
|
setting.treeObj.trigger(consts.event.COLLAPSE, [setting.treeId, node]); |
|
} |
|
}; |
|
root.expandTriggerFlag = false; |
|
} |
|
if (!node.open && node.isParent && ((!$$(node, consts.id.UL, setting).get(0)) || (node[childKey] && node[childKey].length>0 && !$$(node[childKey][0], setting).get(0)))) { |
|
view.appendParentULDom(setting, node); |
|
view.createNodeCallback(setting); |
|
} |
|
if (node.open == expandFlag) { |
|
tools.apply(callback, []); |
|
return; |
|
} |
|
var ulObj = $$(node, consts.id.UL, setting), |
|
switchObj = $$(node, consts.id.SWITCH, setting), |
|
icoObj = $$(node, consts.id.ICON, setting); |
|
|
|
if (node.isParent) { |
|
node.open = !node.open; |
|
if (node.iconOpen && node.iconClose) { |
|
icoObj.attr("style", view.makeNodeIcoStyle(setting, node)); |
|
} |
|
|
|
if (node.open) { |
|
view.replaceSwitchClass(node, switchObj, consts.folder.OPEN); |
|
view.replaceIcoClass(node, icoObj, consts.folder.OPEN); |
|
if (animateFlag == false || setting.view.expandSpeed == "") { |
|
ulObj.show(); |
|
tools.apply(callback, []); |
|
} else { |
|
if (node[childKey] && node[childKey].length > 0) { |
|
ulObj.slideDown(setting.view.expandSpeed, callback); |
|
} else { |
|
ulObj.show(); |
|
tools.apply(callback, []); |
|
} |
|
} |
|
} else { |
|
view.replaceSwitchClass(node, switchObj, consts.folder.CLOSE); |
|
view.replaceIcoClass(node, icoObj, consts.folder.CLOSE); |
|
if (animateFlag == false || setting.view.expandSpeed == "" || !(node[childKey] && node[childKey].length > 0)) { |
|
ulObj.hide(); |
|
tools.apply(callback, []); |
|
} else { |
|
ulObj.slideUp(setting.view.expandSpeed, callback); |
|
} |
|
} |
|
} else { |
|
tools.apply(callback, []); |
|
} |
|
}, |
|
expandCollapseParentNode: function(setting, node, expandFlag, animateFlag, callback) { |
|
if (!node) return; |
|
if (!node.parentTId) { |
|
view.expandCollapseNode(setting, node, expandFlag, animateFlag, callback); |
|
return; |
|
} else { |
|
view.expandCollapseNode(setting, node, expandFlag, animateFlag); |
|
} |
|
if (node.parentTId) { |
|
view.expandCollapseParentNode(setting, node.getParentNode(), expandFlag, animateFlag, callback); |
|
} |
|
}, |
|
expandCollapseSonNode: function(setting, node, expandFlag, animateFlag, callback) { |
|
var root = data.getRoot(setting), |
|
childKey = setting.data.key.children, |
|
treeNodes = (node) ? node[childKey]: root[childKey], |
|
selfAnimateSign = (node) ? false : animateFlag, |
|
expandTriggerFlag = data.getRoot(setting).expandTriggerFlag; |
|
data.getRoot(setting).expandTriggerFlag = false; |
|
if (treeNodes) { |
|
for (var i = 0, l = treeNodes.length; i < l; i++) { |
|
if (treeNodes[i]) view.expandCollapseSonNode(setting, treeNodes[i], expandFlag, selfAnimateSign); |
|
} |
|
} |
|
data.getRoot(setting).expandTriggerFlag = expandTriggerFlag; |
|
view.expandCollapseNode(setting, node, expandFlag, animateFlag, callback ); |
|
}, |
|
isSelectedNode: function (setting, node) { |
|
if (!node) { |
|
return false; |
|
} |
|
var list = data.getRoot(setting).curSelectedList, |
|
i; |
|
for (i=list.length-1; i>=0; i--) { |
|
if (node === list[i]) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
}, |
|
makeDOMNodeIcon: function(html, setting, node) { |
|
var nameStr = data.getNodeName(setting, node), |
|
name = setting.view.nameIsHTML ? nameStr : nameStr.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); |
|
html.push("<span id='", node.tId, consts.id.ICON, |
|
"' title='' treeNode", consts.id.ICON," class='", view.makeNodeIcoClass(setting, node), |
|
"' style='", view.makeNodeIcoStyle(setting, node), "'></span><span id='", node.tId, consts.id.SPAN, |
|
"'>",name,"</span>"); |
|
}, |
|
makeDOMNodeLine: function(html, setting, node) { |
|
html.push("<span id='", node.tId, consts.id.SWITCH, "' title='' class='", view.makeNodeLineClass(setting, node), "' treeNode", consts.id.SWITCH,"></span>"); |
|
}, |
|
makeDOMNodeMainAfter: function(html, setting, node) { |
|
html.push("</li>"); |
|
}, |
|
makeDOMNodeMainBefore: function(html, setting, node) { |
|
html.push("<li id='", node.tId, "' class='", consts.className.LEVEL, node.level,"' tabindex='0' hidefocus='true' treenode>"); |
|
}, |
|
makeDOMNodeNameAfter: function(html, setting, node) { |
|
html.push("</a>"); |
|
}, |
|
makeDOMNodeNameBefore: function(html, setting, node) { |
|
var title = data.getNodeTitle(setting, node), |
|
url = view.makeNodeUrl(setting, node), |
|
fontcss = view.makeNodeFontCss(setting, node), |
|
fontStyle = []; |
|
for (var f in fontcss) { |
|
fontStyle.push(f, ":", fontcss[f], ";"); |
|
} |
|
html.push("<a id='", node.tId, consts.id.A, "' class='", consts.className.LEVEL, node.level,"' treeNode", consts.id.A," onclick=\"", (node.click || ''), |
|
"\" ", ((url != null && url.length > 0) ? "href='" + url + "'" : ""), " target='",view.makeNodeTarget(node),"' style='", fontStyle.join(''), |
|
"'"); |
|
if (tools.apply(setting.view.showTitle, [setting.treeId, node], setting.view.showTitle) && title) {html.push("title='", title.replace(/'/g,"'").replace(/</g,'<').replace(/>/g,'>'),"'");} |
|
html.push(">"); |
|
}, |
|
makeNodeFontCss: function(setting, node) { |
|
var fontCss = tools.apply(setting.view.fontCss, [setting.treeId, node], setting.view.fontCss); |
|
return (fontCss && ((typeof fontCss) != "function")) ? fontCss : {}; |
|
}, |
|
makeNodeIcoClass: function(setting, node) { |
|
var icoCss = ["ico"]; |
|
if (!node.isAjaxing) { |
|
icoCss[0] = (node.iconSkin ? node.iconSkin + "_" : "") + icoCss[0]; |
|
if (node.isParent) { |
|
icoCss.push(node.open ? consts.folder.OPEN : consts.folder.CLOSE); |
|
} else { |
|
icoCss.push(consts.folder.DOCU); |
|
} |
|
} |
|
return consts.className.BUTTON + " " + icoCss.join('_'); |
|
}, |
|
makeNodeIcoStyle: function(setting, node) { |
|
var icoStyle = []; |
|
if (!node.isAjaxing) { |
|
var icon = (node.isParent && node.iconOpen && node.iconClose) ? (node.open ? node.iconOpen : node.iconClose) : node.icon; |
|
if (icon) icoStyle.push("background:url(", icon, ") 0 0 no-repeat;"); |
|
if (setting.view.showIcon == false || !tools.apply(setting.view.showIcon, [setting.treeId, node], true)) { |
|
icoStyle.push("width:0px;height:0px;"); |
|
} |
|
} |
|
return icoStyle.join(''); |
|
}, |
|
makeNodeLineClass: function(setting, node) { |
|
var lineClass = []; |
|
if (setting.view.showLine) { |
|
if (node.level == 0 && node.isFirstNode && node.isLastNode) { |
|
lineClass.push(consts.line.ROOT); |
|
} else if (node.level == 0 && node.isFirstNode) { |
|
lineClass.push(consts.line.ROOTS); |
|
} else if (node.isLastNode) { |
|
lineClass.push(consts.line.BOTTOM); |
|
} else { |
|
lineClass.push(consts.line.CENTER); |
|
} |
|
} else { |
|
lineClass.push(consts.line.NOLINE); |
|
} |
|
if (node.isParent) { |
|
lineClass.push(node.open ? consts.folder.OPEN : consts.folder.CLOSE); |
|
} else { |
|
lineClass.push(consts.folder.DOCU); |
|
} |
|
return view.makeNodeLineClassEx(node) + lineClass.join('_'); |
|
}, |
|
makeNodeLineClassEx: function(node) { |
|
return consts.className.BUTTON + " " + consts.className.LEVEL + node.level + " " + consts.className.SWITCH + " "; |
|
}, |
|
makeNodeTarget: function(node) { |
|
return (node.target || "_blank"); |
|
}, |
|
makeNodeUrl: function(setting, node) { |
|
var urlKey = setting.data.key.url; |
|
return node[urlKey] ? node[urlKey] : null; |
|
}, |
|
makeUlHtml: function(setting, node, html, content) { |
|
html.push("<ul id='", node.tId, consts.id.UL, "' class='", consts.className.LEVEL, node.level, " ", view.makeUlLineClass(setting, node), "' style='display:", (node.open ? "block": "none"),"'>"); |
|
html.push(content); |
|
html.push("</ul>"); |
|
}, |
|
makeUlLineClass: function(setting, node) { |
|
return ((setting.view.showLine && !node.isLastNode) ? consts.line.LINE : ""); |
|
}, |
|
removeChildNodes: function(setting, node) { |
|
if (!node) return; |
|
var childKey = setting.data.key.children, |
|
nodes = node[childKey]; |
|
if (!nodes) return; |
|
|
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
data.removeNodeCache(setting, nodes[i]); |
|
} |
|
data.removeSelectedNode(setting); |
|
delete node[childKey]; |
|
|
|
if (!setting.data.keep.parent) { |
|
node.isParent = false; |
|
node.open = false; |
|
var tmp_switchObj = $$(node, consts.id.SWITCH, setting), |
|
tmp_icoObj = $$(node, consts.id.ICON, setting); |
|
view.replaceSwitchClass(node, tmp_switchObj, consts.folder.DOCU); |
|
view.replaceIcoClass(node, tmp_icoObj, consts.folder.DOCU); |
|
$$(node, consts.id.UL, setting).remove(); |
|
} else { |
|
$$(node, consts.id.UL, setting).empty(); |
|
} |
|
}, |
|
setFirstNode: function(setting, parentNode) { |
|
var childKey = setting.data.key.children, childLength = parentNode[childKey].length; |
|
if ( childLength > 0) { |
|
parentNode[childKey][0].isFirstNode = true; |
|
} |
|
}, |
|
setLastNode: function(setting, parentNode) { |
|
var childKey = setting.data.key.children, childLength = parentNode[childKey].length; |
|
if ( childLength > 0) { |
|
parentNode[childKey][childLength - 1].isLastNode = true; |
|
} |
|
}, |
|
removeNode: function(setting, node) { |
|
var root = data.getRoot(setting), |
|
childKey = setting.data.key.children, |
|
parentNode = (node.parentTId) ? node.getParentNode() : root; |
|
|
|
node.isFirstNode = false; |
|
node.isLastNode = false; |
|
node.getPreNode = function() {return null;}; |
|
node.getNextNode = function() {return null;}; |
|
|
|
if (!data.getNodeCache(setting, node.tId)) { |
|
return; |
|
} |
|
|
|
$$(node, setting).remove(); |
|
data.removeNodeCache(setting, node); |
|
data.removeSelectedNode(setting, node); |
|
|
|
for (var i = 0, l = parentNode[childKey].length; i < l; i++) { |
|
if (parentNode[childKey][i].tId == node.tId) { |
|
parentNode[childKey].splice(i, 1); |
|
break; |
|
} |
|
} |
|
view.setFirstNode(setting, parentNode); |
|
view.setLastNode(setting, parentNode); |
|
|
|
var tmp_ulObj,tmp_switchObj,tmp_icoObj, |
|
childLength = parentNode[childKey].length; |
|
|
|
//repair nodes old parent |
|
if (!setting.data.keep.parent && childLength == 0) { |
|
//old parentNode has no child nodes |
|
parentNode.isParent = false; |
|
parentNode.open = false; |
|
tmp_ulObj = $$(parentNode, consts.id.UL, setting); |
|
tmp_switchObj = $$(parentNode, consts.id.SWITCH, setting); |
|
tmp_icoObj = $$(parentNode, consts.id.ICON, setting); |
|
view.replaceSwitchClass(parentNode, tmp_switchObj, consts.folder.DOCU); |
|
view.replaceIcoClass(parentNode, tmp_icoObj, consts.folder.DOCU); |
|
tmp_ulObj.css("display", "none"); |
|
|
|
} else if (setting.view.showLine && childLength > 0) { |
|
//old parentNode has child nodes |
|
var newLast = parentNode[childKey][childLength - 1]; |
|
tmp_ulObj = $$(newLast, consts.id.UL, setting); |
|
tmp_switchObj = $$(newLast, consts.id.SWITCH, setting); |
|
tmp_icoObj = $$(newLast, consts.id.ICON, setting); |
|
if (parentNode == root) { |
|
if (parentNode[childKey].length == 1) { |
|
//node was root, and ztree has only one root after move node |
|
view.replaceSwitchClass(newLast, tmp_switchObj, consts.line.ROOT); |
|
} else { |
|
var tmp_first_switchObj = $$(parentNode[childKey][0], consts.id.SWITCH, setting); |
|
view.replaceSwitchClass(parentNode[childKey][0], tmp_first_switchObj, consts.line.ROOTS); |
|
view.replaceSwitchClass(newLast, tmp_switchObj, consts.line.BOTTOM); |
|
} |
|
} else { |
|
view.replaceSwitchClass(newLast, tmp_switchObj, consts.line.BOTTOM); |
|
} |
|
tmp_ulObj.removeClass(consts.line.LINE); |
|
} |
|
}, |
|
replaceIcoClass: function(node, obj, newName) { |
|
if (!obj || node.isAjaxing) return; |
|
var tmpName = obj.attr("class"); |
|
if (tmpName == undefined) return; |
|
var tmpList = tmpName.split("_"); |
|
switch (newName) { |
|
case consts.folder.OPEN: |
|
case consts.folder.CLOSE: |
|
case consts.folder.DOCU: |
|
tmpList[tmpList.length-1] = newName; |
|
break; |
|
} |
|
obj.attr("class", tmpList.join("_")); |
|
}, |
|
replaceSwitchClass: function(node, obj, newName) { |
|
if (!obj) return; |
|
var tmpName = obj.attr("class"); |
|
if (tmpName == undefined) return; |
|
var tmpList = tmpName.split("_"); |
|
switch (newName) { |
|
case consts.line.ROOT: |
|
case consts.line.ROOTS: |
|
case consts.line.CENTER: |
|
case consts.line.BOTTOM: |
|
case consts.line.NOLINE: |
|
tmpList[0] = view.makeNodeLineClassEx(node) + newName; |
|
break; |
|
case consts.folder.OPEN: |
|
case consts.folder.CLOSE: |
|
case consts.folder.DOCU: |
|
tmpList[1] = newName; |
|
break; |
|
} |
|
obj.attr("class", tmpList.join("_")); |
|
if (newName !== consts.folder.DOCU) { |
|
obj.removeAttr("disabled"); |
|
} else { |
|
obj.attr("disabled", "disabled"); |
|
} |
|
}, |
|
selectNode: function(setting, node, addFlag) { |
|
if (!addFlag) { |
|
view.cancelPreSelectedNode(setting, null, node); |
|
} |
|
$$(node, consts.id.A, setting).addClass(consts.node.CURSELECTED); |
|
data.addSelectedNode(setting, node); |
|
setting.treeObj.trigger(consts.event.SELECTED, [event, setting.treeId, node]); |
|
}, |
|
setNodeFontCss: function(setting, treeNode) { |
|
var aObj = $$(treeNode, consts.id.A, setting), |
|
fontCss = view.makeNodeFontCss(setting, treeNode); |
|
if (fontCss) { |
|
aObj.css(fontCss); |
|
} |
|
}, |
|
setNodeLineIcos: function(setting, node) { |
|
if (!node) return; |
|
var switchObj = $$(node, consts.id.SWITCH, setting), |
|
ulObj = $$(node, consts.id.UL, setting), |
|
icoObj = $$(node, consts.id.ICON, setting), |
|
ulLine = view.makeUlLineClass(setting, node); |
|
if (ulLine.length==0) { |
|
ulObj.removeClass(consts.line.LINE); |
|
} else { |
|
ulObj.addClass(ulLine); |
|
} |
|
switchObj.attr("class", view.makeNodeLineClass(setting, node)); |
|
if (node.isParent) { |
|
switchObj.removeAttr("disabled"); |
|
} else { |
|
switchObj.attr("disabled", "disabled"); |
|
} |
|
icoObj.removeAttr("style"); |
|
icoObj.attr("style", view.makeNodeIcoStyle(setting, node)); |
|
icoObj.attr("class", view.makeNodeIcoClass(setting, node)); |
|
}, |
|
setNodeName: function(setting, node) { |
|
var title = data.getNodeTitle(setting, node), |
|
nObj = $$(node, consts.id.SPAN, setting); |
|
nObj.empty(); |
|
if (setting.view.nameIsHTML) { |
|
nObj.html(data.getNodeName(setting, node)); |
|
} else { |
|
nObj.text(data.getNodeName(setting, node)); |
|
} |
|
if (tools.apply(setting.view.showTitle, [setting.treeId, node], setting.view.showTitle)) { |
|
var aObj = $$(node, consts.id.A, setting); |
|
aObj.attr("title", !title ? "" : title); |
|
} |
|
}, |
|
setNodeTarget: function(setting, node) { |
|
var aObj = $$(node, consts.id.A, setting); |
|
aObj.attr("target", view.makeNodeTarget(node)); |
|
}, |
|
setNodeUrl: function(setting, node) { |
|
var aObj = $$(node, consts.id.A, setting), |
|
url = view.makeNodeUrl(setting, node); |
|
if (url == null || url.length == 0) { |
|
aObj.removeAttr("href"); |
|
} else { |
|
aObj.attr("href", url); |
|
} |
|
}, |
|
switchNode: function(setting, node) { |
|
if (node.open || !tools.canAsync(setting, node)) { |
|
view.expandCollapseNode(setting, node, !node.open); |
|
} else if (setting.async.enable) { |
|
if (!view.asyncNode(setting, node)) { |
|
view.expandCollapseNode(setting, node, !node.open); |
|
return; |
|
} |
|
} else if (node) { |
|
view.expandCollapseNode(setting, node, !node.open); |
|
} |
|
} |
|
}; |
|
// zTree defind |
|
$.fn.zTree = { |
|
consts : _consts, |
|
_z : { |
|
tools: tools, |
|
view: view, |
|
event: event, |
|
data: data |
|
}, |
|
getZTreeObj: function(treeId) { |
|
var o = data.getZTreeTools(treeId); |
|
return o ? o : null; |
|
}, |
|
destroy: function(treeId) { |
|
if (!!treeId && treeId.length > 0) { |
|
view.destroy(data.getSetting(treeId)); |
|
} else { |
|
for(var s in settings) { |
|
view.destroy(settings[s]); |
|
} |
|
} |
|
}, |
|
init: function(obj, zSetting, zNodes) { |
|
var setting = tools.clone(_setting); |
|
$.extend(true, setting, zSetting); |
|
setting.treeId = obj.attr("id"); |
|
setting.treeObj = obj; |
|
setting.treeObj.empty(); |
|
settings[setting.treeId] = setting; |
|
//For some older browser,(e.g., ie6) |
|
if(typeof document.body.style.maxHeight === "undefined") { |
|
setting.view.expandSpeed = ""; |
|
} |
|
data.initRoot(setting); |
|
var root = data.getRoot(setting), |
|
childKey = setting.data.key.children; |
|
zNodes = zNodes ? tools.clone(tools.isArray(zNodes)? zNodes : [zNodes]) : []; |
|
if (setting.data.simpleData.enable) { |
|
root[childKey] = data.transformTozTreeFormat(setting, zNodes); |
|
} else { |
|
root[childKey] = zNodes; |
|
} |
|
|
|
data.initCache(setting); |
|
event.unbindTree(setting); |
|
event.bindTree(setting); |
|
event.unbindEvent(setting); |
|
event.bindEvent(setting); |
|
|
|
var zTreeTools = { |
|
setting : setting, |
|
addNodes : function(parentNode, newNodes, isSilent) { |
|
if (!newNodes) return null; |
|
if (!parentNode) parentNode = null; |
|
if (parentNode && !parentNode.isParent && setting.data.keep.leaf) return null; |
|
var xNewNodes = tools.clone(tools.isArray(newNodes)? newNodes: [newNodes]); |
|
function addCallback() { |
|
view.addNodes(setting, parentNode, xNewNodes, (isSilent==true)); |
|
} |
|
|
|
if (tools.canAsync(setting, parentNode)) { |
|
view.asyncNode(setting, parentNode, isSilent, addCallback); |
|
} else { |
|
addCallback(); |
|
} |
|
return xNewNodes; |
|
}, |
|
cancelSelectedNode : function(node) { |
|
view.cancelPreSelectedNode(setting, node); |
|
}, |
|
destroy : function() { |
|
view.destroy(setting); |
|
}, |
|
expandAll : function(expandFlag) { |
|
expandFlag = !!expandFlag; |
|
view.expandCollapseSonNode(setting, null, expandFlag, true); |
|
return expandFlag; |
|
}, |
|
expandNode : function(node, expandFlag, sonSign, focus, callbackFlag) { |
|
if (!node || !node.isParent) return null; |
|
if (expandFlag !== true && expandFlag !== false) { |
|
expandFlag = !node.open; |
|
} |
|
callbackFlag = !!callbackFlag; |
|
|
|
if (callbackFlag && expandFlag && (tools.apply(setting.callback.beforeExpand, [setting.treeId, node], true) == false)) { |
|
return null; |
|
} else if (callbackFlag && !expandFlag && (tools.apply(setting.callback.beforeCollapse, [setting.treeId, node], true) == false)) { |
|
return null; |
|
} |
|
if (expandFlag && node.parentTId) { |
|
view.expandCollapseParentNode(setting, node.getParentNode(), expandFlag, false); |
|
} |
|
if (expandFlag === node.open && !sonSign) { |
|
return null; |
|
} |
|
|
|
data.getRoot(setting).expandTriggerFlag = callbackFlag; |
|
if (!tools.canAsync(setting, node) && sonSign) { |
|
view.expandCollapseSonNode(setting, node, expandFlag, true, function() { |
|
if (focus !== false) {try{$$(node, setting).focus().blur();}catch(e){}} |
|
}); |
|
} else { |
|
node.open = !expandFlag; |
|
view.switchNode(this.setting, node); |
|
if (focus !== false) {try{$$(node, setting).focus().blur();}catch(e){}} |
|
} |
|
return expandFlag; |
|
}, |
|
getNodes : function() { |
|
return data.getNodes(setting); |
|
}, |
|
getNodeByParam : function(key, value, parentNode) { |
|
if (!key) return null; |
|
return data.getNodeByParam(setting, parentNode?parentNode[setting.data.key.children]:data.getNodes(setting), key, value); |
|
}, |
|
getNodeByTId : function(tId) { |
|
return data.getNodeCache(setting, tId); |
|
}, |
|
getNodesByParam : function(key, value, parentNode) { |
|
if (!key) return null; |
|
return data.getNodesByParam(setting, parentNode?parentNode[setting.data.key.children]:data.getNodes(setting), key, value); |
|
}, |
|
getNodesByParamFuzzy : function(key, value, parentNode) { |
|
if (!key) return null; |
|
return data.getNodesByParamFuzzy(setting, parentNode?parentNode[setting.data.key.children]:data.getNodes(setting), key, value); |
|
}, |
|
getNodesByFilter: function(filter, isSingle, parentNode, invokeParam) { |
|
isSingle = !!isSingle; |
|
if (!filter || (typeof filter != "function")) return (isSingle ? null : []); |
|
return data.getNodesByFilter(setting, parentNode?parentNode[setting.data.key.children]:data.getNodes(setting), filter, isSingle, invokeParam); |
|
}, |
|
getNodeIndex : function(node) { |
|
if (!node) return null; |
|
var childKey = setting.data.key.children, |
|
parentNode = (node.parentTId) ? node.getParentNode() : data.getRoot(setting); |
|
for (var i=0, l = parentNode[childKey].length; i < l; i++) { |
|
if (parentNode[childKey][i] == node) return i; |
|
} |
|
return -1; |
|
}, |
|
getSelectedNodes : function() { |
|
var r = [], list = data.getRoot(setting).curSelectedList; |
|
for (var i=0, l=list.length; i<l; i++) { |
|
r.push(list[i]); |
|
} |
|
return r; |
|
}, |
|
isSelectedNode : function(node) { |
|
return data.isSelectedNode(setting, node); |
|
}, |
|
reAsyncChildNodes : function(parentNode, reloadType, isSilent) { |
|
if (!this.setting.async.enable) return; |
|
var isRoot = !parentNode; |
|
if (isRoot) { |
|
parentNode = data.getRoot(setting); |
|
} |
|
if (reloadType=="refresh") { |
|
var childKey = this.setting.data.key.children; |
|
for (var i = 0, l = parentNode[childKey] ? parentNode[childKey].length : 0; i < l; i++) { |
|
data.removeNodeCache(setting, parentNode[childKey][i]); |
|
} |
|
data.removeSelectedNode(setting); |
|
parentNode[childKey] = []; |
|
if (isRoot) { |
|
this.setting.treeObj.empty(); |
|
} else { |
|
var ulObj = $$(parentNode, consts.id.UL, setting); |
|
ulObj.empty(); |
|
} |
|
} |
|
view.asyncNode(this.setting, isRoot? null:parentNode, !!isSilent); |
|
}, |
|
refresh : function() { |
|
this.setting.treeObj.empty(); |
|
var root = data.getRoot(setting), |
|
nodes = root[setting.data.key.children] |
|
data.initRoot(setting); |
|
root[setting.data.key.children] = nodes |
|
data.initCache(setting); |
|
view.createNodes(setting, 0, root[setting.data.key.children]); |
|
}, |
|
removeChildNodes : function(node) { |
|
if (!node) return null; |
|
var childKey = setting.data.key.children, |
|
nodes = node[childKey]; |
|
view.removeChildNodes(setting, node); |
|
return nodes ? nodes : null; |
|
}, |
|
removeNode : function(node, callbackFlag) { |
|
if (!node) return; |
|
callbackFlag = !!callbackFlag; |
|
if (callbackFlag && tools.apply(setting.callback.beforeRemove, [setting.treeId, node], true) == false) return; |
|
view.removeNode(setting, node); |
|
if (callbackFlag) { |
|
this.setting.treeObj.trigger(consts.event.REMOVE, [setting.treeId, node]); |
|
} |
|
}, |
|
selectNode : function(node, addFlag) { |
|
if (!node) return; |
|
if (tools.uCanDo(setting)) { |
|
addFlag = setting.view.selectedMulti && addFlag; |
|
if (node.parentTId) { |
|
view.expandCollapseParentNode(setting, node.getParentNode(), true, false, function() { |
|
try{$$(node, setting).focus().blur();}catch(e){} |
|
}); |
|
} else { |
|
try{$$(node, setting).focus().blur();}catch(e){} |
|
} |
|
view.selectNode(setting, node, addFlag); |
|
} |
|
}, |
|
transformTozTreeNodes : function(simpleNodes) { |
|
return data.transformTozTreeFormat(setting, simpleNodes); |
|
}, |
|
transformToArray : function(nodes) { |
|
return data.transformToArrayFormat(setting, nodes); |
|
}, |
|
updateNode : function(node, checkTypeFlag) { |
|
if (!node) return; |
|
var nObj = $$(node, setting); |
|
if (nObj.get(0) && tools.uCanDo(setting)) { |
|
view.setNodeName(setting, node); |
|
view.setNodeTarget(setting, node); |
|
view.setNodeUrl(setting, node); |
|
view.setNodeLineIcos(setting, node); |
|
view.setNodeFontCss(setting, node); |
|
} |
|
} |
|
} |
|
root.treeTools = zTreeTools; |
|
data.setZTreeTools(setting, zTreeTools); |
|
|
|
if (root[childKey] && root[childKey].length > 0) { |
|
view.createNodes(setting, 0, root[childKey]); |
|
} else if (setting.async.enable && setting.async.url && setting.async.url !== '') { |
|
view.asyncNode(setting); |
|
} |
|
return zTreeTools; |
|
} |
|
}; |
|
|
|
var zt = $.fn.zTree, |
|
$$ = tools.$, |
|
consts = zt.consts; |
|
})(jQuery);/* |
|
* JQuery zTree excheck v3.5.18 |
|
* http://zTree.me/ |
|
* |
|
* Copyright (c) 2010 Hunter.z |
|
* |
|
* Licensed same as jquery - MIT License |
|
* http://www.opensource.org/licenses/mit-license.php |
|
* |
|
* email: hunter.z@263.net |
|
* Date: 2015-06-18 |
|
*/ |
|
(function($){ |
|
//default consts of excheck |
|
var _consts = { |
|
event: { |
|
CHECK: "ztree_check" |
|
}, |
|
id: { |
|
CHECK: "_check" |
|
}, |
|
checkbox: { |
|
STYLE: "checkbox", |
|
DEFAULT: "chk", |
|
DISABLED: "disable", |
|
FALSE: "false", |
|
TRUE: "true", |
|
FULL: "full", |
|
PART: "part", |
|
FOCUS: "focus" |
|
}, |
|
radio: { |
|
STYLE: "radio", |
|
TYPE_ALL: "all", |
|
TYPE_LEVEL: "level" |
|
} |
|
}, |
|
//default setting of excheck |
|
_setting = { |
|
check: { |
|
enable: false, |
|
autoCheckTrigger: false, |
|
chkStyle: _consts.checkbox.STYLE, |
|
nocheckInherit: false, |
|
chkDisabledInherit: false, |
|
radioType: _consts.radio.TYPE_LEVEL, |
|
chkboxType: { |
|
"Y": "ps", |
|
"N": "ps" |
|
} |
|
}, |
|
data: { |
|
key: { |
|
checked: "checked" |
|
} |
|
}, |
|
callback: { |
|
beforeCheck:null, |
|
onCheck:null |
|
} |
|
}, |
|
//default root of excheck |
|
_initRoot = function (setting) { |
|
var r = data.getRoot(setting); |
|
r.radioCheckedList = []; |
|
}, |
|
//default cache of excheck |
|
_initCache = function(treeId) {}, |
|
//default bind event of excheck |
|
_bindEvent = function(setting) { |
|
var o = setting.treeObj, |
|
c = consts.event; |
|
o.bind(c.CHECK, function (event, srcEvent, treeId, node) { |
|
event.srcEvent = srcEvent; |
|
tools.apply(setting.callback.onCheck, [event, treeId, node]); |
|
}); |
|
}, |
|
_unbindEvent = function(setting) { |
|
var o = setting.treeObj, |
|
c = consts.event; |
|
o.unbind(c.CHECK); |
|
}, |
|
//default event proxy of excheck |
|
_eventProxy = function(e) { |
|
var target = e.target, |
|
setting = data.getSetting(e.data.treeId), |
|
tId = "", node = null, |
|
nodeEventType = "", treeEventType = "", |
|
nodeEventCallback = null, treeEventCallback = null; |
|
|
|
if (tools.eqs(e.type, "mouseover")) { |
|
if (setting.check.enable && tools.eqs(target.tagName, "span") && target.getAttribute("treeNode"+ consts.id.CHECK) !== null) { |
|
tId = tools.getNodeMainDom(target).id; |
|
nodeEventType = "mouseoverCheck"; |
|
} |
|
} else if (tools.eqs(e.type, "mouseout")) { |
|
if (setting.check.enable && tools.eqs(target.tagName, "span") && target.getAttribute("treeNode"+ consts.id.CHECK) !== null) { |
|
tId = tools.getNodeMainDom(target).id; |
|
nodeEventType = "mouseoutCheck"; |
|
} |
|
} else if (tools.eqs(e.type, "click")) { |
|
if (setting.check.enable && tools.eqs(target.tagName, "span") && target.getAttribute("treeNode"+ consts.id.CHECK) !== null) { |
|
tId = tools.getNodeMainDom(target).id; |
|
nodeEventType = "checkNode"; |
|
} |
|
} |
|
if (tId.length>0) { |
|
node = data.getNodeCache(setting, tId); |
|
switch (nodeEventType) { |
|
case "checkNode" : |
|
nodeEventCallback = _handler.onCheckNode; |
|
break; |
|
case "mouseoverCheck" : |
|
nodeEventCallback = _handler.onMouseoverCheck; |
|
break; |
|
case "mouseoutCheck" : |
|
nodeEventCallback = _handler.onMouseoutCheck; |
|
break; |
|
} |
|
} |
|
var proxyResult = { |
|
stop: nodeEventType === "checkNode", |
|
node: node, |
|
nodeEventType: nodeEventType, |
|
nodeEventCallback: nodeEventCallback, |
|
treeEventType: treeEventType, |
|
treeEventCallback: treeEventCallback |
|
}; |
|
return proxyResult |
|
}, |
|
//default init node of excheck |
|
_initNode = function(setting, level, n, parentNode, isFirstNode, isLastNode, openFlag) { |
|
if (!n) return; |
|
var checkedKey = setting.data.key.checked; |
|
if (typeof n[checkedKey] == "string") n[checkedKey] = tools.eqs(n[checkedKey], "true"); |
|
n[checkedKey] = !!n[checkedKey]; |
|
n.checkedOld = n[checkedKey]; |
|
if (typeof n.nocheck == "string") n.nocheck = tools.eqs(n.nocheck, "true"); |
|
n.nocheck = !!n.nocheck || (setting.check.nocheckInherit && parentNode && !!parentNode.nocheck); |
|
if (typeof n.chkDisabled == "string") n.chkDisabled = tools.eqs(n.chkDisabled, "true"); |
|
n.chkDisabled = !!n.chkDisabled || (setting.check.chkDisabledInherit && parentNode && !!parentNode.chkDisabled); |
|
if (typeof n.halfCheck == "string") n.halfCheck = tools.eqs(n.halfCheck, "true"); |
|
n.halfCheck = !!n.halfCheck; |
|
n.check_Child_State = -1; |
|
n.check_Focus = false; |
|
n.getCheckStatus = function() {return data.getCheckStatus(setting, n);}; |
|
|
|
if (setting.check.chkStyle == consts.radio.STYLE && setting.check.radioType == consts.radio.TYPE_ALL && n[checkedKey] ) { |
|
var r = data.getRoot(setting); |
|
r.radioCheckedList.push(n); |
|
} |
|
}, |
|
//add dom for check |
|
_beforeA = function(setting, node, html) { |
|
var checkedKey = setting.data.key.checked; |
|
if (setting.check.enable) { |
|
data.makeChkFlag(setting, node); |
|
html.push("<span ID='", node.tId, consts.id.CHECK, "' class='", view.makeChkClass(setting, node), "' treeNode", consts.id.CHECK, (node.nocheck === true?" style='display:none;'":""),"></span>"); |
|
} |
|
}, |
|
//update zTreeObj, add method of check |
|
_zTreeTools = function(setting, zTreeTools) { |
|
zTreeTools.checkNode = function(node, checked, checkTypeFlag, callbackFlag) { |
|
var checkedKey = this.setting.data.key.checked; |
|
if (node.chkDisabled === true) return; |
|
if (checked !== true && checked !== false) { |
|
checked = !node[checkedKey]; |
|
} |
|
callbackFlag = !!callbackFlag; |
|
|
|
if (node[checkedKey] === checked && !checkTypeFlag) { |
|
return; |
|
} else if (callbackFlag && tools.apply(this.setting.callback.beforeCheck, [this.setting.treeId, node], true) == false) { |
|
return; |
|
} |
|
if (tools.uCanDo(this.setting) && this.setting.check.enable && node.nocheck !== true) { |
|
node[checkedKey] = checked; |
|
var checkObj = $$(node, consts.id.CHECK, this.setting); |
|
if (checkTypeFlag || this.setting.check.chkStyle === consts.radio.STYLE) view.checkNodeRelation(this.setting, node); |
|
view.setChkClass(this.setting, checkObj, node); |
|
view.repairParentChkClassWithSelf(this.setting, node); |
|
if (callbackFlag) { |
|
this.setting.treeObj.trigger(consts.event.CHECK, [null, this.setting.treeId, node]); |
|
} |
|
} |
|
} |
|
|
|
zTreeTools.checkAllNodes = function(checked) { |
|
view.repairAllChk(this.setting, !!checked); |
|
} |
|
|
|
zTreeTools.getCheckedNodes = function(checked) { |
|
var childKey = this.setting.data.key.children; |
|
checked = (checked !== false); |
|
return data.getTreeCheckedNodes(this.setting, data.getRoot(this.setting)[childKey], checked); |
|
} |
|
|
|
zTreeTools.getChangeCheckedNodes = function() { |
|
var childKey = this.setting.data.key.children; |
|
return data.getTreeChangeCheckedNodes(this.setting, data.getRoot(this.setting)[childKey]); |
|
} |
|
|
|
zTreeTools.setChkDisabled = function(node, disabled, inheritParent, inheritChildren) { |
|
disabled = !!disabled; |
|
inheritParent = !!inheritParent; |
|
inheritChildren = !!inheritChildren; |
|
view.repairSonChkDisabled(this.setting, node, disabled, inheritChildren); |
|
view.repairParentChkDisabled(this.setting, node.getParentNode(), disabled, inheritParent); |
|
} |
|
|
|
var _updateNode = zTreeTools.updateNode; |
|
zTreeTools.updateNode = function(node, checkTypeFlag) { |
|
if (_updateNode) _updateNode.apply(zTreeTools, arguments); |
|
if (!node || !this.setting.check.enable) return; |
|
var nObj = $$(node, this.setting); |
|
if (nObj.get(0) && tools.uCanDo(this.setting)) { |
|
var checkObj = $$(node, consts.id.CHECK, this.setting); |
|
if (checkTypeFlag == true || this.setting.check.chkStyle === consts.radio.STYLE) view.checkNodeRelation(this.setting, node); |
|
view.setChkClass(this.setting, checkObj, node); |
|
view.repairParentChkClassWithSelf(this.setting, node); |
|
} |
|
} |
|
}, |
|
//method of operate data |
|
_data = { |
|
getRadioCheckedList: function(setting) { |
|
var checkedList = data.getRoot(setting).radioCheckedList; |
|
for (var i=0, j=checkedList.length; i<j; i++) { |
|
if(!data.getNodeCache(setting, checkedList[i].tId)) { |
|
checkedList.splice(i, 1); |
|
i--; j--; |
|
} |
|
} |
|
return checkedList; |
|
}, |
|
getCheckStatus: function(setting, node) { |
|
if (!setting.check.enable || node.nocheck || node.chkDisabled) return null; |
|
var checkedKey = setting.data.key.checked, |
|
r = { |
|
checked: node[checkedKey], |
|
half: node.halfCheck ? node.halfCheck : (setting.check.chkStyle == consts.radio.STYLE ? (node.check_Child_State === 2) : (node[checkedKey] ? (node.check_Child_State > -1 && node.check_Child_State < 2) : (node.check_Child_State > 0))) |
|
}; |
|
return r; |
|
}, |
|
getTreeCheckedNodes: function(setting, nodes, checked, results) { |
|
if (!nodes) return []; |
|
var childKey = setting.data.key.children, |
|
checkedKey = setting.data.key.checked, |
|
onlyOne = (checked && setting.check.chkStyle == consts.radio.STYLE && setting.check.radioType == consts.radio.TYPE_ALL); |
|
results = !results ? [] : results; |
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
if (nodes[i].nocheck !== true && nodes[i].chkDisabled !== true && nodes[i][checkedKey] == checked) { |
|
results.push(nodes[i]); |
|
if(onlyOne) { |
|
break; |
|
} |
|
} |
|
data.getTreeCheckedNodes(setting, nodes[i][childKey], checked, results); |
|
if(onlyOne && results.length > 0) { |
|
break; |
|
} |
|
} |
|
return results; |
|
}, |
|
getTreeChangeCheckedNodes: function(setting, nodes, results) { |
|
if (!nodes) return []; |
|
var childKey = setting.data.key.children, |
|
checkedKey = setting.data.key.checked; |
|
results = !results ? [] : results; |
|
for (var i = 0, l = nodes.length; i < l; i++) { |
|
if (nodes[i].nocheck !== true && nodes[i].chkDisabled !== true && nodes[i][checkedKey] != nodes[i].checkedOld) { |
|
results.push(nodes[i]); |
|
} |
|
data.getTreeChangeCheckedNodes(setting, nodes[i][childKey], results); |
|
} |
|
return results; |
|
}, |
|
makeChkFlag: function(setting, node) { |
|
if (!node) return; |
|
var childKey = setting.data.key.children, |
|
checkedKey = setting.data.key.checked, |
|
chkFlag = -1; |
|
if (node[childKey]) { |
|
for (var i = 0, l = node[childKey].length; i < l; i++) { |
|
var cNode = node[childKey][i]; |
|
var tmp = -1; |
|
if (setting.check.chkStyle == consts.radio.STYLE) { |
|
if (cNode.nocheck === true || cNode.chkDisabled === true) { |
|
tmp = cNode.check_Child_State; |
|
} else if (cNode.halfCheck === true) { |
|
tmp = 2; |
|
} else if (cNode[checkedKey]) { |
|
tmp = 2; |
|
} else { |
|
tmp = cNode.check_Child_State > 0 ? 2:0; |
|
} |
|
if (tmp == 2) { |
|
chkFlag = 2; break; |
|
} else if (tmp == 0){ |
|
chkFlag = 0; |
|
} |
|
} else if (setting.check.chkStyle == consts.checkbox.STYLE) { |
|
if (cNode.nocheck === true || cNode.chkDisabled === true) { |
|
tmp = cNode.check_Child_State; |
|
} else if (cNode.halfCheck === true) { |
|
tmp = 1; |
|
} else if (cNode[checkedKey] ) { |
|
tmp = (cNode.check_Child_State === -1 || cNode.check_Child_State === 2) ? 2 : 1; |
|
} else { |
|
tmp = (cNode.check_Child_State > 0) ? 1 : 0; |
|
} |
|
if (tmp === 1) { |
|
chkFlag = 1; break; |
|
} else if (tmp === 2 && chkFlag > -1 && i > 0 && tmp !== chkFlag) { |
|
chkFlag = 1; break; |
|
} else if (chkFlag === 2 && tmp > -1 && tmp < 2) { |
|
chkFlag = 1; break; |
|
} else if (tmp > -1) { |
|
chkFlag = tmp; |
|
} |
|
} |
|
} |
|
} |
|
node.check_Child_State = chkFlag; |
|
} |
|
}, |
|
//method of event proxy |
|
_event = { |
|
|
|
}, |
|
//method of event handler |
|
_handler = { |
|
onCheckNode: function (event, node) { |
|
if (node.chkDisabled === true) return false; |
|
var setting = data.getSetting(event.data.treeId), |
|
checkedKey = setting.data.key.checked; |
|
if (tools.apply(setting.callback.beforeCheck, [setting.treeId, node], true) == false) return true; |
|
node[checkedKey] = !node[checkedKey]; |
|
view.checkNodeRelation(setting, node); |
|
var checkObj = $$(node, consts.id.CHECK, setting); |
|
view.setChkClass(setting, checkObj, node); |
|
view.repairParentChkClassWithSelf(setting, node); |
|
setting.treeObj.trigger(consts.event.CHECK, [event, setting.treeId, node]); |
|
return true; |
|
}, |
|
onMouseoverCheck: function(event, node) { |
|
if (node.chkDisabled === true) return false; |
|
var setting = data.getSetting(event.data.treeId), |
|
checkObj = $$(node, consts.id.CHECK, setting); |
|
node.check_Focus = true; |
|
view.setChkClass(setting, checkObj, node); |
|
return true; |
|
}, |
|
onMouseoutCheck: function(event, node) { |
|
if (node.chkDisabled === true) return false; |
|
var setting = data.getSetting(event.data.treeId), |
|
checkObj = $$(node, consts.id.CHECK, setting); |
|
node.check_Focus = false; |
|
view.setChkClass(setting, checkObj, node); |
|
return true; |
|
} |
|
}, |
|
//method of tools for zTree |
|
_tools = { |
|
|
|
}, |
|
//method of operate ztree dom |
|
_view = { |
|
checkNodeRelation: function(setting, node) { |
|
var pNode, i, l, |
|
childKey = setting.data.key.children, |
|
checkedKey = setting.data.key.checked, |
|
r = consts.radio; |
|
if (setting.check.chkStyle == r.STYLE) { |
|
var checkedList = data.getRadioCheckedList(setting); |
|
if (node[checkedKey]) { |
|
if (setting.check.radioType == r.TYPE_ALL) { |
|
for (i = checkedList.length-1; i >= 0; i--) { |
|
pNode = checkedList[i]; |
|
if (pNode[checkedKey] && pNode != node) { |
|
pNode[checkedKey] = false; |
|
checkedList.splice(i, 1); |
|
|
|
view.setChkClass(setting, $$(pNode, consts.id.CHECK, setting), pNode); |
|
if (pNode.parentTId != node.parentTId) { |
|
view.repairParentChkClassWithSelf(setting, pNode); |
|
} |
|
} |
|
} |
|
checkedList.push(node); |
|
} else { |
|
var parentNode = (node.parentTId) ? node.getParentNode() : data.getRoot(setting); |
|
for (i = 0, l = parentNode[childKey].length; i < l; i++) { |
|
pNode = parentNode[childKey][i]; |
|
if (pNode[checkedKey] && pNode != node) { |
|
pNode[checkedKey] = false; |
|
view.setChkClass(setting, $$(pNode, consts.id.CHECK, setting), pNode); |
|
} |
|
} |
|
} |
|
} else if (setting.check.radioType == r.TYPE_ALL) { |
|
for (i = 0, l = checkedList.length; i < l; i++) { |
|
if (node == checkedList[i]) { |
|
checkedList.splice(i, 1); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
} else { |
|
if (node[checkedKey] && (!node[childKey] || node[childKey].length==0 || setting.check.chkboxType.Y.indexOf("s") > -1)) { |
|
view.setSonNodeCheckBox(setting, node, true); |
|
} |
|
if (!node[checkedKey] && (!node[childKey] || node[childKey].length==0 || setting.check.chkboxType.N.indexOf("s") > -1)) { |
|
view.setSonNodeCheckBox(setting, node, false); |
|
} |
|
if (node[checkedKey] && setting.check.chkboxType.Y.indexOf("p") > -1) { |
|
view.setParentNodeCheckBox(setting, node, true); |
|
} |
|
if (!node[checkedKey] && setting.check.chkboxType.N.indexOf("p") > -1) { |
|
view.setParentNodeCheckBox(setting, node, false); |
|
} |
|
} |
|
}, |
|
makeChkClass: function(setting, node) { |
|
var checkedKey = setting.data.key.checked, |
|
c = consts.checkbox, r = consts.radio, |
|
fullStyle = ""; |
|
if (node.chkDisabled === true) { |
|
fullStyle = c.DISABLED; |
|
} else if (node.halfCheck) { |
|
fullStyle = c.PART; |
|
} else if (setting.check.chkStyle == r.STYLE) { |
|
fullStyle = (node.check_Child_State < 1)? c.FULL:c.PART; |
|
} else { |
|
fullStyle = node[checkedKey] ? ((node.check_Child_State === 2 || node.check_Child_State === -1) ? c.FULL:c.PART) : ((node.check_Child_State < 1)? c.FULL:c.PART); |
|
} |
|
var chkName = setting.check.chkStyle + "_" + (node[checkedKey] ? c.TRUE : c.FALSE) + "_" + fullStyle; |
|
chkName = (node.check_Focus && node.chkDisabled !== true) ? chkName + "_" + c.FOCUS : chkName; |
|
return consts.className.BUTTON + " " + c.DEFAULT + " " + chkName; |
|
}, |
|
repairAllChk: function(setting, checked) { |
|
if (setting.check.enable && setting.check.chkStyle === consts.checkbox.STYLE) { |
|
var checkedKey = setting.data.key.checked, |
|
childKey = setting.data.key.children, |
|
root = data.getRoot(setting); |
|
for (var i = 0, l = root[childKey].length; i<l ; i++) { |
|
var node = root[childKey][i]; |
|
if (node.nocheck !== true && node.chkDisabled !== true) { |
|
node[checkedKey] = checked; |
|
} |
|
view.setSonNodeCheckBox(setting, node, checked); |
|
} |
|
} |
|
}, |
|
repairChkClass: function(setting, node) { |
|
if (!node) return; |
|
data.makeChkFlag(setting, node); |
|
if (node.nocheck !== true) { |
|
var checkObj = $$(node, consts.id.CHECK, setting); |
|
view.setChkClass(setting, checkObj, node); |
|
} |
|
}, |
|
repairParentChkClass: function(setting, node) { |
|
if (!node || !node.parentTId) return; |
|
var pNode = node.getParentNode(); |
|
view.repairChkClass(setting, pNode); |
|
view.repairParentChkClass(setting, pNode); |
|
}, |
|
repairParentChkClassWithSelf: function(setting, node) { |
|
if (!node) return; |
|
var childKey = setting.data.key.children; |
|
if (node[childKey] && node[childKey].length > 0) { |
|
view.repairParentChkClass(setting, node[childKey][0]); |
|
} else { |
|
view.repairParentChkClass(setting, node); |
|
} |
|
}, |
|
repairSonChkDisabled: function(setting, node, chkDisabled, inherit) { |
|
if (!node) return; |
|
var childKey = setting.data.key.children; |
|
if (node.chkDisabled != chkDisabled) { |
|
node.chkDisabled = chkDisabled; |
|
} |
|
view.repairChkClass(setting, node); |
|
if (node[childKey] && inherit) { |
|
for (var i = 0, l = node[childKey].length; i < l; i++) { |
|
var sNode = node[childKey][i]; |
|
view.repairSonChkDisabled(setting, sNode, chkDisabled, inherit); |
|
} |
|
} |
|
}, |
|
repairParentChkDisabled: function(setting, node, chkDisabled, inherit) { |
|
if (!node) return; |
|
if (node.chkDisabled != chkDisabled && inherit) { |
|
node.chkDisabled = chkDisabled; |
|
} |
|
view.repairChkClass(setting, node); |
|
view.repairParentChkDisabled(setting, node.getParentNode(), chkDisabled, inherit); |
|
}, |
|
setChkClass: function(setting, obj, node) { |
|
if (!obj) return; |
|
if (node.nocheck === true) { |
|
obj.hide(); |
|
} else { |
|
obj.show(); |
|
} |
|
obj.attr('class', view.makeChkClass(setting, node)); |
|
}, |
|
setParentNodeCheckBox: function(setting, node, value, srcNode) { |
|
var childKey = setting.data.key.children, |
|
checkedKey = setting.data.key.checked, |
|
checkObj = $$(node, consts.id.CHECK, setting); |
|
if (!srcNode) srcNode = node; |
|
data.makeChkFlag(setting, node); |
|
if (node.nocheck !== true && node.chkDisabled !== true) { |
|
node[checkedKey] = value; |
|
view.setChkClass(setting, checkObj, node); |
|
if (setting.check.autoCheckTrigger && node != srcNode) { |
|
setting.treeObj.trigger(consts.event.CHECK, [null, setting.treeId, node]); |
|
} |
|
} |
|
if (node.parentTId) { |
|
var pSign = true; |
|
if (!value) { |
|
var pNodes = node.getParentNode()[childKey]; |
|
for (var i = 0, l = pNodes.length; i < l; i++) { |
|
if ((pNodes[i].nocheck !== true && pNodes[i].chkDisabled !== true && pNodes[i][checkedKey]) |
|
|| ((pNodes[i].nocheck === true || pNodes[i].chkDisabled === true) && pNodes[i].check_Child_State > 0)) { |
|
pSign = false; |
|
break; |
|
} |
|
} |
|
} |
|
if (pSign) { |
|
view.setParentNodeCheckBox(setting, node.getParentNode(), value, srcNode); |
|
} |
|
} |
|
}, |
|
setSonNodeCheckBox: function(setting, node, value, srcNode) { |
|
if (!node) return; |
|
var childKey = setting.data.key.children, |
|
checkedKey = setting.data.key.checked, |
|
checkObj = $$(node, consts.id.CHECK, setting); |
|
if (!srcNode) srcNode = node; |
|
|
|
var hasDisable = false; |
|
if (node[childKey]) { |
|
for (var i = 0, l = node[childKey].length; i < l && node.chkDisabled !== true; i++) { |
|
var sNode = node[childKey][i]; |
|
view.setSonNodeCheckBox(setting, sNode, value, srcNode); |
|
if (sNode.chkDisabled === true) hasDisable = true; |
|
} |
|
} |
|
|
|
if (node != data.getRoot(setting) && node.chkDisabled !== true) { |
|
if (hasDisable && node.nocheck !== true) { |
|
data.makeChkFlag(setting, node); |
|
} |
|
if (node.nocheck !== true && node.chkDisabled !== true) { |
|
node[checkedKey] = value; |
|
if (!hasDisable) node.check_Child_State = (node[childKey] && node[childKey].length > 0) ? (value ? 2 : 0) : -1; |
|
} else { |
|
node.check_Child_State = -1; |
|
} |
|
view.setChkClass(setting, checkObj, node); |
|
if (setting.check.autoCheckTrigger && node != srcNode && node.nocheck !== true && node.chkDisabled !== true) { |
|
setting.treeObj.trigger(consts.event.CHECK, [null, setting.treeId, node]); |
|
} |
|
} |
|
|
|
} |
|
}, |
|
|
|
_z = { |
|
tools: _tools, |
|
view: _view, |
|
event: _event, |
|
data: _data |
|
}; |
|
$.extend(true, $.fn.zTree.consts, _consts); |
|
$.extend(true, $.fn.zTree._z, _z); |
|
|
|
var zt = $.fn.zTree, |
|
tools = zt._z.tools, |
|
consts = zt.consts, |
|
view = zt._z.view, |
|
data = zt._z.data, |
|
event = zt._z.event, |
|
$$ = tools.$; |
|
|
|
data.exSetting(_setting); |
|
data.addInitBind(_bindEvent); |
|
data.addInitUnBind(_unbindEvent); |
|
data.addInitCache(_initCache); |
|
data.addInitNode(_initNode); |
|
data.addInitProxy(_eventProxy, true); |
|
data.addInitRoot(_initRoot); |
|
data.addBeforeA(_beforeA); |
|
data.addZTreeTools(_zTreeTools); |
|
|
|
var _createNodes = view.createNodes; |
|
view.createNodes = function(setting, level, nodes, parentNode) { |
|
if (_createNodes) _createNodes.apply(view, arguments); |
|
if (!nodes) return; |
|
view.repairParentChkClassWithSelf(setting, parentNode); |
|
} |
|
var _removeNode = view.removeNode; |
|
view.removeNode = function(setting, node) { |
|
var parentNode = node.getParentNode(); |
|
if (_removeNode) _removeNode.apply(view, arguments); |
|
if (!node || !parentNode) return; |
|
view.repairChkClass(setting, parentNode); |
|
view.repairParentChkClass(setting, parentNode); |
|
} |
|
|
|
var _appendNodes = view.appendNodes; |
|
view.appendNodes = function(setting, level, nodes, parentNode, initFlag, openFlag) { |
|
var html = ""; |
|
if (_appendNodes) { |
|
html = _appendNodes.apply(view, arguments); |
|
} |
|
if (parentNode) { |
|
data.makeChkFlag(setting, parentNode); |
|
} |
|
return html; |
|
} |
|
})(jQuery);/** |
|
* 当没有元素时有提示信息的view |
|
* |
|
* Created by GUY on 2015/9/8. |
|
* @class BI.Pane |
|
* @extends BI.Widget |
|
* @abstract |
|
*/ |
|
BI.Pane = BI.inherit(BI.Widget, { |
|
|
|
_defaultConfig: function () { |
|
return BI.extend(BI.Pane.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-pane", |
|
tipText: BI.i18nText("BI-No_Selected_Item"), |
|
overlap: true, |
|
onLoaded: BI.emptyFn |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Pane.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
_assertTip: function () { |
|
var o = this.options; |
|
if (!this._tipText) { |
|
this._tipText = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "bi-tips", |
|
text: o.tipText, |
|
height: 25 |
|
}); |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
element: this, |
|
items: [this._tipText], |
|
bgap: 25 |
|
}); |
|
} |
|
}, |
|
|
|
loading: function () { |
|
var self = this, o = this.options; |
|
if (o.overlap === true) { |
|
if (!BI.Maskers.has(this.getName())) { |
|
BI.createWidget({ |
|
type: 'bi.vtape', |
|
items: [{ |
|
el: { |
|
type: "bi.layout", |
|
cls: "loading-background" |
|
}, |
|
height: 30 |
|
}], |
|
element: BI.Maskers.make(this.getName(), this) |
|
}); |
|
} |
|
BI.Maskers.show(self.getName()); |
|
} else if (BI.isNull(this._loading)) { |
|
this._loading = BI.createWidget({ |
|
type: "bi.layout", |
|
cls: "loading-background", |
|
height: 30 |
|
}); |
|
this._loading.element.css("zIndex", 1); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this._loading, |
|
left: 0, |
|
right: 0, |
|
top: 0 |
|
}] |
|
}) |
|
} |
|
}, |
|
|
|
loaded: function () { |
|
var self = this, o = this.options; |
|
BI.Maskers.remove(self.getName()); |
|
this._loading && this._loading.destroy(); |
|
this._loading && (this._loading = null); |
|
o.onLoaded(); |
|
self.fireEvent(BI.Pane.EVENT_LOADED); |
|
}, |
|
|
|
check: function () { |
|
this.setTipVisible(BI.isEmpty(this.options.items)); |
|
}, |
|
|
|
setTipVisible: function (b) { |
|
if (b === true) { |
|
this._assertTip(); |
|
this._tipText.setVisible(true); |
|
} else { |
|
this._tipText && this._tipText.setVisible(false); |
|
} |
|
}, |
|
|
|
populate: function (items) { |
|
this.options.items = items || []; |
|
this.check(); |
|
}, |
|
|
|
empty: function () { |
|
|
|
} |
|
}); |
|
BI.Pane.EVENT_LOADED = "EVENT_LOADED";/** |
|
* guy |
|
* 这仅仅只是一个超类, 所有简单控件的基类 |
|
* 1、类的控制, |
|
* 2、title的控制 |
|
* 3、文字超过边界显示3个点 |
|
* 4、cursor默认pointor |
|
* @class BI.Single |
|
* @extends BI.Widget |
|
* @abstract |
|
*/ |
|
BI.Single = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
var conf = BI.Single.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-single", |
|
readonly: false, |
|
title: null, |
|
warningTitle: null, |
|
tipType: null, // success或warning |
|
value: null |
|
}) |
|
}, |
|
|
|
_showToolTip: function (e, opt) { |
|
opt || (opt = {}); |
|
var self = this; |
|
var type = this.getTipType() || (this.isEnabled() ? "success" : "warning"); |
|
var title = type === "success" ? this.getTitle() : (this.getWarningTitle() || this.getTitle()); |
|
if (BI.isKey(title)) { |
|
BI.Tooltips.show(e, this.getName(), title, type, this, opt); |
|
} |
|
}, |
|
|
|
_hideTooltip: function () { |
|
var self = this; |
|
var tooltip = BI.Tooltips.get(this.getName()); |
|
if (BI.isNotNull(tooltip)) { |
|
tooltip.element.fadeOut(200, function () { |
|
BI.Tooltips.remove(self.getName()); |
|
}); |
|
} |
|
}, |
|
|
|
_init: function () { |
|
BI.Single.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
if (BI.isKey(o.title) || BI.isKey(o.warningTitle) |
|
|| BI.isFunction(o.title) || BI.isFunction(o.warningTitle)) { |
|
this.enableHover(); |
|
} |
|
}, |
|
|
|
enableHover: function (opt) { |
|
opt || (opt = {}); |
|
var self = this; |
|
if (!this._hoverBinded) { |
|
this.element.on("mouseenter.title" + this.getName(), function (e) { |
|
self._e = e; |
|
if (self.getTipType() === "warning" || (BI.isKey(self.getWarningTitle()) && !self.isEnabled())) { |
|
self.timeout = BI.delay(function () { |
|
self._showToolTip(self._e || e, opt); |
|
}, 200); |
|
} else if (self.getTipType() === "success" || self.isEnabled()) { |
|
self.timeout = BI.delay(function () { |
|
self._showToolTip(self._e || e, opt); |
|
}, 500); |
|
} |
|
}); |
|
this.element.on("mousemove.title" + this.getName(), function (e) { |
|
self._e = e; |
|
if (!self.element.__isMouseInBounds__(e)) { |
|
if (BI.isNotNull(self.timeout)) { |
|
clearTimeout(self.timeout); |
|
} |
|
self._hideTooltip(); |
|
} |
|
}); |
|
this.element.on("mouseleave.title" + this.getName(), function () { |
|
self._e = null; |
|
if (BI.isNotNull(self.timeout)) { |
|
clearTimeout(self.timeout); |
|
} |
|
self._hideTooltip(); |
|
}); |
|
this._hoverBinded = true; |
|
} |
|
}, |
|
|
|
disabledHover: function () { |
|
//取消hover事件 |
|
if (BI.isNotNull(this.timeout)) { |
|
clearTimeout(this.timeout); |
|
} |
|
this._hideTooltip(); |
|
$(this.element).unbind("mouseenter.title" + this.getName()) |
|
.unbind("mousemove.title" + this.getName()) |
|
.unbind("mouseleave.title" + this.getName()); |
|
this._hoverBinded = false; |
|
}, |
|
|
|
populate: function (items) { |
|
this.items = items || []; |
|
}, |
|
|
|
//opt: {container: '', belowMouse: false} |
|
setTitle: function (title, opt) { |
|
this.options.title = title; |
|
if (BI.isKey(title)) { |
|
this.enableHover(opt); |
|
} else { |
|
this.disabledHover(); |
|
} |
|
}, |
|
|
|
setWarningTitle: function (title, opt) { |
|
this.options.warningTitle = title; |
|
if (BI.isKey(title)) { |
|
this.enableHover(opt); |
|
} else { |
|
this.disabledHover(); |
|
} |
|
}, |
|
|
|
getTipType: function () { |
|
return this.options.tipType; |
|
}, |
|
|
|
isReadOnly: function () { |
|
return !!this.options.readonly; |
|
}, |
|
|
|
getTitle: function () { |
|
var title = this.options.title; |
|
if(BI.isFunction(title)) { |
|
return title(); |
|
} |
|
return title; |
|
}, |
|
|
|
getWarningTitle: function () { |
|
var title = this.options.warningTitle; |
|
if(BI.isFunction(title)) { |
|
return title(); |
|
} |
|
return title; |
|
}, |
|
|
|
setValue: function (val) { |
|
if (!this.options.readonly) { |
|
this.options.value = val; |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.options.value; |
|
} |
|
});/** |
|
* guy 表示一行数据,通过position来定位位置的数据 |
|
* @class BI.Text |
|
* @extends BI.Single |
|
*/ |
|
BI.Text = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.Text.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-text", |
|
textAlign: "left", |
|
whiteSpace: "normal", |
|
lineHeight: null, |
|
hgap: 0, |
|
vgap: 0, |
|
lgap: 0, |
|
rgap: 0, |
|
tgap: 0, |
|
bgap: 0, |
|
text: "", |
|
py: "" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Text.superclass._init.apply(this, arguments); |
|
var o = this.options; |
|
this.text = BI.createWidget({ |
|
type: "bi.layout", |
|
cls: "bi-text" |
|
}); |
|
this.text.element.appendTo(this.element); |
|
if (BI.isKey(o.text)) { |
|
this.text.element.text(o.text); |
|
} else if (BI.isKey(o.value)) { |
|
this.text.element.text(o.value); |
|
} |
|
if (o.hgap + o.lgap > 0) { |
|
this.text.element.css({ |
|
"margin-left": o.hgap + o.lgap + "px" |
|
}) |
|
} |
|
if (o.hgap + o.rgap > 0) { |
|
this.text.element.css({ |
|
"margin-right": o.hgap + o.rgap + "px" |
|
}) |
|
} |
|
if (o.vgap + o.tgap > 0) { |
|
this.text.element.css({ |
|
"margin-top": o.vgap + o.tgap + "px" |
|
}) |
|
} |
|
if (o.vgap + o.bgap > 0) { |
|
this.text.element.css({ |
|
"margin-bottom": o.vgap + o.bgap + "px" |
|
}) |
|
} |
|
if (BI.isNumber(o.height)) { |
|
this.element.css({"lineHeight": o.height + "px"}); |
|
} |
|
if (BI.isNumber(o.lineHeight)) { |
|
this.element.css({"lineHeight": o.lineHeight + "px"}); |
|
} |
|
this.text.element.css({ |
|
"textAlign": o.textAlign, |
|
"whiteSpace": o.whiteSpace |
|
}); |
|
this.element.css({ |
|
"textAlign": o.textAlign, |
|
"whiteSpace": o.whiteSpace |
|
}); |
|
if (BI.isKey(o.keyword)) { |
|
this.text.element.__textKeywordMarked__(o.text, o.keyword, o.py); |
|
} |
|
}, |
|
|
|
doRedMark: function (keyword) { |
|
var o = this.options; |
|
this.text.element.__textKeywordMarked__(o.text || o.value, keyword, o.py); |
|
}, |
|
|
|
unRedMark: function () { |
|
var o = this.options; |
|
this.text.element.__textKeywordMarked__(o.text || o.value, "", o.py); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.element.addClass("bi-high-light"); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.element.removeClass("bi-high-light"); |
|
}, |
|
|
|
setValue: function (text) { |
|
BI.Text.superclass.setValue.apply(this, arguments); |
|
if (!this.isReadOnly()) { |
|
this.text.element.text(text); |
|
} |
|
}, |
|
|
|
setStyle: function (css) { |
|
this.text.element.css(css) |
|
}, |
|
|
|
setText: function (text) { |
|
BI.Text.superclass.setText.apply(this, arguments); |
|
this.options.text = text; |
|
this.text.element.text(text); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.text", BI.Text);/** |
|
* guy |
|
* @class BI.BasicButton |
|
* @extends BI.Single |
|
* |
|
* 一般的button父级 |
|
*/ |
|
BI.BasicButton = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.BasicButton.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-basic-button" + (conf.invalid ? "" : " cursor-pointer"), |
|
value: "", |
|
text: "", |
|
stopEvent: false, |
|
stopPropagation: false, |
|
selected: false, |
|
once: false, //点击一次选中有效,再点无效 |
|
forceSelected: false, //点击即选中, 选中了就不会被取消 |
|
forceNotSelected: false, //无论怎么点击都不会被选中 |
|
disableSelected: false, //使能选中 |
|
|
|
shadow: false, |
|
isShadowShowingOnSelected: false, //选中状态下是否显示阴影 |
|
trigger: null, |
|
handler: BI.emptyFn |
|
}) |
|
}, |
|
_init: function () { |
|
BI.BasicButton.superclass._init.apply(this, arguments); |
|
var opts = this.options; |
|
if (opts.selected === true) { |
|
BI.nextTick(BI.bind(function () { |
|
this.setSelected(opts.selected); |
|
}, this)); |
|
} |
|
BI.nextTick(BI.bind(this.bindEvent, this)); |
|
|
|
if (opts.shadow) { |
|
this._createShadow(); |
|
} |
|
}, |
|
|
|
_createShadow: function () { |
|
var self = this, o = this.options; |
|
|
|
var assertMask = function () { |
|
if (!self.$mask) { |
|
self.$mask = BI.createWidget(BI.isObject(o.shadow) ? o.shadow : {}, { |
|
type: "bi.layout", |
|
cls: "bi-button-mask" |
|
}); |
|
self.$mask.invisible(); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: self, |
|
items: [{ |
|
el: self.$mask, |
|
left: 0, |
|
right: 0, |
|
top: 0, |
|
bottom: 0 |
|
}] |
|
}); |
|
} |
|
}; |
|
|
|
this.element.mouseup(function () { |
|
if (!self._hover && !o.isShadowShowingOnSelected) { |
|
assertMask(); |
|
self.$mask.invisible(); |
|
} |
|
}); |
|
this.element.on("mouseenter." + this.getName(), function (e) { |
|
if (self.element.__isMouseInBounds__(e)) { |
|
if (self.isEnabled() && !self._hover && (o.isShadowShowingOnSelected || !self.isSelected())) { |
|
assertMask(); |
|
self.$mask.visible(); |
|
} |
|
} |
|
}); |
|
this.element.on("mousemove." + this.getName(), function (e) { |
|
if (!self.element.__isMouseInBounds__(e)) { |
|
if (self.isEnabled() && !self._hover) { |
|
assertMask(); |
|
self.$mask.invisible(); |
|
} |
|
} |
|
}); |
|
this.element.on("mouseleave." + this.getName(), function () { |
|
if (self.isEnabled() && !self._hover) { |
|
assertMask(); |
|
self.$mask.invisible(); |
|
} |
|
}); |
|
}, |
|
|
|
bindEvent: function () { |
|
var self = this; |
|
var o = this.options, hand = this.handle(); |
|
if (!hand) { |
|
return; |
|
} |
|
hand = hand.element; |
|
switch (o.trigger) { |
|
case "mouseup": |
|
var mouseDown = false; |
|
hand.mousedown(function () { |
|
mouseDown = true; |
|
ev(e); |
|
}); |
|
hand.mouseup(function (e) { |
|
if (mouseDown === true) { |
|
clk(e); |
|
} |
|
mouseDown = false; |
|
ev(e); |
|
}); |
|
break; |
|
case "mousedown": |
|
var mouseDown = false; |
|
var selected = false; |
|
hand.mousedown(function (e) { |
|
// if (e.button === 0) { |
|
$(document).bind("mouseup." + self.getName(), function (e) { |
|
// if (e.button === 0) { |
|
if (BI.DOM.isExist(self) && !hand.__isMouseInBounds__(e) && mouseDown === true && !selected) { |
|
self.setSelected(!self.isSelected()); |
|
self._trigger(); |
|
} |
|
mouseDown = false; |
|
$(document).unbind("mouseup." + self.getName()); |
|
// } |
|
}); |
|
if (mouseDown === true) { |
|
return; |
|
} |
|
if (self.isSelected()) { |
|
selected = true; |
|
} else { |
|
clk(e); |
|
} |
|
mouseDown = true; |
|
ev(e); |
|
// } |
|
}); |
|
hand.mouseup(function (e) { |
|
// if (e.button === 0) { |
|
if (BI.DOM.isExist(self) && mouseDown === true && selected === true) { |
|
clk(e); |
|
} |
|
mouseDown = false; |
|
selected = false; |
|
$(document).unbind("mouseup." + self.getName()); |
|
// } |
|
}); |
|
break; |
|
case "dblclick": |
|
hand.dblclick(clk); |
|
break; |
|
default: |
|
hand.mousedown(function (e) { |
|
ev(e); |
|
}); |
|
hand.mouseup(function (e) { |
|
ev(e); |
|
}); |
|
hand.click(clk); |
|
break; |
|
} |
|
//之后的300ms点击无效 |
|
var onClick = BI.debounce(this.doClick, BI.EVENT_RESPONSE_TIME, true); |
|
|
|
function ev(e) { |
|
if (o.stopEvent) { |
|
e.stopEvent(); |
|
} |
|
if (o.stopPropagation) { |
|
e.stopPropagation(); |
|
} |
|
} |
|
|
|
function clk(e) { |
|
ev(e); |
|
if (!self.isEnabled() || (self.isOnce() && self.isSelected())) { |
|
return; |
|
} |
|
onClick.apply(self); |
|
} |
|
}, |
|
|
|
_trigger: function () { |
|
var o = this.options; |
|
if (this.isValid()) { |
|
o.handler.call(this, this.getValue(), this); |
|
var v = this.getValue(); |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.CLICK, v, this); |
|
this.fireEvent(BI.BasicButton.EVENT_CHANGE, v, this); |
|
} |
|
}, |
|
|
|
doClick: function () { |
|
if (!this.isDisableSelected()) { |
|
this.isForceSelected() ? this.setSelected(true) : |
|
(this.isForceNotSelected() ? this.setSelected(false) : |
|
this.setSelected(!this.isSelected())); |
|
} |
|
this._trigger(); |
|
}, |
|
|
|
handle: function () { |
|
return this; |
|
}, |
|
|
|
hover: function () { |
|
this._hover = true; |
|
this.handle().element.addClass("hover"); |
|
if (this.options.shadow) { |
|
this.$mask && this.$mask.setVisible(true); |
|
} |
|
}, |
|
|
|
dishover: function () { |
|
this._hover = false; |
|
this.handle().element.removeClass("hover"); |
|
if (this.options.shadow) { |
|
this.$mask && this.$mask.setVisible(false); |
|
} |
|
}, |
|
|
|
setSelected: function (b) { |
|
var o = this.options; |
|
o.selected = b; |
|
if (this.isSelected()) { |
|
this.handle().element.addClass("active"); |
|
} else { |
|
this.handle().element.removeClass("active"); |
|
} |
|
if (o.shadow && !o.isShadowShowingOnSelected) { |
|
this.$mask && this.$mask.setVisible(false); |
|
} |
|
}, |
|
|
|
isSelected: function () { |
|
return this.options.selected; |
|
}, |
|
|
|
isOnce: function () { |
|
return this.options.once; |
|
}, |
|
|
|
isForceSelected: function () { |
|
return this.options.forceSelected; |
|
}, |
|
|
|
isForceNotSelected: function () { |
|
return this.options.forceNotSelected; |
|
}, |
|
|
|
isDisableSelected: function () { |
|
return this.options.disableSelected; |
|
}, |
|
|
|
setText: function (text) { |
|
this.options.text = text; |
|
}, |
|
|
|
getText: function () { |
|
return this.options.text; |
|
}, |
|
|
|
setEnable: function (b) { |
|
BI.BasicButton.superclass.setEnable.apply(this, arguments); |
|
if (!b) { |
|
if (this.options.shadow) { |
|
this.$mask && this.$mask.setVisible(false); |
|
} |
|
} |
|
}, |
|
|
|
empty: function () { |
|
$(document).unbind("mouseup." + this.getName()); |
|
BI.BasicButton.superclass.empty.apply(this, arguments); |
|
}, |
|
|
|
destroy: function () { |
|
BI.BasicButton.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.BasicButton.EVENT_CHANGE = "BasicButton.EVENT_CHANGE";/** |
|
* 表示一个可以展开的节点, 不仅有选中状态而且有展开状态 |
|
* |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.NodeButton |
|
* @extends BI.BasicButton |
|
* @abstract |
|
*/ |
|
BI.NodeButton = BI.inherit(BI.BasicButton, { |
|
_defaultConfig: function() { |
|
var conf = BI.NodeButton.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend( conf, { |
|
baseCls: (conf.baseCls || "") + " bi-node", |
|
open: false |
|
}) |
|
}, |
|
|
|
_init:function() { |
|
BI.NodeButton.superclass._init.apply(this, arguments); |
|
var self = this; |
|
BI.nextTick(function(){ |
|
self.setOpened(self.isOpened()); |
|
}) |
|
}, |
|
|
|
doClick: function(){ |
|
BI.NodeButton.superclass.doClick.apply(this, arguments); |
|
this.setOpened(!this.isOpened()); |
|
}, |
|
|
|
isOnce: function(){ |
|
return false; |
|
}, |
|
|
|
isOpened: function(){ |
|
return !!this.options.open; |
|
}, |
|
|
|
setOpened: function(b){ |
|
this.options.open = !!b; |
|
}, |
|
|
|
triggerCollapse: function(){ |
|
if(this.isOpened()) { |
|
this.setOpened(false); |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.COLLAPSE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
triggerExpand: function(){ |
|
if(!this.isOpened()) { |
|
this.setOpened(true); |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EXPAND, this.getValue(), this); |
|
} |
|
} |
|
});/** |
|
* Created by GUY on 2015/6/26. |
|
* @class BI.ButtonGroup |
|
* @extends BI.Widget |
|
*/ |
|
|
|
BI.ButtonGroup = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.ButtonGroup.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-button-group", |
|
behaviors: {}, |
|
items: [ |
|
{ |
|
el: {type: "bi.text_button", text: "", value: ""} |
|
} |
|
], |
|
chooseType: BI.Selection.Single, |
|
layouts: [{ |
|
type: "bi.center", |
|
hgap: 0, |
|
vgap: 0 |
|
}] |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.ButtonGroup.superclass._init.apply(this, arguments); |
|
var behaviors = {}; |
|
BI.each(this.options.behaviors, function (key, rule) { |
|
behaviors[key] = BI.BehaviorFactory.createBehavior(key, { |
|
rule: rule |
|
}) |
|
}); |
|
this.behaviors = behaviors; |
|
this.populate(this.options.items); |
|
}, |
|
|
|
_createBtns: function (items) { |
|
var o = this.options; |
|
return BI.createWidgets(BI.createItems(items, { |
|
type: "bi.text_button" |
|
})); |
|
}, |
|
|
|
_btnsCreator: function (items) { |
|
var self = this, args = Array.prototype.slice.call(arguments), o = this.options; |
|
var buttons = this._createBtns(items); |
|
args[0] = buttons; |
|
|
|
BI.each(this.behaviors, function (i, behavior) { |
|
behavior.doBehavior.apply(behavior, args); |
|
}); |
|
BI.each(buttons, function (i, btn) { |
|
btn.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
if (type === BI.Events.CLICK) { |
|
switch (o.chooseType) { |
|
case BI.ButtonGroup.CHOOSE_TYPE_SINGLE: |
|
self.setValue(btn.getValue()); |
|
break; |
|
case BI.ButtonGroup.CHOOSE_TYPE_NONE: |
|
self.setValue([]); |
|
break; |
|
} |
|
self.fireEvent(BI.ButtonGroup.EVENT_CHANGE, value, obj); |
|
} |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
}); |
|
btn.on(BI.Events.DESTROY, function () { |
|
BI.remove(self.buttons, btn); |
|
}) |
|
}); |
|
|
|
return buttons; |
|
}, |
|
|
|
_packageBtns: function (btns) { |
|
var o = this.options; |
|
|
|
for (var i = o.layouts.length - 1; i > 0; i--) { |
|
btns = BI.map(btns, function (k, it) { |
|
return BI.extend({}, o.layouts[i], { |
|
items: [ |
|
BI.extend({}, o.layouts[i].el, { |
|
el: it |
|
}) |
|
] |
|
}) |
|
}) |
|
} |
|
return btns; |
|
}, |
|
|
|
_packageItems: function (items, packBtns) { |
|
return BI.createItems(BI.makeArrayByArray(items, {}), BI.clone(packBtns)); |
|
}, |
|
|
|
_packageLayout: function (items) { |
|
var o = this.options, layout = BI.deepClone(o.layouts[0]); |
|
|
|
var lay = BI.formatEL(layout).el; |
|
while (lay && lay.items && !BI.isEmpty(lay.items)) { |
|
lay = BI.formatEL(lay.items[0]).el; |
|
} |
|
lay.items = items; |
|
return layout; |
|
}, |
|
|
|
|
|
doBehavior: function () { |
|
var args = Array.prototype.slice.call(arguments); |
|
args.unshift(this.buttons); |
|
BI.each(this.behaviors, function (i, behavior) { |
|
behavior.doBehavior.apply(behavior, args); |
|
}) |
|
}, |
|
|
|
prependItems: function (items) { |
|
var o = this.options; |
|
var btns = this._btnsCreator.apply(this, arguments); |
|
this.buttons = BI.concat(btns, this.buttons); |
|
|
|
//如果是一个简单的layout |
|
if (o.layouts.length === 1 && !BI.isNotEmptyArray(o.layouts[0].items) |
|
&& this.layouts && this.layouts.prependItems) { |
|
this.layouts.prependItems(btns); |
|
return; |
|
} |
|
|
|
var items = this._packageItems(items, this._packageBtns(btns)); |
|
BI.createWidget(BI.extend(this._packageLayout(items))).element.children().prependTo(this.element); |
|
}, |
|
|
|
addItems: function (items) { |
|
var o = this.options; |
|
var btns = this._btnsCreator.apply(this, arguments); |
|
this.buttons = BI.concat(this.buttons, btns); |
|
|
|
//如果是一个简单的layout |
|
if (o.layouts.length === 1 && !BI.isNotEmptyArray(o.layouts[0].items) |
|
&& this.layouts && this.layouts.addItems) { |
|
this.layouts.addItems(btns); |
|
return; |
|
} |
|
|
|
var items = this._packageItems(items, this._packageBtns(btns)); |
|
BI.createWidget(BI.extend(this._packageLayout(items))).element.children().appendTo(this.element); |
|
}, |
|
|
|
removeItemAt: function (indexes) { |
|
var self = this; |
|
indexes = BI.isArray(indexes) ? indexes : [indexes]; |
|
var buttons = []; |
|
BI.each(indexes, function (i, index) { |
|
buttons.push(self.buttons[index]); |
|
}); |
|
BI.each(buttons, function (i, btn) { |
|
btn && btn.destroy(); |
|
}) |
|
}, |
|
|
|
removeItems: function (v) { |
|
v = BI.isArray(v) ? v : [v]; |
|
var indexes = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (BI.deepContains(v, item.getValue())) { |
|
indexes.push(i); |
|
} |
|
}); |
|
this.removeItemAt(indexes); |
|
}, |
|
|
|
populate: function (items) { |
|
this.options.items = items || []; |
|
this.empty(); |
|
|
|
this.buttons = this._btnsCreator.apply(this, arguments); |
|
var items = this._packageItems(items, this._packageBtns(this.buttons)); |
|
|
|
this.layouts = BI.createWidget(BI.extend({element: this}, this._packageLayout(items))); |
|
}, |
|
|
|
setEnable: function (b) { |
|
BI.ButtonGroup.superclass.setEnable.apply(this, arguments); |
|
BI.each(this.buttons, function (i, item) { |
|
item.setEnable(b); |
|
}); |
|
}, |
|
|
|
setNotSelectedValue: function (v) { |
|
v = BI.isArray(v) ? v : [v]; |
|
BI.each(this.buttons, function (i, item) { |
|
if (BI.deepContains(v, item.getValue())) { |
|
item.setSelected && item.setSelected(false); |
|
} else { |
|
item.setSelected && item.setSelected(true); |
|
} |
|
}); |
|
}, |
|
|
|
setEnabledValue: function (v) { |
|
v = BI.isArray(v) ? v : [v]; |
|
BI.each(this.buttons, function (i, item) { |
|
if (BI.deepContains(v, item.getValue())) { |
|
item.setEnable(true); |
|
} else { |
|
item.setEnable(false); |
|
} |
|
}); |
|
}, |
|
|
|
setValue: function (v) { |
|
v = BI.isArray(v) ? v : [v]; |
|
BI.each(this.buttons, function (i, item) { |
|
if (BI.deepContains(v, item.getValue())) { |
|
item.setSelected && item.setSelected(true); |
|
} else { |
|
item.setSelected && item.setSelected(false); |
|
} |
|
}); |
|
}, |
|
|
|
getNotSelectedValue: function () { |
|
var v = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isEnabled() && !(item.isSelected && item.isSelected())) { |
|
v.push(item.getValue()); |
|
} |
|
}); |
|
return v; |
|
}, |
|
|
|
getValue: function () { |
|
var v = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isEnabled() && item.isSelected && item.isSelected()) { |
|
v.push(item.getValue()); |
|
} |
|
}); |
|
return v; |
|
}, |
|
|
|
getAllButtons: function () { |
|
return this.buttons; |
|
}, |
|
|
|
getAllLeaves: function () { |
|
return this.buttons; |
|
}, |
|
|
|
getSelectedButtons: function () { |
|
var btns = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isSelected && item.isSelected()) { |
|
btns.push(item); |
|
} |
|
}); |
|
return btns; |
|
}, |
|
|
|
getNotSelectedButtons: function () { |
|
var btns = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isSelected && !item.isSelected()) { |
|
btns.push(item); |
|
} |
|
}); |
|
return btns; |
|
}, |
|
|
|
getIndexByValue: function (value) { |
|
var index = -1; |
|
BI.any(this.buttons, function (i, item) { |
|
if (item.isEnabled() && item.getValue() === value) { |
|
index = i; |
|
return true; |
|
} |
|
}); |
|
return index; |
|
}, |
|
|
|
getNodeById: function (id) { |
|
var node; |
|
BI.any(this.buttons, function (i, item) { |
|
if (item.isEnabled() && item.options.id === id) { |
|
node = item; |
|
return true; |
|
} |
|
}); |
|
return node; |
|
}, |
|
|
|
getNodeByValue: function (value) { |
|
var node; |
|
BI.any(this.buttons, function (i, item) { |
|
if (item.isEnabled() && item.getValue() === value) { |
|
node = item; |
|
return true; |
|
} |
|
}); |
|
return node; |
|
}, |
|
|
|
destroy: function () { |
|
BI.ButtonGroup.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.extend(BI.ButtonGroup, { |
|
CHOOSE_TYPE_SINGLE: BI.Selection.Single, |
|
CHOOSE_TYPE_MULTI: BI.Selection.Multi, |
|
CHOOSE_TYPE_ALL: BI.Selection.All, |
|
CHOOSE_TYPE_NONE: BI.Selection.None, |
|
CHOOSE_TYPE_DEFAULT: BI.Selection.Default |
|
}); |
|
BI.ButtonGroup.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.button_group", BI.ButtonGroup);/** |
|
* Created by GUY on 2015/8/10. |
|
* @class BI.ButtonTree |
|
* @extends BI.ButtonGroup |
|
*/ |
|
|
|
BI.ButtonTree = BI.inherit(BI.ButtonGroup, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.ButtonTree.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-button-tree" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.ButtonTree.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
setNotSelectedValue: function (v) { |
|
v = BI.isArray(v) ? v : [v]; |
|
BI.each(this.buttons, function (i, item) { |
|
if (!BI.isFunction(item.setSelected)) { |
|
item.setNotSelectedValue(v); |
|
return; |
|
} |
|
if (BI.deepContains(v, item.getValue())) { |
|
item.setSelected(false); |
|
} else { |
|
item.setSelected(true); |
|
} |
|
}); |
|
}, |
|
|
|
setEnabledValue: function (v) { |
|
v = BI.isArray(v) ? v : [v]; |
|
BI.each(this.buttons, function (i, item) { |
|
if (BI.isFunction(item.setEnabledValue)) { |
|
item.setEnabledValue(v); |
|
return; |
|
} |
|
if (BI.deepContains(v, item.getValue())) { |
|
item.setEnable(true); |
|
} else { |
|
item.setEnable(false); |
|
} |
|
}); |
|
}, |
|
|
|
setValue: function (v) { |
|
v = BI.isArray(v) ? v : [v]; |
|
BI.each(this.buttons, function (i, item) { |
|
if (!BI.isFunction(item.setSelected)) { |
|
item.setValue(v); |
|
return; |
|
} |
|
if (BI.deepContains(v, item.getValue())) { |
|
item.setSelected(true); |
|
} else { |
|
item.setSelected(false); |
|
} |
|
}); |
|
}, |
|
|
|
getNotSelectedValue: function () { |
|
var v = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isEnabled() && !BI.isFunction(item.setSelected)) { |
|
v = BI.concat(v, item.getNotSelectedValue()); |
|
return; |
|
} |
|
if (item.isEnabled() && item.isSelected && !item.isSelected()) { |
|
v.push(item.getValue()); |
|
} |
|
}); |
|
return v; |
|
}, |
|
|
|
getValue: function () { |
|
var v = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isEnabled() && !BI.isFunction(item.setSelected)) { |
|
v = BI.concat(v, item.getValue()); |
|
return; |
|
} |
|
if (item.isEnabled() && item.isSelected && item.isSelected()) { |
|
v.push(item.getValue()); |
|
} |
|
}); |
|
return v; |
|
}, |
|
|
|
getSelectedButtons: function () { |
|
var btns = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isEnabled() && !BI.isFunction(item.setSelected)) { |
|
btns = btns.concat(item.getSelectedButtons()); |
|
return; |
|
} |
|
if (item.isSelected && item.isSelected()) { |
|
btns.push(item); |
|
} |
|
}); |
|
return btns; |
|
}, |
|
|
|
getNotSelectedButtons: function () { |
|
var btns = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isEnabled() && !BI.isFunction(item.setSelected)) { |
|
btns = btns.concat(item.getNotSelectedButtons()); |
|
return; |
|
} |
|
if (item.isSelected && !item.isSelected()) { |
|
btns.push(item); |
|
} |
|
}); |
|
return btns; |
|
}, |
|
|
|
//获取所有的叶子节点 |
|
getAllLeaves: function () { |
|
var leaves = []; |
|
BI.each(this.buttons, function (i, item) { |
|
if (item.isEnabled() && !BI.isFunction(item.setSelected)) { |
|
leaves = leaves.concat(item.getAllLeaves()); |
|
return; |
|
} |
|
if (item.isEnabled()) { |
|
leaves.push(item); |
|
} |
|
}); |
|
return leaves; |
|
}, |
|
|
|
getIndexByValue: function (value) { |
|
var index = -1; |
|
BI.any(this.buttons, function (i, item) { |
|
var vs = item.getValue(); |
|
if (item.isEnabled() && (vs === value || BI.contains(vs, value))) { |
|
index = i; |
|
return true; |
|
} |
|
}); |
|
return index; |
|
}, |
|
|
|
getNodeById: function (id) { |
|
var node; |
|
BI.any(this.buttons, function (i, item) { |
|
if (item.isEnabled()) { |
|
if (item.attr("id") === id) { |
|
node = item; |
|
return true; |
|
} else if (BI.isFunction(item.getNodeById)) { |
|
if (node = item.getNodeById(id)) { |
|
return true; |
|
} |
|
} |
|
} |
|
}); |
|
return node; |
|
}, |
|
|
|
getNodeByValue: function (value) { |
|
var node; |
|
BI.any(this.buttons, function (i, item) { |
|
if (item.isEnabled()) { |
|
if (item.getValue() === value) { |
|
node = item; |
|
return true; |
|
} else if (BI.isFunction(item.getNodeByValue)) { |
|
if (node = item.getNodeByValue(value)) { |
|
return true; |
|
} |
|
} |
|
} |
|
}); |
|
return node; |
|
} |
|
}); |
|
BI.ButtonTree.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.button_tree", BI.ButtonTree);/** |
|
* value作为key值缓存button, 不支持顺序读写 |
|
* 适合用于频繁增删的一组button |
|
* Created by GUY on 2015/8/10. |
|
* @class BI.ButtonMap |
|
* @extends BI.ButtonTree |
|
*/ |
|
|
|
BI.ButtonMap = BI.inherit(BI.ButtonTree, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.ButtonMap.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-button-map" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.ButtonMap.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
_createBtns: function (items) { |
|
var self = this, o = this.options; |
|
var buttons = BI.createWidgets(BI.createItems(items, {type: "bi.text_button", once: o.chooseType === 0})); |
|
var keys = BI.map(items, function (i, item) { |
|
item = BI.stripEL(item); |
|
if (!(item.id || item.value)) { |
|
throw new Error("item必须包含id或value属性"); |
|
} |
|
return item.id || item.value; |
|
}); |
|
return BI.object(keys, buttons); |
|
}, |
|
|
|
setValue: function (v) { |
|
var self = this; |
|
v = BI.isArray(v) ? v : [v]; |
|
BI.each(this.buttons, function (val, item) { |
|
if (!BI.isFunction(item.setSelected)) { |
|
item.setValue(v); |
|
return; |
|
} |
|
if (v.contains(val)) { |
|
item.setSelected && item.setSelected(true); |
|
} else { |
|
item.setSelected && item.setSelected(false); |
|
} |
|
}); |
|
}, |
|
|
|
setNotSelectedValue: function (v) { |
|
var self = this; |
|
v = BI.isArray(v) ? v : [v]; |
|
BI.each(this.buttons, function (val, item) { |
|
if (!BI.isFunction(item.setSelected)) { |
|
item.setNotSelectedValue(v); |
|
return; |
|
} |
|
if (v.contains(val)) { |
|
item.setSelected && item.setSelected(false); |
|
} else { |
|
item.setSelected && item.setSelected(true); |
|
} |
|
}); |
|
}, |
|
|
|
populate: function (items) { |
|
var self = this; |
|
var args = [].slice.call(arguments); |
|
var linkHashMap = new BI.LinkHashMap(); |
|
var val = function (item) { |
|
return item.id || item.value; |
|
}; |
|
if (!this.buttons) { |
|
this.buttons = {}; |
|
} |
|
//所有已存在的和新添加的 |
|
var willCreated = []; |
|
BI.each(items, function (i, item) { |
|
item = BI.stripEL(item); |
|
if (self.buttons[val(item)]) { |
|
var ob = self.buttons[val(item)]; |
|
args[0] = item.items; |
|
args[2] = item; |
|
ob.populate && ob.populate.apply(ob, args); |
|
} else { |
|
willCreated.push(item); |
|
} |
|
}); |
|
//创建新元素 |
|
args[0] = willCreated; |
|
var newBtns = this._btnsCreator.apply(this, args); |
|
|
|
//整理 |
|
var array = []; |
|
BI.each(items, function (i, item) { |
|
item = BI.stripEL(item); |
|
var button = self.buttons[val(item)] || newBtns[val(item)]; |
|
linkHashMap.add(val(item), button); |
|
array.push(button); |
|
}); |
|
this.buttons = linkHashMap.map; |
|
|
|
BI.DOM.hang(this.buttons); |
|
this.empty(); |
|
|
|
var packages = this._packageItems(items, this._packageBtns(array)); |
|
BI.createWidget(BI.extend({element: this}, this._packageLayout(packages))); |
|
}, |
|
|
|
getIndexByValue: function (value) { |
|
throw new Error("不能使用getIndexByValue方法"); |
|
} |
|
}); |
|
BI.ButtonMap.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.button_map", BI.ButtonMap);/** |
|
* guy |
|
* 异步树 |
|
* @class BI.TreeView |
|
* @extends BI.Pane |
|
*/ |
|
BI.TreeView = BI.inherit(BI.Pane, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.TreeView.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-tree", |
|
paras: {}, |
|
itemsCreator: BI.emptyFn |
|
}) |
|
}, |
|
_init: function () { |
|
BI.TreeView.superclass._init.apply(this, arguments); |
|
|
|
|
|
|
|
|
|
this._stop = false; |
|
this.container = BI.createWidget(); |
|
|
|
this._createTree(); |
|
this.tip = BI.createWidget({ |
|
type: "bi.loading_bar", |
|
invisible: true, |
|
handler: BI.bind(this._loadMore, this) |
|
}); |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: true, |
|
scrolly: false, |
|
element: this, |
|
items: [this.container, this.tip] |
|
}); |
|
|
|
}, |
|
|
|
_createTree: function () { |
|
this.id = "bi-tree" + BI.UUID(); |
|
if (this.nodes) { |
|
this.nodes.destroy(); |
|
} |
|
if (this.tree) { |
|
this.tree.destroy(); |
|
} |
|
this.tree = BI.createWidget({ |
|
type: "bi.layout", |
|
element: "<ul id='" + this.id + "' class='ztree'></ul>" |
|
}); |
|
BI.createWidget({ |
|
type: "bi.default", |
|
element: this.container, |
|
items: [this.tree] |
|
}); |
|
}, |
|
|
|
//选择节点触发方法 |
|
_selectTreeNode: function (treeId, treeNode) { |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.CLICK, treeNode, this); |
|
this.fireEvent(BI.TreeView.EVENT_CHANGE, treeNode, this); |
|
}, |
|
|
|
//配置属性 |
|
_configSetting: function () { |
|
var paras = this.options.paras; |
|
var self = this; |
|
var setting = { |
|
async: { |
|
enable: true, |
|
url: getUrl, |
|
autoParam: ["id", "name"], |
|
otherParam: BI.cjkEncodeDO(paras) |
|
}, |
|
check: { |
|
enable: true |
|
}, |
|
data: { |
|
key: { |
|
title: "title", |
|
name: "text" |
|
}, |
|
simpleData: { |
|
enable: true |
|
} |
|
}, |
|
view: { |
|
showIcon: false, |
|
expandSpeed: "", |
|
nameIsHTML: true, |
|
dblClickExpand: false |
|
}, |
|
callback: { |
|
beforeExpand: beforeExpand, |
|
onAsyncSuccess: onAsyncSuccess, |
|
onAsyncError: onAsyncError, |
|
beforeCheck: beforeCheck, |
|
onCheck: onCheck, |
|
onExpand: onExpand, |
|
onCollapse: onCollapse, |
|
onClick: onClick |
|
} |
|
}; |
|
var className = "dark", perTime = 100; |
|
|
|
function onClick(event, treeId, treeNode) { |
|
self.nodes.checkNode(treeNode, !treeNode.checked, true, true); |
|
} |
|
|
|
function getUrl(treeId, treeNode) { |
|
var parentNode = self._getParentValues(treeNode); |
|
treeNode.times = treeNode.times || 1; |
|
var param = "id=" + treeNode.id |
|
+ "×=" + (treeNode.times++) |
|
+ "&parent_values= " + window.encodeURIComponent(BI.jsonEncode(parentNode)) |
|
+ "&check_state=" + window.encodeURIComponent(BI.jsonEncode(treeNode.getCheckStatus())); |
|
|
|
return BI.servletURL + '?op=' + self.options.op + '&cmd=' + self.options.cmd + "&" + param; |
|
} |
|
|
|
function beforeExpand(treeId, treeNode) { |
|
if (!treeNode.isAjaxing) { |
|
if (!treeNode.children) { |
|
treeNode.times = 1; |
|
ajaxGetNodes(treeNode, "refresh"); |
|
} |
|
return true; |
|
} else { |
|
BI.Msg.toast("Please Wait。", "warning"); |
|
return false; |
|
} |
|
} |
|
|
|
function onAsyncSuccess(event, treeId, treeNode, msg) { |
|
treeNode.halfCheck = false; |
|
if (!msg || msg.length === 0 || /^<html>[\s,\S]*<\/html>$/gi.test(msg) || self._stop) { |
|
return; |
|
} |
|
var zTree = self.nodes; |
|
var totalCount = treeNode.count || 0; |
|
|
|
//尝试去获取下一组节点,若获取值为空数组,表示获取完成 |
|
// TODO by GUY |
|
if (treeNode.children.length > totalCount) { |
|
treeNode.count = treeNode.children.length; |
|
BI.delay(function () { |
|
ajaxGetNodes(treeNode); |
|
}, perTime); |
|
} else { |
|
//treeNode.icon = ""; |
|
zTree.updateNode(treeNode); |
|
zTree.selectNode(treeNode.children[0]); |
|
//className = (className === "dark" ? "":"dark"); |
|
} |
|
} |
|
|
|
function onAsyncError(event, treeId, treeNode, XMLHttpRequest, textStatus, errorThrown) { |
|
var zTree = self.nodes; |
|
BI.Msg.toast("Error!", "warning"); |
|
//treeNode.icon = ""; |
|
//zTree.updateNode(treeNode); |
|
} |
|
|
|
function ajaxGetNodes(treeNode, reloadType) { |
|
var zTree = self.nodes; |
|
if (reloadType == "refresh") { |
|
//treeNode.icon = BI.servletURL +"?op=resource&resource=/com/fr/bi/web/css/base/third/ztree/img/loading.gif"; |
|
zTree.updateNode(treeNode); |
|
} |
|
zTree.reAsyncChildNodes(treeNode, reloadType, true); |
|
} |
|
|
|
function beforeCheck(treeId, treeNode) { |
|
treeNode.halfCheck = false; |
|
if (treeNode.checked === true) { |
|
//将展开的节点halfCheck设为false,解决展开节点存在halfCheck=true的情况 guy |
|
//所有的半选状态都需要取消halfCheck=true的情况 |
|
function track(children) { |
|
BI.each(children, function (i, ch) { |
|
if (ch.halfCheck === true) { |
|
ch.halfCheck = false; |
|
track(ch.children); |
|
} |
|
}) |
|
} |
|
|
|
track(treeNode.children); |
|
var treeObj = self.nodes; |
|
var nodes = treeObj.getSelectedNodes(); |
|
$.each(nodes, function (index, node) { |
|
node.halfCheck = false; |
|
}) |
|
} |
|
} |
|
|
|
function onCheck(event, treeId, treeNode) { |
|
self._selectTreeNode(treeId, treeNode); |
|
} |
|
|
|
function onExpand(event, treeId, treeNode) { |
|
treeNode.halfCheck = false; |
|
} |
|
|
|
function onCollapse(event, treeId, treeNode) { |
|
} |
|
|
|
return setting; |
|
}, |
|
|
|
_getParentValues: function (treeNode) { |
|
if (!treeNode.getParentNode()) { |
|
return []; |
|
} |
|
var parentNode = treeNode.getParentNode(); |
|
var result = this._getParentValues(parentNode); |
|
result = result.concat([this._getNodeValue(parentNode)]); |
|
return result; |
|
}, |
|
|
|
_getNodeValue: function (node) { |
|
//去除标红 |
|
return node.value || node.text.replace(/<[^>]+>/g, ""); |
|
}, |
|
|
|
//获取半选框值 |
|
_getHalfSelectedValues: function (map, node) { |
|
var self = this; |
|
var checkState = node.getCheckStatus(); |
|
//将未选的去掉 |
|
if (checkState.checked === false && checkState.half === false) { |
|
return; |
|
} |
|
//如果节点已展开,并且是半选 |
|
if (BI.isNotEmptyArray(node.children) && checkState.half === true) { |
|
var children = node.children; |
|
BI.each(children, function (i, ch) { |
|
self._getHalfSelectedValues(map, ch); |
|
}); |
|
return; |
|
} |
|
var parent = node.parentValues || self._getParentValues(node); |
|
var path = parent.concat(this._getNodeValue(node)); |
|
if (BI.isNotEmptyArray(node.children) || checkState.half === false) { |
|
this._buildTree(map, path); |
|
return; |
|
} |
|
var storeValues = BI.deepClone(this.options.paras.selected_values); |
|
var treeNode = this._getTree(storeValues, path); |
|
this._addTreeNode(map, parent, this._getNodeValue(node), treeNode); |
|
}, |
|
|
|
_getTree: function (map, values) { |
|
var cur = map; |
|
BI.any(values, function (i, value) { |
|
if (cur[value] == null) { |
|
return true; |
|
} |
|
cur = cur[value]; |
|
}); |
|
return cur; |
|
}, |
|
|
|
_addTreeNode: function (map, values, key, value) { |
|
var cur = map; |
|
BI.each(values, function (i, value) { |
|
if (cur[value] == null) { |
|
cur[value] = {}; |
|
} |
|
cur = cur[value]; |
|
}); |
|
cur[key] = value; |
|
}, |
|
|
|
//构造树节点 |
|
_buildTree: function (map, values) { |
|
var cur = map; |
|
BI.each(values, function (i, value) { |
|
if (cur[value] == null) { |
|
cur[value] = {}; |
|
} |
|
cur = cur[value]; |
|
}) |
|
}, |
|
|
|
//获取选中的值 |
|
_getSelectedValues: function () { |
|
var self = this; |
|
var hashMap = {}; |
|
var rootNoots = this.nodes.getNodes(); |
|
track(rootNoots); |
|
function track(nodes) { |
|
BI.each(nodes, function (i, node) { |
|
var checkState = node.getCheckStatus(); |
|
if (checkState.checked === true || checkState.half === true) { |
|
if (checkState.half === true) { |
|
self._getHalfSelectedValues(hashMap, node); |
|
} else { |
|
var parentValues = node.parentValues || self._getParentValues(node); |
|
var values = parentValues.concat([self._getNodeValue(node)]); |
|
self._buildTree(hashMap, values); |
|
} |
|
} |
|
}) |
|
} |
|
|
|
return hashMap; |
|
}, |
|
|
|
//处理节点 |
|
_dealWidthNodes: function (nodes) { |
|
var self = this, o = this.options; |
|
//处理标红 |
|
if (BI.isKey(o.paras.keyword)) { |
|
var keyword = o.paras.keyword; |
|
var ns = BI.Tree.arrayFormat(nodes); |
|
BI.each(ns, function (i, n) { |
|
n.text = $("<div>").__textKeywordMarked__(n.text, keyword, n.py).html(); |
|
}); |
|
} |
|
return nodes; |
|
}, |
|
|
|
_loadMore: function () { |
|
var self = this, o = this.options; |
|
this.tip.setLoading(); |
|
var op = BI.extend({}, o.paras, { |
|
times: ++this.times |
|
}); |
|
o.itemsCreator(op, function (res) { |
|
if (self._stop === true) { |
|
return; |
|
} |
|
var hasNext = !!res.hasNext, nodes = res.items || []; |
|
|
|
if (!hasNext) { |
|
self.tip.setEnd(); |
|
} else { |
|
self.tip.setLoaded(); |
|
} |
|
if (nodes.length > 0) { |
|
self.nodes.addNodes(null, self._dealWidthNodes(nodes)); |
|
} |
|
}); |
|
}, |
|
|
|
//生成树内部方法 |
|
_initTree: function (setting) { |
|
var self = this, o = this.options; |
|
self.fireEvent(BI.Events.INIT); |
|
this.times = 1; |
|
var tree = this.tree; |
|
tree.empty(); |
|
this.loading(); |
|
this.tip.setVisible(false); |
|
var callback = function (nodes) { |
|
if (self._stop === true) { |
|
return; |
|
} |
|
self.nodes = $.fn.zTree.init(tree.element, setting, nodes); |
|
}; |
|
var op = BI.extend({}, o.paras, { |
|
times: 1, |
|
type: BICst.TREE.TREE_REQ_TYPE.INIT_DATA |
|
}); |
|
|
|
o.itemsCreator(op, function (res) { |
|
if (self._stop === true) { |
|
return; |
|
} |
|
var hasNext = !!res.hasNext, nodes = res.items || []; |
|
if (nodes.length > 0) { |
|
callback(self._dealWidthNodes(nodes)); |
|
} |
|
self.setTipVisible(nodes.length <= 0); |
|
self.loaded(); |
|
if (!hasNext) { |
|
self.tip.invisible(); |
|
} else { |
|
self.tip.setLoaded(); |
|
} |
|
op.times === 1 && self.fireEvent(BI.Events.AFTERINIT); |
|
}); |
|
}, |
|
|
|
//构造树结构, |
|
initTree: function (nodes, setting) { |
|
var setting = setting || { |
|
async: { |
|
enable: false |
|
}, |
|
check: { |
|
enable: false |
|
}, |
|
data: { |
|
key: { |
|
title: "title", |
|
name: "text" |
|
}, |
|
simpleData: { |
|
enable: true |
|
} |
|
}, |
|
view: { |
|
showIcon: false, |
|
expandSpeed: "" |
|
}, |
|
callback: {} |
|
}; |
|
this.nodes = $.fn.zTree.init(this.tree.element, setting, nodes); |
|
}, |
|
|
|
start: function () { |
|
this._stop = false; |
|
}, |
|
|
|
stop: function () { |
|
this._stop = true; |
|
}, |
|
|
|
//生成树方法 |
|
stroke: function (config) { |
|
delete this.options.keyword; |
|
BI.extend(this.options.paras, config); |
|
var setting = this._configSetting(); |
|
this._createTree(); |
|
this.start(); |
|
this._initTree(setting); |
|
}, |
|
|
|
populate: function () { |
|
this.stroke.apply(this, arguments); |
|
}, |
|
|
|
hasChecked: function () { |
|
var treeObj = this.nodes; |
|
return treeObj.getCheckedNodes(true).length > 0; |
|
}, |
|
|
|
checkAll: function (checked) { |
|
this.nodes && this.nodes.checkAllNodes(checked); |
|
}, |
|
|
|
expandAll: function (flag) { |
|
this.nodes && this.nodes.expandAll(flag); |
|
}, |
|
|
|
setValue: function (value, param) { |
|
this.setSelectedValue(value); |
|
this.checkAll(false); |
|
this.updateValue(value, param); |
|
this.refresh(); |
|
}, |
|
|
|
setSelectedValue: function (value) { |
|
this.options.paras.selected_values = value || {}; |
|
this.selected_values = BI.deepClone(value) || {}; |
|
}, |
|
|
|
updateValue: function (values, param) { |
|
if (!this.nodes) { |
|
return; |
|
} |
|
param || (param = "value"); |
|
var treeObj = this.nodes; |
|
BI.each(values, function (v, op) { |
|
var nodes = treeObj.getNodesByParam(param, v, null); |
|
BI.each(nodes, function (j, node) { |
|
BI.extend(node, {checked: true}, op); |
|
treeObj.updateNode(node); |
|
}) |
|
}); |
|
}, |
|
|
|
refresh: function () { |
|
this.nodes && this.nodes.refresh(); |
|
}, |
|
|
|
getValue: function () { |
|
if (!this.nodes) { |
|
return null; |
|
} |
|
return this._getSelectedValues(); |
|
}, |
|
|
|
empty: function () { |
|
BI.isNotNull(this.nodes) && this.nodes.destroy(); |
|
}, |
|
|
|
destroy: function () { |
|
this.stop(); |
|
this.nodes && this.nodes.destroy(); |
|
BI.TreeView.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.extend(BI.TreeView, { |
|
REQ_TYPE_INIT_DATA: 1, |
|
REQ_TYPE_ADJUST_DATA: 2, |
|
REQ_TYPE_CALCULATE_SELECT_DATA: 3, |
|
REQ_TYPE_SELECTED_DATA: 4 |
|
}); |
|
|
|
BI.TreeView.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.TreeView.EVENT_INIT = BI.Events.INIT; |
|
BI.TreeView.EVENT_AFTERINIT = BI.Events.AFTERINIT; |
|
|
|
$.shortcut("bi.tree", BI.TreeView);/** |
|
* guy |
|
* 同步树 |
|
* @class BI.SyncTree |
|
* @extends BI.TreeView |
|
*/ |
|
BI.SyncTree = BI.inherit(BI.TreeView, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.SyncTree.superclass._defaultConfig.apply(this, arguments), {}) |
|
}, |
|
_init: function () { |
|
BI.SyncTree.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
//配置属性 |
|
_configSetting: function () { |
|
var paras = this.options.paras; |
|
var self = this; |
|
var setting = { |
|
async: { |
|
enable: false, |
|
otherParam: BI.cjkEncodeDO(paras) |
|
}, |
|
check: { |
|
enable: true |
|
}, |
|
data: { |
|
key: { |
|
title: "title", |
|
name: "text" |
|
}, |
|
simpleData: { |
|
enable: true |
|
} |
|
}, |
|
view: { |
|
showIcon: false, |
|
expandSpeed: "", |
|
nameIsHTML: true, |
|
dblClickExpand: false |
|
}, |
|
callback: { |
|
beforeCheck: beforeCheck, |
|
onCheck: onCheck, |
|
beforeExpand: beforeExpand, |
|
onExpand: onExpand, |
|
onCollapse: onCollapse, |
|
onClick: onClick |
|
} |
|
}; |
|
|
|
function onClick(event, treeId, treeNode) { |
|
var zTree = $.fn.zTree.getZTreeObj(treeId); |
|
zTree.checkNode(treeNode, !treeNode.checked, true, true); |
|
} |
|
|
|
function beforeCheck(treeId, treeNode) { |
|
treeNode.halfCheck = false; |
|
if (treeNode.checked === true) { |
|
//将展开的节点halfCheck设为false,解决展开节点存在halfCheck=true的情况 guy |
|
//所有的半选状态都需要取消halfCheck=true的情况 |
|
function track(children) { |
|
BI.each(children, function (i, ch) { |
|
if (ch.halfCheck === true) { |
|
ch.halfCheck = false; |
|
track(ch.children); |
|
} |
|
}) |
|
} |
|
|
|
track(treeNode.children); |
|
|
|
var treeObj = $.fn.zTree.getZTreeObj(treeId); |
|
var nodes = treeObj.getSelectedNodes(); |
|
BI.each(nodes, function (index, node) { |
|
node.halfCheck = false; |
|
}) |
|
} |
|
} |
|
|
|
function beforeExpand(treeId, treeNode) { |
|
self._expandNode(treeId, treeNode); |
|
} |
|
|
|
function onCheck(event, treeId, treeNode) { |
|
self._selectTreeNode(treeId, treeNode); |
|
} |
|
|
|
function onExpand(event, treeId, treeNode) { |
|
treeNode.halfCheck = false; |
|
} |
|
|
|
function onCollapse(event, treeId, treeNode) { |
|
treeNode.halfCheck = false; |
|
} |
|
|
|
return setting; |
|
}, |
|
|
|
_selectTreeNode: function (treeId, treeNode) { |
|
var self = this, o = this.options; |
|
var parent_values = BI.deepClone(treeNode.parentValues || self._getParentValues(treeNode)); |
|
var name = this._getNodeValue(treeNode) |
|
// var values = parent_values.concat([name]); |
|
if (treeNode.checked === true) { |
|
} else { |
|
var tNode = treeNode; |
|
var pNode = this._getTree(this.selected_values, parent_values); |
|
if (BI.isNotNull(pNode[name])) { |
|
delete pNode[name]; |
|
} |
|
while (tNode != null && BI.isEmpty(pNode)) { |
|
parent_values = parent_values.slice(0, parent_values.length - 1); |
|
tNode = tNode.getParentNode(); |
|
if (tNode != null) { |
|
pNode = this._getTree(this.selected_values, parent_values); |
|
name = this._getNodeValue(tNode); |
|
delete pNode[name]; |
|
} |
|
} |
|
} |
|
BI.SyncTree.superclass._selectTreeNode.apply(self, arguments); |
|
}, |
|
|
|
//展开节点 |
|
_expandNode: function (treeId, treeNode) { |
|
var self = this, o = this.options; |
|
var parentValues = treeNode.parentValues || self._getParentValues(treeNode); |
|
var op = BI.extend({}, o.paras, { |
|
"id": treeNode.id, |
|
"times": 1, |
|
"parent_values": parentValues.concat(this._getNodeValue(treeNode)), |
|
"check_state": treeNode.getCheckStatus() |
|
}); |
|
var complete = function (d) { |
|
var nodes = d.items || []; |
|
if (nodes.length > 0) { |
|
callback(self._dealWidthNodes(nodes), !!d.hasNext); |
|
} |
|
}; |
|
var times = 1; |
|
|
|
function callback(nodes, hasNext) { |
|
self.nodes.addNodes(treeNode, nodes); |
|
|
|
if (hasNext === true) { |
|
BI.delay(function () { |
|
times++; |
|
op.times = times; |
|
o.itemsCreator(op, complete); |
|
}, 100); |
|
} |
|
} |
|
|
|
if (!treeNode.children) { |
|
o.itemsCreator(op, complete) |
|
} |
|
}, |
|
|
|
_join: function (valueA, valueB) { |
|
var self = this; |
|
var map = {}; |
|
track([], valueA, valueB); |
|
track([], valueB, valueA); |
|
function track(parent, node, compare) { |
|
BI.each(node, function (n, item) { |
|
if (BI.isNull(compare[n])) { |
|
self._addTreeNode(map, parent, n, item); |
|
} else if (BI.isEmpty(compare[n])) { |
|
self._addTreeNode(map, parent, n, {}); |
|
} else { |
|
track(parent.concat([n]), node[n], compare[n]); |
|
} |
|
}) |
|
} |
|
|
|
return map; |
|
}, |
|
|
|
hasChecked: function () { |
|
return !BI.isEmpty(this.selected_values) || BI.SyncTree.superclass.hasChecked.apply(this, arguments); |
|
}, |
|
|
|
getValue: function () { |
|
if (!this.nodes) { |
|
return {}; |
|
} |
|
var checkedValues = this._getSelectedValues(); |
|
if (BI.isEmpty(checkedValues)) { |
|
return this.selected_values; |
|
} |
|
if (BI.isEmpty(this.selected_values)) { |
|
return checkedValues; |
|
} |
|
return this._join(checkedValues, this.selected_values); |
|
}, |
|
|
|
//生成树方法 |
|
stroke: function (config) { |
|
delete this.options.keyword; |
|
BI.extend(this.options.paras, config); |
|
//取消选中时使用 |
|
this.selected_values = BI.deepClone(this.options.paras.selected_values) || {}; |
|
var setting = this._configSetting(); |
|
this._initTree(setting); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.sync_tree", BI.SyncTree);/** |
|
* guy |
|
* 局部树,两个请求树, 第一个请求构造树,第二个请求获取节点 |
|
* @class BI.PartTree |
|
* @extends BI.SyncTree |
|
*/ |
|
BI.PartTree = BI.inherit(BI.SyncTree, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.PartTree.superclass._defaultConfig.apply(this, arguments), {}) |
|
}, |
|
|
|
_init: function () { |
|
BI.PartTree.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
_loadMore: function () { |
|
var self = this, o = this.options; |
|
var op = BI.extend({}, o.paras, { |
|
type: BI.TreeView.REQ_TYPE_INIT_DATA, |
|
times: ++this.times |
|
}); |
|
this.tip.setLoading(); |
|
o.itemsCreator(op, function (d) { |
|
var hasNext = !!d.hasNext, nodes = d.items || []; |
|
o.paras.last_search_value = d.last_search_value; |
|
if (self._stop === true) { |
|
return; |
|
} |
|
if (!hasNext) { |
|
self.tip.setEnd(); |
|
} else { |
|
self.tip.setLoaded(); |
|
} |
|
if (nodes.length > 0) { |
|
self.nodes.addNodes(null, self._dealWidthNodes(nodes)); |
|
} |
|
}); |
|
}, |
|
|
|
_selectTreeNode: function (treeId, treeNode) { |
|
var self = this, o = this.options; |
|
var parent_values = BI.deepClone(treeNode.parentValues || self._getParentValues(treeNode)); |
|
var name = this._getNodeValue(treeNode) |
|
// var values = parent_values.concat([name]); |
|
if (treeNode.checked === true) { |
|
BI.SyncTree.superclass._selectTreeNode.apply(self, arguments); |
|
} else { |
|
o.itemsCreator(BI.extend({}, o.paras, { |
|
type: BI.TreeView.REQ_TYPE_CALCULATE_SELECT_DATA, |
|
selected_values: this.selected_values, |
|
not_selected_value: name, |
|
parent_values: parent_values |
|
}), function (new_values) { |
|
if (BI.isEqual(self.selected_values, new_values)) { |
|
var tNode = treeNode; |
|
var pNode = self._getTree(new_values, parent_values); |
|
if (pNode[name]) { |
|
delete pNode[name]; |
|
} |
|
while (tNode != null && BI.isEmpty(pNode)) { |
|
parent_values = parent_values.slice(0, parent_values.length - 1); |
|
tNode = tNode.getParentNode(); |
|
if (tNode != null) { |
|
pNode = self._getTree(new_values, parent_values); |
|
name = self._getNodeValue(tNode); |
|
delete pNode[name]; |
|
} |
|
} |
|
} |
|
self.selected_values = new_values; |
|
BI.SyncTree.superclass._selectTreeNode.apply(self, arguments); |
|
}); |
|
} |
|
}, |
|
|
|
_getSelectedValues: function () { |
|
var self = this; |
|
var hashMap = {}; |
|
var rootNoots = this.nodes.getNodes(); |
|
track(rootNoots); |
|
function track(nodes) { |
|
BI.each(nodes, function (i, node) { |
|
var checkState = node.getCheckStatus(); |
|
if (checkState.checked === false) { |
|
return true; |
|
} |
|
var parentValues = node.parentValues || self._getParentValues(node); |
|
//把文字中的html去掉,其实就是把文字颜色去掉 |
|
var values = parentValues.concat([self._getNodeValue(node)]); |
|
self._buildTree(hashMap, values); |
|
// if(checkState.checked === true && checkState.half === false && nodes[i].flag === true){ |
|
// continue; |
|
// } |
|
if (BI.isNotEmptyArray(node.children)) { |
|
track(node.children); |
|
return true; |
|
} |
|
if (checkState.half === true) { |
|
self._getHalfSelectedValues(hashMap, node); |
|
} |
|
}) |
|
} |
|
|
|
return hashMap; |
|
}, |
|
|
|
_initTree: function (setting, keyword) { |
|
var self = this, o = this.options; |
|
this.times = 1; |
|
var tree = this.tree; |
|
tree.empty(); |
|
self.tip.setVisible(false); |
|
this.loading(); |
|
var op = BI.extend({}, o.paras, { |
|
type: BI.TreeView.REQ_TYPE_INIT_DATA, |
|
times: this.times |
|
}); |
|
var complete = function (d) { |
|
if (self._stop === true || keyword != o.paras.keyword) { |
|
return; |
|
} |
|
var hasNext = !!d.hasNext, nodes = d.items || []; |
|
o.paras.last_search_value = d.last_search_value; |
|
if (nodes.length > 0) { |
|
callback(self._dealWidthNodes(nodes)); |
|
} |
|
self.setTipVisible(nodes.length <= 0); |
|
self.loaded(); |
|
if (!hasNext) { |
|
self.tip.invisible(); |
|
} else { |
|
self.tip.setLoaded(); |
|
} |
|
self.fireEvent(BI.Events.AFTERINIT); |
|
}; |
|
|
|
function callback(nodes) { |
|
if (self._stop === true) { |
|
return; |
|
} |
|
self.nodes = $.fn.zTree.init(tree.element, setting, nodes); |
|
} |
|
|
|
BI.delay(function () { |
|
o.itemsCreator(op, complete); |
|
}, 100); |
|
}, |
|
|
|
getValue: function () { |
|
var o = this.options; |
|
var result = BI.PartTree.superclass.getValue.apply(this, arguments); |
|
o.itemsCreator({ |
|
type: BI.TreeView.REQ_TYPE_ADJUST_DATA, |
|
selected_values: result |
|
}, function (res) { |
|
result = res; |
|
}); |
|
return result; |
|
}, |
|
|
|
//生成树方法 |
|
stroke: function (config) { |
|
var o = this.options; |
|
delete o.paras.keyword; |
|
BI.extend(o.paras, config); |
|
delete o.paras.last_search_value; |
|
//取消选中时使用 |
|
this.selected_values = BI.deepClone(o.paras.selected_values) || {}; |
|
//delete this.options.paras.selected_values; |
|
var setting = this._configSetting(); |
|
this._initTree(setting, o.paras.keyword); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.part_tree", BI.PartTree);BI.Resizers = new BI.ResizeController(); |
|
BI.Layers = new BI.LayerController(); |
|
BI.Maskers = new BI.MaskersController(); |
|
BI.Bubbles = new BI.BubblesController(); |
|
BI.Tooltips = new BI.TooltipsController(); |
|
BI.Popovers = new BI.FloatBoxController(); |
|
BI.Broadcasts = new BI.BroadcastController(); |
|
/** |
|
* canvas绘图 |
|
* |
|
* Created by GUY on 2015/11/18. |
|
* @class BI.Canvas |
|
* @extends BI.Widget |
|
*/ |
|
BI.Canvas = BI.inherit(BI.Widget, { |
|
|
|
_defaultConfig: function () { |
|
return BI.extend(BI.Canvas.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-canvas" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Canvas.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
var canvas = document.createElement("canvas"); |
|
if (!document.createElement('canvas').getContext) { |
|
canvas = window.G_vmlCanvasManager.initElement(canvas); |
|
} |
|
this.element.append(canvas); |
|
canvas.width = o.width; |
|
canvas.height = o.height; |
|
$(canvas).width("100%"); |
|
$(canvas).height("100%"); |
|
this.canvas = canvas; |
|
this._queue = []; |
|
}, |
|
|
|
_getContext: function () { |
|
if (!this.ctx) { |
|
this.ctx = this.canvas.getContext('2d'); |
|
} |
|
return this.ctx; |
|
}, |
|
|
|
_attr: function (key, value) { |
|
var self = this; |
|
if (BI.isNull(key)) { |
|
return; |
|
} |
|
if (BI.isObject(key)) { |
|
BI.each(key, function (k, v) { |
|
self._queue.push({k: k, v: v}); |
|
}); |
|
return; |
|
} |
|
this._queue.push({k: key, v: value}); |
|
}, |
|
|
|
_line: function (x0, y0) { |
|
var self = this; |
|
var args = [].slice.call(arguments, 2); |
|
if (BI.isOdd(args.length)) { |
|
this._attr(BI.last(args)); |
|
args = BI.initial(args); |
|
} |
|
this._attr("moveTo", [x0, y0]); |
|
var odd = BI.filter(args, function (i) { |
|
return i % 2 === 0; |
|
}); |
|
var even = BI.filter(args, function (i) { |
|
return i % 2 !== 0; |
|
}); |
|
args = BI.zip(odd, even); |
|
BI.each(args, function (i, point) { |
|
self._attr("lineTo", point); |
|
}); |
|
}, |
|
|
|
line: function (x0, y0, x1, y1) { |
|
this._line.apply(this, arguments); |
|
this._attr("stroke", []); |
|
}, |
|
|
|
rect: function (x, y, w, h, color) { |
|
this._attr("fillStyle", color); |
|
this._attr("fillRect", [x, y, w, h]); |
|
}, |
|
|
|
circle: function (x, y, radius, color) { |
|
this._attr({ |
|
fillStyle: color, |
|
beginPath: [], |
|
arc: [x, y, radius, 0, Math.PI * 2, true], |
|
closePath: [], |
|
fill: [] |
|
}); |
|
}, |
|
|
|
hollow: function () { |
|
this._attr("beginPath", []); |
|
this._line.apply(this, arguments); |
|
this._attr("closePath", []); |
|
this._attr("stroke", []); |
|
}, |
|
|
|
solid: function () { |
|
this.hollow.apply(this, arguments); |
|
this._attr("fill", []); |
|
}, |
|
|
|
gradient: function (x0, y0, x1, y1, start, end) { |
|
var grd = this._getContext().createLinearGradient(x0, y0, x1, y1); |
|
grd.addColorStop(0, start); |
|
grd.addColorStop(1, end); |
|
return grd; |
|
}, |
|
|
|
reset: function () { |
|
this._getContext().clearRect(0, 0, this.canvas.width, this.canvas.height); |
|
}, |
|
|
|
stroke: function (callback) { |
|
var self = this; |
|
BI.nextTick(function () { |
|
var ctx = self._getContext(); |
|
BI.each(self._queue, function (i, q) { |
|
if (BI.isFunction(ctx[q.k])) { |
|
ctx[q.k].apply(ctx, q.v); |
|
} else { |
|
ctx[q.k] = q.v; |
|
} |
|
}); |
|
self._queue = []; |
|
callback && callback(); |
|
}); |
|
} |
|
}); |
|
$.shortcut("bi.canvas", BI.Canvas);/** |
|
* Collection |
|
* |
|
* Created by GUY on 2016/1/15. |
|
* @class BI.Collection |
|
* @extends BI.Widget |
|
*/ |
|
BI.Collection = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.Collection.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-collection", |
|
width: 400, |
|
height: 300, |
|
overflowX: true, |
|
overflowY: true, |
|
cellSizeAndPositionGetter: BI.emptyFn, |
|
horizontalOverscanSize: 0, |
|
verticalOverscanSize: 0, |
|
scrollLeft: 0, |
|
scrollTop: 0, |
|
items: [] |
|
}); |
|
}, |
|
|
|
_init: function () { |
|
BI.Collection.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.renderedCells = []; |
|
this.renderedKeys = []; |
|
this._scrollLock = false; |
|
this._debounceRelease = BI.debounce(function () { |
|
self._scrollLock = false; |
|
}, 150); |
|
this.container = BI.createWidget({ |
|
type: "bi.absolute" |
|
}); |
|
this.element.scroll(function () { |
|
if (self._scrollLock === true) { |
|
return; |
|
} |
|
o.scrollLeft = self.element.scrollLeft(); |
|
o.scrollTop = self.element.scrollTop(); |
|
self._calculateChildrenToRender(); |
|
self.fireEvent(BI.Collection.EVENT_SCROLL, { |
|
scrollLeft: o.scrollLeft, |
|
scrollTop: o.scrollTop |
|
}); |
|
}); |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
element: this, |
|
scrollable: o.overflowX === true && o.overflowY === true, |
|
scrolly: o.overflowX === false && o.overflowY === true, |
|
scrollx: o.overflowX === true && o.overflowY === false, |
|
items: [this.container] |
|
}); |
|
if (o.items.length > 0) { |
|
this._calculateSizeAndPositionData(); |
|
this._populate(); |
|
} |
|
if (o.scrollLeft !== 0 || o.scrollTop !== 0) { |
|
BI.nextTick(function () { |
|
self.element.scrollTop(o.scrollTop); |
|
self.element.scrollLeft(o.scrollLeft); |
|
}); |
|
} |
|
}, |
|
|
|
_calculateSizeAndPositionData: function () { |
|
var o = this.options; |
|
var cellMetadata = []; |
|
var sectionManager = new BI.SectionManager(); |
|
var height = 0; |
|
var width = 0; |
|
|
|
for (var index = 0, len = o.items.length; index < len; index++) { |
|
var cellMetadatum = o.cellSizeAndPositionGetter(index); |
|
|
|
if ( |
|
cellMetadatum.height == null || isNaN(cellMetadatum.height) || |
|
cellMetadatum.width == null || isNaN(cellMetadatum.width) || |
|
cellMetadatum.x == null || isNaN(cellMetadatum.x) || |
|
cellMetadatum.y == null || isNaN(cellMetadatum.y) |
|
) { |
|
throw Error(); |
|
} |
|
|
|
height = Math.max(height, cellMetadatum.y + cellMetadatum.height); |
|
width = Math.max(width, cellMetadatum.x + cellMetadatum.width); |
|
|
|
cellMetadatum.index = index; |
|
cellMetadata[index] = cellMetadatum; |
|
sectionManager.registerCell(cellMetadatum, index); |
|
} |
|
|
|
this._cellMetadata = cellMetadata; |
|
this._sectionManager = sectionManager; |
|
this._height = height; |
|
this._width = width; |
|
}, |
|
|
|
_cellRenderers: function (height, width, x, y) { |
|
this._lastRenderedCellIndices = this._sectionManager.getCellIndices(height, width, x, y); |
|
return this._cellGroupRenderer() |
|
}, |
|
|
|
_cellGroupRenderer: function () { |
|
var self = this, o = this.options; |
|
var rendered = []; |
|
BI.each(this._lastRenderedCellIndices, function (i, index) { |
|
var cellMetadata = self._sectionManager.getCellMetadata(index); |
|
rendered.push(cellMetadata); |
|
}); |
|
return rendered; |
|
}, |
|
|
|
_calculateChildrenToRender: function () { |
|
var self = this, o = this.options; |
|
var scrollLeft = BI.clamp(o.scrollLeft, 0, this._getMaxScrollLeft()); |
|
var scrollTop = BI.clamp(o.scrollTop, 0, this._getMaxScrollTop()); |
|
var left = Math.max(0, scrollLeft - o.horizontalOverscanSize); |
|
var top = Math.max(0, scrollTop - o.verticalOverscanSize); |
|
var right = Math.min(this._width, scrollLeft + o.width + o.horizontalOverscanSize); |
|
var bottom = Math.min(this._height, scrollTop + o.height + o.verticalOverscanSize); |
|
var childrenToDisplay = this._cellRenderers(bottom - top, right - left, left, top); |
|
var renderedCells = [], renderedKeys = []; |
|
for (var i = 0, len = childrenToDisplay.length; i < len; i++) { |
|
var datum = childrenToDisplay[i]; |
|
var index = BI.deepIndexOf(this.renderedKeys, datum.index); |
|
if (index > -1) { |
|
if (datum.width !== this.renderedCells[index]._width) { |
|
this.renderedCells[index]._width = datum.width; |
|
this.renderedCells[index].el.setWidth(datum.width); |
|
} |
|
if (datum.height !== this.renderedCells[index]._height) { |
|
this.renderedCells[index]._height = datum.height; |
|
this.renderedCells[index].el.setHeight(datum.height); |
|
} |
|
if (this.renderedCells[index].left !== datum.x) { |
|
this.renderedCells[index].el.element.css("left", datum.x + "px"); |
|
} |
|
if (this.renderedCells[index].top !== datum.y) { |
|
this.renderedCells[index].el.element.css("top", datum.y + "px"); |
|
} |
|
renderedCells.push(this.renderedCells[index]); |
|
} else { |
|
var child = BI.createWidget(BI.extend({ |
|
type: "bi.label", |
|
width: datum.width, |
|
height: datum.height |
|
}, o.items[datum.index], { |
|
cls: (o.items[datum.index].cls || "") + " container-cell" + (datum.y === 0 ? " first-row" : "") + (datum.x === 0 ? " first-col" : ""), |
|
_left: datum.x, |
|
_top: datum.y |
|
})); |
|
renderedCells.push({ |
|
el: child, |
|
left: datum.x, |
|
top: datum.y, |
|
_width: datum.width, |
|
_height: datum.height |
|
}); |
|
} |
|
renderedKeys.push(datum.index); |
|
} |
|
//已存在的, 需要添加的和需要删除的 |
|
var existSet = {}, addSet = {}, deleteArray = []; |
|
BI.each(renderedKeys, function (i, key) { |
|
if (BI.deepContains(self.renderedKeys, key)) { |
|
existSet[i] = key; |
|
} else { |
|
addSet[i] = key; |
|
} |
|
}); |
|
BI.each(this.renderedKeys, function (i, key) { |
|
if (BI.deepContains(existSet, key)) { |
|
return; |
|
} |
|
if (BI.deepContains(addSet, key)) { |
|
return; |
|
} |
|
deleteArray.push(i); |
|
}); |
|
BI.each(deleteArray, function (i, index) { |
|
self.renderedCells[index].el.destroy(); |
|
}); |
|
var addedItems = []; |
|
BI.each(addSet, function (index) { |
|
addedItems.push(renderedCells[index]) |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this.container, |
|
items: addedItems |
|
}); |
|
this.renderedCells = renderedCells; |
|
this.renderedKeys = renderedKeys; |
|
}, |
|
|
|
_getMaxScrollLeft: function () { |
|
return Math.max(0, this._width - this.options.width + (this.options.overflowX ? BI.DOM.getScrollWidth() : 0)); |
|
}, |
|
|
|
_getMaxScrollTop: function () { |
|
return Math.max(0, this._height - this.options.height + (this.options.overflowY ? BI.DOM.getScrollWidth() : 0)); |
|
}, |
|
|
|
_populate: function () { |
|
var o = this.options; |
|
if (o.items.length > 0) { |
|
this.container.setWidth(this._width); |
|
this.container.setHeight(this._height); |
|
|
|
this._calculateChildrenToRender(); |
|
this.element.scrollTop(o.scrollTop); |
|
this.element.scrollLeft(o.scrollLeft); |
|
} |
|
}, |
|
|
|
setScrollLeft: function (scrollLeft) { |
|
if (this.options.scrollLeft === scrollLeft) { |
|
return; |
|
} |
|
this._scrollLock = true; |
|
this.options.scrollLeft = BI.clamp(scrollLeft || 0, 0, this._getMaxScrollLeft()); |
|
this._debounceRelease(); |
|
this._calculateChildrenToRender(); |
|
this.element.scrollLeft(this.options.scrollLeft); |
|
}, |
|
|
|
setScrollTop: function (scrollTop) { |
|
if (this.options.scrollTop === scrollTop) { |
|
return; |
|
} |
|
this._scrollLock = true; |
|
this.options.scrollTop = BI.clamp(scrollTop || 0, 0, this._getMaxScrollTop()); |
|
this._debounceRelease(); |
|
this._calculateChildrenToRender(); |
|
this.element.scrollTop(this.options.scrollTop); |
|
}, |
|
|
|
setOverflowX: function (b) { |
|
var self = this; |
|
if (this.options.overflowX !== !!b) { |
|
this.options.overflowX = !!b; |
|
BI.nextTick(function () { |
|
self.element.css({overflowX: !!b ? "auto" : "hidden"}); |
|
}); |
|
} |
|
}, |
|
|
|
setOverflowY: function (b) { |
|
var self = this; |
|
if (this.options.overflowY !== !!b) { |
|
this.options.overflowY = !!b; |
|
BI.nextTick(function () { |
|
self.element.css({overflowY: !!b ? "auto" : "hidden"}); |
|
}); |
|
} |
|
}, |
|
|
|
getScrollLeft: function () { |
|
return this.options.scrollLeft; |
|
}, |
|
|
|
getScrollTop: function () { |
|
return this.options.scrollTop; |
|
}, |
|
|
|
getMaxScrollLeft: function () { |
|
return this._getMaxScrollLeft(); |
|
}, |
|
|
|
getMaxScrollTop: function () { |
|
return this._getMaxScrollTop(); |
|
}, |
|
|
|
restore: function () { |
|
BI.each(this.renderedCells, function (i, cell) { |
|
cell.el.destroy(); |
|
}); |
|
this.renderedCells = []; |
|
this.renderedKeys = []; |
|
this._scrollLock = false; |
|
}, |
|
|
|
populate: function (items) { |
|
if (items && items !== this.options.items) { |
|
this.options.items = items; |
|
this._calculateSizeAndPositionData(); |
|
} |
|
this._populate(); |
|
} |
|
}); |
|
BI.Collection.EVENT_SCROLL = "EVENT_SCROLL"; |
|
$.shortcut('bi.collection_view', BI.Collection);/** |
|
* @class BI.Combo |
|
* @extends BI.Widget |
|
*/ |
|
BI.Combo = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
var conf = BI.Combo.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-combo", |
|
trigger: "click", |
|
toggle: true, |
|
direction: "bottom", //top||bottom||left||right||top,left||top,right||bottom,left||bottom,right |
|
isDefaultInit: false, |
|
isNeedAdjustHeight: true,//是否需要高度调整 |
|
isNeedAdjustWidth: true, |
|
stopEvent: false, |
|
stopPropagation: false, |
|
adjustLength: 0,//调整的距离 |
|
adjustXOffset: 0, |
|
adjustYOffset: 0, |
|
hideChecker: BI.emptyFn, |
|
offsetStyle: "left", //left,right,center |
|
el: {}, |
|
popup: {}, |
|
comboClass: "bi-combo-popup", |
|
hoverClass: "bi-combo-hover" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Combo.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this._initCombo(); |
|
this._initPullDownAction(); |
|
this.combo.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
if (self.isEnabled() && this.isEnabled()) { |
|
if (type === BI.Events.EXPAND) { |
|
self._popupView(); |
|
} |
|
if (type === BI.Events.COLLAPSE) { |
|
self._hideView(); |
|
} |
|
if (type === BI.Events.EXPAND || type === BI.Events.COLLAPSE) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
} |
|
|
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.Combo.EVENT_TRIGGER_CHANGE, obj); |
|
} |
|
} |
|
}); |
|
|
|
self.element.on("mouseenter." + self.getName(), function (e) { |
|
if (self.isEnabled() && self.combo.isEnabled()) { |
|
self.element.addClass(o.hoverClass); |
|
} |
|
}); |
|
self.element.on("mouseleave." + self.getName(), function (e) { |
|
if (self.isEnabled() && self.combo.isEnabled()) { |
|
self.element.removeClass(o.hoverClass); |
|
} |
|
}); |
|
|
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
scrolly: false, |
|
element: this, |
|
items: [ |
|
{el: this.combo} |
|
] |
|
}); |
|
o.isDefaultInit && (this._assertPopupView()); |
|
BI.Resizers.add(this.getName(), BI.bind(function () { |
|
if (this.isViewVisible()) { |
|
this._hideView(); |
|
} |
|
}, this)); |
|
}, |
|
|
|
_toggle: function () { |
|
this._assertPopupViewRender(); |
|
if (this.popupView.isVisible()) { |
|
this._hideView(); |
|
} else { |
|
if (this.isEnabled()) { |
|
this._popupView(); |
|
} |
|
} |
|
}, |
|
|
|
_initPullDownAction: function () { |
|
var self = this, o = this.options; |
|
var evs = this.options.trigger.split(","); |
|
var st = function (e) { |
|
if (o.stopEvent) { |
|
e.stopEvent(); |
|
} |
|
if (o.stopPropagation) { |
|
e.stopPropagation(); |
|
} |
|
}; |
|
BI.each(evs, function (i, ev) { |
|
switch (ev) { |
|
case "hover": |
|
self.element.on("mouseenter." + self.getName(), function (e) { |
|
if (self.isEnabled() && self.combo.isEnabled()) { |
|
self._popupView(); |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EXPAND, "", self.combo); |
|
self.fireEvent(BI.Combo.EVENT_EXPAND); |
|
} |
|
}); |
|
self.element.on("mouseleave." + self.getName(), function (e) { |
|
if (self.isEnabled() && self.combo.isEnabled() && o.toggle === true) { |
|
self._hideView(); |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.COLLAPSE, "", self.combo); |
|
self.fireEvent(BI.Combo.EVENT_COLLAPSE); |
|
} |
|
}); |
|
break; |
|
case "click": |
|
var debounce = BI.debounce(function (e) { |
|
if (self.combo.element.__isMouseInBounds__(e)) { |
|
if (self.isEnabled() && self.combo.isEnabled()) { |
|
o.toggle ? self._toggle() : self._popupView(); |
|
if (self.isViewVisible()) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EXPAND, "", self.combo); |
|
self.fireEvent(BI.Combo.EVENT_EXPAND); |
|
} else { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.COLLAPSE, "", self.combo); |
|
self.fireEvent(BI.Combo.EVENT_COLLAPSE); |
|
} |
|
} |
|
} |
|
}, BI.EVENT_RESPONSE_TIME, true); |
|
self.element.off(ev + "." + self.getName()).on(ev + "." + self.getName(), function(e){ |
|
debounce(e); |
|
st(e); |
|
}); |
|
break; |
|
} |
|
}); |
|
}, |
|
|
|
_initCombo: function () { |
|
this.combo = BI.createWidget(this.options.el); |
|
}, |
|
|
|
_assertPopupView: function () { |
|
var self = this; |
|
if (this.popupView == null) { |
|
this.popupView = BI.createWidget(this.options.popup, { |
|
type: "bi.popup_view" |
|
}); |
|
this.popupView.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
if (type === BI.Events.CLICK) { |
|
self.combo.setValue(self.getValue()); |
|
self.fireEvent(BI.Combo.EVENT_CHANGE, value, obj); |
|
} |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
}); |
|
this.popupView.setVisible(false); |
|
BI.nextTick(function () { |
|
self.fireEvent(BI.Combo.EVENT_AFTER_INIT); |
|
}); |
|
} |
|
}, |
|
|
|
_assertPopupViewRender: function () { |
|
this._assertPopupView(); |
|
if (!this._rendered) { |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
scrolly: false, |
|
element: this, |
|
items: [ |
|
{el: this.popupView} |
|
] |
|
}); |
|
this._rendered = true; |
|
} |
|
}, |
|
|
|
_hideIf: function (e) { |
|
if (this.element.__isMouseInBounds__(e) || (this.popupView && this.popupView.element.__isMouseInBounds__(e))) { |
|
return; |
|
} |
|
if (this.element.find(e.target).length > 0) { |
|
return; |
|
} |
|
var isHide = this.options.hideChecker.apply(this, [e]); |
|
if (isHide === false) { |
|
return; |
|
} |
|
this._hideView(); |
|
}, |
|
|
|
_hideView: function () { |
|
this.fireEvent(BI.Combo.EVENT_BEFORE_HIDEVIEW); |
|
this.popupView && this.popupView.invisible(); |
|
this.element.removeClass(this.options.comboClass); |
|
|
|
$(document).unbind("mousedown." + this.getName()).unbind("mousewheel." + this.getName()); |
|
this.fireEvent(BI.Combo.EVENT_AFTER_HIDEVIEW); |
|
}, |
|
|
|
_popupView: function () { |
|
this._assertPopupViewRender(); |
|
this.fireEvent(BI.Combo.EVENT_BEFORE_POPUPVIEW); |
|
|
|
this.popupView.visible(); |
|
this.adjustWidth(); |
|
this.adjustHeight(); |
|
|
|
this.element.addClass(this.options.comboClass); |
|
$(document).bind("mousedown." + this.getName(), BI.bind(this._hideIf, this)).bind("mousewheel." + this.getName(), BI.bind(this._hideIf, this)); |
|
this.fireEvent(BI.Combo.EVENT_AFTER_POPUPVIEW); |
|
}, |
|
|
|
adjustWidth: function () { |
|
var o = this.options; |
|
if (!this.popupView) { |
|
return; |
|
} |
|
if (o.isNeedAdjustWidth === true) { |
|
this.resetListWidth(""); |
|
var width = this.popupView.element.outerWidth(); |
|
var maxW = this.element.outerWidth() || o.width; |
|
if (width > maxW + 80) { |
|
maxW = maxW + 80; |
|
} else if (width > maxW) { |
|
maxW = width; |
|
} |
|
this.resetListWidth(maxW < 100 ? 100 : maxW); |
|
} |
|
}, |
|
|
|
adjustHeight: function () { |
|
var o = this.options, p = {}; |
|
if (!this.popupView) { |
|
return; |
|
} |
|
var isVisible = this.popupView.isVisible(); |
|
this.popupView.visible(); |
|
switch (o.direction) { |
|
case "bottom": |
|
case "bottom,right": |
|
p = $.getComboPosition(this.combo, this.popupView, o.adjustXOffset, o.adjustYOffset || o.adjustLength, o.isNeedAdjustHeight, ['bottom', 'top', 'right', 'left'], o.offsetStyle); |
|
break; |
|
case "top": |
|
case "top,right": |
|
p = $.getComboPosition(this.combo, this.popupView, o.adjustYOffset || o.adjustLength, o.isNeedAdjustHeight, ['top', 'bottom', 'right', 'left'], o.offsetStyle); |
|
break; |
|
case "left": |
|
case "left,bottom": |
|
p = $.getComboPosition(this.combo, this.popupView, o.adjustXOffset || o.adjustLength, o.adjustYOffset, o.isNeedAdjustHeight, ['left', 'right', 'bottom', 'top'], o.offsetStyle); |
|
break; |
|
case "right": |
|
case "right,bottom": |
|
p = $.getComboPosition(this.combo, this.popupView, o.adjustXOffset || o.adjustLength, o.adjustYOffset, o.isNeedAdjustHeight, ['right', 'left', 'bottom', 'top'], o.offsetStyle); |
|
break; |
|
case "top,left": |
|
p = $.getComboPosition(this.combo, this.popupView, o.adjustXOffset, o.adjustYOffset || o.adjustLength, o.isNeedAdjustHeight, ['top', 'bottom', 'left', 'right'], o.offsetStyle); |
|
break; |
|
case "bottom,left": |
|
p = $.getComboPosition(this.combo, this.popupView, o.adjustXOffset, o.adjustYOffset || o.adjustLength, o.isNeedAdjustHeight, ['bottom', 'top', 'left', 'right'], o.offsetStyle); |
|
break; |
|
case "left,top": |
|
p = $.getComboPosition(this.combo, this.popupView, o.adjustXOffset || o.adjustLength, o.adjustYOffset, o.isNeedAdjustHeight, ['left', 'right', 'top', 'bottom'], o.offsetStyle); |
|
break; |
|
case "right,top": |
|
p = $.getComboPosition(this.combo, this.popupView, o.adjustXOffset || o.adjustLength, o.adjustYOffset, o.isNeedAdjustHeight, ['right', 'left', 'top', 'bottom'], o.offsetStyle); |
|
break; |
|
case "top,custom": |
|
case "custom,top": |
|
p = $.getTopAdaptPosition(this.combo, this.popupView, o.adjustYOffset || o.adjustLength, o.isNeedAdjustHeight); |
|
break; |
|
case "custom,bottom": |
|
case "bottom,custom": |
|
p = $.getBottomAdaptPosition(this.combo, this.popupView, o.adjustYOffset || o.adjustLength, o.isNeedAdjustHeight); |
|
break; |
|
case "left,custom": |
|
case "custom,left": |
|
p = $.getLeftAdaptPosition(this.combo, this.popupView, o.adjustXOffset || o.adjustLength); |
|
delete p.top; |
|
delete p.adaptHeight; |
|
break; |
|
case "custom,right": |
|
case "right,custom": |
|
p = $.getRightAdaptPosition(this.combo, this.popupView, o.adjustXOffset || o.adjustLength); |
|
delete p.top; |
|
delete p.adaptHeight; |
|
break; |
|
} |
|
|
|
if ("adaptHeight" in p) { |
|
this.resetListHeight(p['adaptHeight']); |
|
} |
|
if ("left" in p) { |
|
this.popupView.element.css({ |
|
left: p.left |
|
}); |
|
} |
|
if ("top" in p) { |
|
this.popupView.element.css({ |
|
top: p.top |
|
}); |
|
} |
|
this.position = p; |
|
this.popupView.setVisible(isVisible); |
|
}, |
|
|
|
resetListHeight: function (h) { |
|
this._assertPopupView(); |
|
this.popupView.resetHeight && this.popupView.resetHeight(h); |
|
}, |
|
|
|
resetListWidth: function (w) { |
|
this._assertPopupView(); |
|
this.popupView.resetWidth && this.popupView.resetWidth(w); |
|
}, |
|
|
|
populate: function (items) { |
|
this._assertPopupView(); |
|
this.popupView.populate.apply(this.popupView, arguments); |
|
this.combo.populate.apply(this.combo, arguments); |
|
}, |
|
|
|
setEnable: function (arg) { |
|
BI.Combo.superclass.setEnable.apply(this, arguments); |
|
this.combo && this.combo.setEnable(arg); |
|
this.popupView && this.popupView.setEnable(arg); |
|
!arg && this.isViewVisible() && this._hideView(); |
|
}, |
|
|
|
setValue: function (v) { |
|
this._assertPopupView(); |
|
this.combo.setValue(v); |
|
this.popupView && this.popupView.setValue(v); |
|
}, |
|
|
|
getValue: function () { |
|
this._assertPopupView(); |
|
return this.popupView && this.popupView.getValue(); |
|
}, |
|
|
|
isViewVisible: function () { |
|
return this.isEnabled() && this.combo.isEnabled() && !!this.popupView && this.popupView.isVisible(); |
|
}, |
|
|
|
showView: function () { |
|
if (this.isEnabled() && this.combo.isEnabled()) { |
|
this._popupView(); |
|
} |
|
}, |
|
|
|
hideView: function () { |
|
this._hideView(); |
|
}, |
|
|
|
getView: function () { |
|
return this.popupView; |
|
}, |
|
|
|
getPopupPosition: function () { |
|
return this.position; |
|
}, |
|
|
|
doBehavior: function () { |
|
this._assertPopupView(); |
|
this.popupView && this.popupView.doBehavior.apply(this.popupView, arguments); |
|
}, |
|
|
|
toggle: function () { |
|
this._toggle(); |
|
}, |
|
|
|
destroy: function () { |
|
$(document).unbind("mousedown." + this.getName()) |
|
.unbind("mousewheel." + this.getName()) |
|
.unbind("mouseenter." + this.getName()) |
|
.unbind("mousemove." + this.getName()) |
|
.unbind("mouseleave." + this.getName()); |
|
BI.Resizers.remove(this.getName()); |
|
BI.Combo.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.Combo.EVENT_TRIGGER_CHANGE = "EVENT_TRIGGER_CHANGE"; |
|
BI.Combo.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.Combo.EVENT_EXPAND = "EVENT_EXPAND"; |
|
BI.Combo.EVENT_COLLAPSE = "EVENT_COLLAPSE"; |
|
BI.Combo.EVENT_AFTER_INIT = "EVENT_AFTER_INIT"; |
|
|
|
|
|
BI.Combo.EVENT_BEFORE_POPUPVIEW = "EVENT_BEFORE_POPUPVIEW"; |
|
BI.Combo.EVENT_AFTER_POPUPVIEW = "EVENT_AFTER_POPUPVIEW"; |
|
BI.Combo.EVENT_BEFORE_HIDEVIEW = "EVENT_BEFORE_HIDEVIEW"; |
|
BI.Combo.EVENT_AFTER_HIDEVIEW = "EVENT_AFTER_HIDEVIEW"; |
|
|
|
$.shortcut("bi.combo", BI.Combo);/** |
|
* |
|
* 某个可以展开的节点 |
|
* |
|
* Created by GUY on 2015/9/10. |
|
* @class BI.Expander |
|
* @extends BI.Widget |
|
*/ |
|
BI.Expander = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.Expander.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-expander", |
|
trigger: "click", |
|
toggle: true, |
|
//direction: "bottom", //top,bottom四个方向 |
|
isDefaultInit: false, //是否默认初始化子节点 |
|
el: {}, |
|
popup: {}, |
|
expanderClass: "bi-expander-popup", |
|
hoverClass: "bi-expander-hover" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Expander.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this._expanded = !!o.el.open; |
|
this._initExpander(); |
|
this._initPullDownAction(); |
|
this.expander.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
if (type === BI.Events.EXPAND) { |
|
self._popupView(); |
|
} |
|
if (type === BI.Events.COLLAPSE) { |
|
self._hideView(); |
|
} |
|
if (self.isEnabled() && this.isEnabled()) { |
|
if (type === BI.Events.EXPAND) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
self.fireEvent(BI.Expander.EVENT_EXPAND); |
|
} |
|
if(type === BI.Events.COLLAPSE) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
self.fireEvent(BI.Expander.EVENT_COLLAPSE); |
|
} |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.Expander.EVENT_TRIGGER_CHANGE, value, obj); |
|
} |
|
} |
|
}); |
|
|
|
this.element.hover(function () { |
|
if (self.isEnabled() && self.expander.isEnabled()) { |
|
self.element.addClass(o.hoverClass); |
|
} |
|
}, function () { |
|
if (self.isEnabled() && self.expander.isEnabled()) { |
|
self.element.removeClass(o.hoverClass); |
|
} |
|
}); |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
scrolly: false, |
|
element: this, |
|
items: [ |
|
{el: this.expander} |
|
] |
|
}); |
|
o.isDefaultInit && this._assertPopupView(); |
|
if (this.expander.isOpened() === true) { |
|
this._popupView(); |
|
} |
|
}, |
|
|
|
_toggle: function () { |
|
this._assertPopupViewRender(); |
|
if (this.popupView.isVisible()) { |
|
this._hideView(); |
|
} else { |
|
if (this.isEnabled()) { |
|
this._popupView(); |
|
} |
|
} |
|
}, |
|
|
|
_initPullDownAction: function () { |
|
var self = this, o = this.options; |
|
var evs = this.options.trigger.split(","); |
|
BI.each(evs, function (i, e) { |
|
switch (e) { |
|
case "hover": |
|
self.element[e](function (e) { |
|
if (self.isEnabled() && self.expander.isEnabled()) { |
|
self._popupView(); |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EXPAND, '', self.expander); |
|
self.fireEvent(BI.Expander.EVENT_EXPAND); |
|
} |
|
}, function () { |
|
if (self.isEnabled() && self.expander.isEnabled() && o.toggle) { |
|
self._hideView(); |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.COLLAPSE, '', self.expander); |
|
self.fireEvent(BI.Expander.EVENT_COLLAPSE); |
|
} |
|
}); |
|
break; |
|
default : |
|
if (e) { |
|
self.element.off(e + "." + self.getName()).on(e + "." + self.getName(), BI.debounce(function (e) { |
|
if (self.expander.element.__isMouseInBounds__(e)) { |
|
if (self.isEnabled() && self.expander.isEnabled()) { |
|
o.toggle ? self._toggle() : self._popupView(); |
|
if (self.isExpanded()) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EXPAND, "", self.expander); |
|
self.fireEvent(BI.Expander.EVENT_EXPAND); |
|
} else { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.COLLAPSE, '', self.expander); |
|
self.fireEvent(BI.Expander.EVENT_COLLAPSE); |
|
} |
|
} |
|
} |
|
}, BI.EVENT_RESPONSE_TIME, true)); |
|
} |
|
break; |
|
} |
|
}) |
|
}, |
|
|
|
_initExpander: function () { |
|
this.expander = BI.createWidget(this.options.el); |
|
}, |
|
|
|
_assertPopupView: function () { |
|
var self = this; |
|
if (this.popupView == null) { |
|
this.popupView = BI.createWidget(this.options.popup, { |
|
type: "bi.button_group", |
|
cls: "expander-popup", |
|
layouts: [{ |
|
type: "bi.vertical", |
|
hgap: 0, |
|
vgap: 0 |
|
}] |
|
}); |
|
this.popupView.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
//self.setValue(self.getValue()); |
|
self.fireEvent(BI.Expander.EVENT_CHANGE, value, obj); |
|
} |
|
}); |
|
this.popupView.setVisible(this.isExpanded()); |
|
BI.nextTick(function () { |
|
self.fireEvent(BI.Expander.EVENT_AFTER_INIT); |
|
}); |
|
} |
|
}, |
|
|
|
_assertPopupViewRender: function () { |
|
this._assertPopupView(); |
|
if (!this._rendered) { |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
scrolly: false, |
|
element: this, |
|
items: [ |
|
{el: this.popupView} |
|
] |
|
}); |
|
this._rendered = true; |
|
} |
|
}, |
|
|
|
_hideView: function () { |
|
this.fireEvent(BI.Expander.EVENT_BEFORE_HIDEVIEW); |
|
this._expanded = false; |
|
this.expander.setOpened(false); |
|
this.popupView && this.popupView.invisible(); |
|
this.element.removeClass(this.options.expanderClass); |
|
|
|
this.fireEvent(BI.Expander.EVENT_AFTER_HIDEVIEW); |
|
}, |
|
|
|
_popupView: function () { |
|
this._assertPopupViewRender(); |
|
this.fireEvent(BI.Expander.EVENT_BEFORE_POPUPVIEW); |
|
this._expanded = true; |
|
this.expander.setOpened(true); |
|
this.popupView.visible(); |
|
this.element.addClass(this.options.expanderClass); |
|
this.fireEvent(BI.Expander.EVENT_AFTER_POPUPVIEW); |
|
}, |
|
|
|
populate: function (items) { |
|
//this._assertPopupView(); |
|
this.popupView && this.popupView.populate.apply(this.popupView, arguments); |
|
this.expander.populate.apply(this.expander, arguments); |
|
}, |
|
|
|
setEnable: function (arg) { |
|
BI.Expander.superclass.setEnable.apply(this, arguments); |
|
this.expander && this.expander.setEnable(arg); |
|
this.popupView && this.popupView.setEnable(arg); |
|
!arg && this._hideView(); |
|
}, |
|
|
|
setValue: function (v) { |
|
//this._assertPopupView(); |
|
this.expander.setValue(v); |
|
this.popupView && this.popupView.setValue(v); |
|
}, |
|
|
|
getValue: function () { |
|
//this._assertPopupView(); |
|
return this.popupView ? this.popupView.getValue() : []; |
|
}, |
|
|
|
isViewVisible: function () { |
|
return this.isEnabled() && this.expander.isEnabled() && !!this.popupView && this.popupView.isVisible(); |
|
}, |
|
|
|
isExpanded: function () { |
|
return this._expanded; |
|
}, |
|
|
|
showView: function () { |
|
if (this.isEnabled() && this.expander.isEnabled()) { |
|
this._popupView(); |
|
} |
|
}, |
|
|
|
hideView: function () { |
|
this._hideView(); |
|
}, |
|
|
|
getView: function () { |
|
return this.popupView; |
|
}, |
|
|
|
doBehavior: function () { |
|
//this._assertPopupView(); |
|
this.popupView && this.popupView.doBehavior.apply(this.popupView, arguments); |
|
}, |
|
|
|
getAllLeaves: function () { |
|
return this.popupView && this.popupView.getAllLeaves(); |
|
}, |
|
|
|
getNodeById: function (id) { |
|
if (this.expander.options.id === id) { |
|
return this.expander; |
|
} |
|
return this.popupView && this.popupView.getNodeById(id); |
|
}, |
|
|
|
getNodeByValue: function (value) { |
|
if (this.expander.getValue() === value) { |
|
return this.expander; |
|
} |
|
return this.popupView && this.popupView.getNodeByValue(value); |
|
}, |
|
|
|
destroy: function () { |
|
BI.Expander.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.Expander.EVENT_EXPAND = "EVENT_EXPAND"; |
|
BI.Expander.EVENT_COLLAPSE = "EVENT_COLLAPSE"; |
|
BI.Expander.EVENT_TRIGGER_CHANGE = "EVENT_TRIGGER_CHANGE"; |
|
BI.Expander.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.Expander.EVENT_AFTER_INIT = "EVENT_AFTER_INIT"; |
|
|
|
|
|
BI.Expander.EVENT_BEFORE_POPUPVIEW = "EVENT_BEFORE_POPUPVIEW"; |
|
BI.Expander.EVENT_AFTER_POPUPVIEW = "EVENT_AFTER_POPUPVIEW"; |
|
BI.Expander.EVENT_BEFORE_HIDEVIEW = "EVENT_BEFORE_HIDEVIEW"; |
|
BI.Expander.EVENT_AFTER_HIDEVIEW = "EVENT_AFTER_HIDEVIEW"; |
|
|
|
$.shortcut("bi.expander", BI.Expander);/** |
|
* Created by GUY on 2015/8/10. |
|
*/ |
|
|
|
BI.ComboGroup = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.ComboGroup.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-combo-group", |
|
|
|
//以下这些属性对每一个combo都是公用的 |
|
trigger: "click,hover", |
|
direction: "right", |
|
adjustLength: 0, |
|
isDefaultInit: false, |
|
isNeedAdjustHeight: false, |
|
isNeedAdjustWidth: false, |
|
|
|
el: {type: "bi.text_button", text: "", value: ""}, |
|
children: [], |
|
|
|
popup: { |
|
el: { |
|
type: "bi.button_tree", |
|
chooseType: 0, |
|
layouts: [{ |
|
type: "bi.vertical" |
|
}] |
|
} |
|
} |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.ComboGroup.superclass._init.apply(this, arguments); |
|
this.populate(this.options.el); |
|
}, |
|
|
|
populate: function (item) { |
|
var self = this, o = this.options; |
|
var children = o.children; |
|
if (BI.isEmpty(children)) { |
|
throw new Error("ComboGroup构造错误"); |
|
} |
|
BI.each(children, function (i, ch) { |
|
var son = BI.formatEL(ch).el.children; |
|
ch = BI.formatEL(ch).el; |
|
if (!BI.isEmpty(son)) { |
|
ch.el = BI.clone(ch); |
|
ch.children = son; |
|
ch.type = "bi.combo_group"; |
|
ch.action = o.action; |
|
ch.height = o.height; |
|
ch.direction = o.direction; |
|
ch.isDefaultInit = o.isDefaultInit; |
|
ch.isNeedAdjustHeight = o.isNeedAdjustHeight; |
|
ch.isNeedAdjustWidth = o.isNeedAdjustWidth; |
|
ch.adjustLength = o.adjustLength; |
|
ch.popup = o.popup; |
|
} |
|
}) |
|
this.combo = BI.createWidget({ |
|
type: "bi.combo", |
|
element: this, |
|
height: o.height, |
|
trigger: o.trigger, |
|
direction: o.direction, |
|
isDefaultInit: o.isDefaultInit, |
|
isNeedAdjustWidth: o.isNeedAdjustWidth, |
|
isNeedAdjustHeight: o.isNeedAdjustHeight, |
|
adjustLength: o.adjustLength, |
|
el: item, |
|
popup: BI.extend({}, o.popup, { |
|
el: BI.extend({ |
|
items: children |
|
}, o.popup.el) |
|
}) |
|
}) |
|
this.combo.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.ComboGroup.EVENT_CHANGE, obj); |
|
} |
|
}) |
|
}, |
|
|
|
setEnable: function (b) { |
|
BI.ComboGroup.superclass.setEnable.apply(this, arguments); |
|
this.combo && this.combo.setEnable(b); |
|
}, |
|
|
|
getValue: function () { |
|
return this.combo.getValue(); |
|
}, |
|
|
|
setValue: function (v) { |
|
this.combo.setValue(v); |
|
} |
|
}); |
|
BI.ComboGroup.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.combo_group", BI.ComboGroup);/** |
|
* 加载控件 |
|
* |
|
* Created by GUY on 2015/8/31. |
|
* @class BI.Loader |
|
* @extends BI.Widget |
|
*/ |
|
BI.Loader = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.Loader.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-loader", |
|
|
|
direction: "top", |
|
isDefaultInit: true,//是否默认初始化数据 |
|
logic: { |
|
dynamic: true, |
|
scrolly: true |
|
}, |
|
|
|
//下面是button_group的属性 |
|
el: { |
|
type: "bi.button_group" |
|
}, |
|
|
|
items: [], |
|
itemsCreator: BI.emptyFn, |
|
onLoaded: BI.emptyFn, |
|
|
|
//下面是分页信息 |
|
count: false, |
|
prev: false, |
|
next: {}, |
|
hasPrev: BI.emptyFn, |
|
hasNext: BI.emptyFn |
|
}) |
|
}, |
|
|
|
_prevLoad: function () { |
|
var self = this, o = this.options; |
|
this.prev.setLoading(); |
|
o.itemsCreator.apply(this, [{times: --this.times}, function () { |
|
self.prev.setLoaded(); |
|
self.prependItems.apply(self, arguments); |
|
}]); |
|
}, |
|
|
|
_nextLoad: function () { |
|
var self = this, o = this.options; |
|
this.next.setLoading(); |
|
o.itemsCreator.apply(this, [{times: ++this.times}, function () { |
|
self.next.setLoaded(); |
|
self.addItems.apply(self, arguments); |
|
}]); |
|
}, |
|
|
|
_init: function () { |
|
BI.Loader.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
if (o.itemsCreator === false) { |
|
o.prev = false; |
|
o.next = false; |
|
} |
|
if (o.prev !== false) { |
|
this.prev = BI.createWidget(BI.extend({ |
|
type: "bi.loading_bar" |
|
}, o.prev)); |
|
this.prev.on(BI.Controller.EVENT_CHANGE, function (type) { |
|
if (type === BI.Events.CLICK) { |
|
self._prevLoad(); |
|
} |
|
}); |
|
} |
|
|
|
this.button_group = BI.createWidget(o.el, { |
|
type: "bi.button_group", |
|
chooseType: 0, |
|
items: o.items, |
|
behaviors: {}, |
|
layouts: [{ |
|
type: "bi.vertical" |
|
}] |
|
}); |
|
this.button_group.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.Loader.EVENT_CHANGE, obj); |
|
} |
|
}); |
|
|
|
if (o.next !== false) { |
|
this.next = BI.createWidget(BI.extend({ |
|
type: "bi.loading_bar" |
|
}, o.next)); |
|
this.next.on(BI.Controller.EVENT_CHANGE, function (type) { |
|
if (type === BI.Events.CLICK) { |
|
self._nextLoad(); |
|
} |
|
}) |
|
} |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic(BI.LogicFactory.createLogicTypeByDirection(o.direction), BI.extend({ |
|
scrolly: true |
|
}, o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection(o.direction, this.prev, this.button_group, this.next) |
|
})))); |
|
|
|
o.isDefaultInit && BI.isEmpty(o.items) && BI.nextTick(BI.bind(function () { |
|
o.isDefaultInit && BI.isEmpty(o.items) && this.populate(); |
|
}, this)); |
|
if (BI.isNotEmptyArray(o.items)) { |
|
this.populate(o.items); |
|
} |
|
}, |
|
|
|
hasPrev: function () { |
|
var o = this.options; |
|
if (BI.isNumber(o.count)) { |
|
return this.count < o.count; |
|
} |
|
return !!o.hasPrev.apply(this, [{ |
|
times: this.times, |
|
count: this.count |
|
}]) |
|
}, |
|
|
|
hasNext: function () { |
|
var o = this.options; |
|
if (BI.isNumber(o.count)) { |
|
return this.count < o.count; |
|
} |
|
return !!o.hasNext.apply(this, [{ |
|
times: this.times, |
|
count: this.count |
|
}]) |
|
}, |
|
|
|
prependItems: function (items) { |
|
this.count += items.length; |
|
if (this.next !== false) { |
|
if (this.hasPrev()) { |
|
this.options.items = this.options.items.concat(items); |
|
this.prev.setLoaded(); |
|
} else { |
|
this.prev.setEnd(); |
|
} |
|
} |
|
this.button_group.prependItems.apply(this.button_group, arguments); |
|
}, |
|
|
|
addItems: function (items) { |
|
this.count += items.length; |
|
if (BI.isObject(this.next)) { |
|
if (this.hasNext()) { |
|
this.options.items = this.options.items.concat(items); |
|
this.next.setLoaded(); |
|
} else { |
|
this.next.setEnd(); |
|
} |
|
} |
|
this.button_group.addItems.apply(this.button_group, arguments); |
|
}, |
|
|
|
populate: function (items) { |
|
var self = this, o = this.options; |
|
if (arguments.length === 0 && (BI.isFunction(o.itemsCreator))) { |
|
o.itemsCreator.apply(this, [{times: 1}, function () { |
|
if (arguments.length === 0) { |
|
throw new Error("arguments can not be null!!!"); |
|
} |
|
self.populate.apply(self, arguments); |
|
o.onLoaded(); |
|
}]); |
|
return; |
|
} |
|
this.options.items = items; |
|
this.times = 1; |
|
this.count = 0; |
|
this.count += items.length; |
|
if (BI.isObject(this.next)) { |
|
if (this.hasNext()) { |
|
this.next.setLoaded(); |
|
} else { |
|
this.next.invisible(); |
|
} |
|
} |
|
if (BI.isObject(this.prev)) { |
|
if (this.hasPrev()) { |
|
this.prev.setLoaded(); |
|
} else { |
|
this.prev.invisible(); |
|
} |
|
} |
|
this.button_group.populate.apply(this.button_group, arguments); |
|
}, |
|
|
|
setEnable: function (v) { |
|
this.button_group.setEnable(v); |
|
}, |
|
|
|
doBehavior: function () { |
|
this.button_group.doBehavior.apply(this.button_group, arguments); |
|
}, |
|
|
|
setNotSelectedValue: function () { |
|
this.button_group.setNotSelectedValue.apply(this.button_group, arguments); |
|
}, |
|
|
|
getNotSelectedValue: function () { |
|
return this.button_group.getNotSelectedValue(); |
|
}, |
|
|
|
setValue: function () { |
|
this.button_group.setValue.apply(this.button_group, arguments); |
|
}, |
|
|
|
getValue: function () { |
|
return this.button_group.getValue.apply(this.button_group, arguments); |
|
}, |
|
|
|
getAllButtons: function () { |
|
return this.button_group.getAllButtons(); |
|
}, |
|
|
|
getAllLeaves: function () { |
|
return this.button_group.getAllLeaves(); |
|
}, |
|
|
|
getSelectedButtons: function () { |
|
return this.button_group.getSelectedButtons(); |
|
}, |
|
|
|
getNotSelectedButtons: function () { |
|
return this.button_group.getNotSelectedButtons(); |
|
}, |
|
|
|
getIndexByValue: function (value) { |
|
return this.button_group.getIndexByValue(value); |
|
}, |
|
|
|
getNodeById: function (id) { |
|
return this.button_group.getNodeById(id); |
|
}, |
|
|
|
getNodeByValue: function (value) { |
|
return this.button_group.getNodeByValue(value); |
|
}, |
|
|
|
empty: function () { |
|
this.button_group.empty(); |
|
BI.each([this.prev, this.next], function (i, ob) { |
|
ob && ob.setVisible(false); |
|
}); |
|
}, |
|
|
|
destroy: function () { |
|
BI.Loader.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.Loader.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.loader", BI.Loader);/** |
|
* Created by GUY on 2015/6/26. |
|
*/ |
|
|
|
BI.Navigation = BI.inherit(BI.Widget, { |
|
_defaultConfig: function(){ |
|
return BI.extend(BI.Navigation.superclass._defaultConfig.apply(this,arguments), { |
|
direction: "bottom",//top, bottom, left, right, custom |
|
logic: { |
|
dynamic: false |
|
}, |
|
defaultShowIndex: 0, |
|
tab: { |
|
type: "bi.button_group", |
|
items: [], |
|
layouts: [] |
|
}, |
|
cardCreator: function(v){ |
|
return BI.createWidget(); |
|
}, |
|
|
|
afterCardCreated: BI.emptyFn, |
|
afterCardShow: BI.emptyFn |
|
}) |
|
}, |
|
|
|
_init: function(){ |
|
BI.Navigation.superclass._init.apply(this,arguments); |
|
var self = this, o = this.options; |
|
this.tab = BI.createWidget(this.options.tab, {type: "bi.button_group"}); |
|
this.cardMap = {}; |
|
this.showIndex = 0; |
|
this.layout = BI.createWidget({ |
|
type: "bi.card" |
|
}); |
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic(BI.LogicFactory.createLogicTypeByDirection(o.direction), BI.extend({}, o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection(o.direction, this.tab, this.layout) |
|
})))); |
|
|
|
|
|
new BI.ShowListener({ |
|
eventObj: this.tab, |
|
cardLayout: this.layout, |
|
cardNameCreator: function(v){ |
|
return self.showIndex + v; |
|
}, |
|
cardCreator: function(v){ |
|
var card = o.cardCreator(v); |
|
self.cardMap[v] = card; |
|
return card; |
|
}, |
|
afterCardCreated: BI.bind(this.afterCardCreated, this), |
|
afterCardShow: BI.bind(this.afterCardShow, this) |
|
}) |
|
if(o.defaultShowIndex !== false){ |
|
this.setSelect(o.defaultShowIndex); |
|
} |
|
}, |
|
|
|
afterCardCreated: function(v){ |
|
var self = this; |
|
this.cardMap[v].on(BI.Controller.EVENT_CHANGE, function(type, value, obj){ |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if(type === BI.Events.CLICK) { |
|
self.fireEvent(BI.Navigation.EVENT_CHANGE, obj); |
|
} |
|
}) |
|
this.options.afterCardCreated.apply(this, arguments); |
|
}, |
|
|
|
afterCardShow: function(v){ |
|
this.showIndex = v; |
|
this.options.afterCardShow.apply(this, arguments); |
|
}, |
|
|
|
populate: function(){ |
|
var card = this.layout.getShowingCard(); |
|
if(card){ |
|
return card.populate.apply(card, arguments); |
|
} |
|
}, |
|
|
|
setSelect: function(v){ |
|
this.showIndex = v; |
|
if(!this.layout.isCardExisted(v)){ |
|
var card = this.options.cardCreator(v); |
|
this.cardMap[v] = card; |
|
this.layout.addCardByName(v, card); |
|
this.afterCardCreated(v); |
|
} |
|
this.layout.showCardByName(v); |
|
BI.nextTick(BI.bind(this.afterCardShow, this, v)); |
|
}, |
|
|
|
getSelect: function(){ |
|
return this.showIndex; |
|
}, |
|
|
|
getSelectedCard: function(){ |
|
if(BI.isKey(this.showIndex)){ |
|
return this.cardMap[this.showIndex]; |
|
} |
|
}, |
|
|
|
/** |
|
* @override |
|
*/ |
|
setValue: function(v){ |
|
var card = this.layout.getShowingCard(); |
|
if(card){ |
|
card.setValue(v); |
|
} |
|
}, |
|
|
|
/** |
|
* @override |
|
*/ |
|
getValue: function(){ |
|
var card = this.layout.getShowingCard(); |
|
if(card){ |
|
return card.getValue(); |
|
} |
|
}, |
|
|
|
empty: function(){ |
|
this.layout.deleteAllCard(); |
|
this.cardMap = {}; |
|
}, |
|
|
|
destroy: function(){ |
|
BI.Navigation.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.Navigation.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.navigation", BI.Navigation);/** |
|
* 搜索逻辑控件 |
|
* |
|
* Created by GUY on 2015/9/28. |
|
* @class BI.Searcher |
|
* @extends BI.Widget |
|
*/ |
|
|
|
BI.Searcher = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.Searcher.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-searcher", |
|
lgap: 0, |
|
rgap: 0, |
|
tgap: 0, |
|
bgap: 0, |
|
vgap: 0, |
|
hgap: 0, |
|
|
|
isDefaultInit: false, |
|
isAutoSearch: true, //是否自动搜索 |
|
isAutoSync: true, //是否自动同步数据, 即是否保持搜索面板和adapter面板状态值的统一 |
|
chooseType: BI.ButtonGroup.CHOOSE_TYPE_SINGLE, |
|
|
|
//isAutoSearch为false时启用 |
|
onSearch: function (op, callback) { |
|
callback([]) |
|
}, |
|
|
|
|
|
el: { |
|
type: "bi.search_editor" |
|
}, |
|
|
|
popup: { |
|
type: "bi.searcher_view" |
|
}, |
|
|
|
adapter: null, |
|
masker: { //masker层 |
|
offset: {} |
|
} |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Searcher.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
|
|
this.editor = BI.createWidget(o.el, { |
|
type: "bi.search_editor" |
|
}); |
|
|
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
element: this, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
vgap: o.vgap, |
|
hgap: o.hgap, |
|
items: [this.editor] |
|
}); |
|
o.isDefaultInit && (this._assertPopupView()); |
|
|
|
var search = BI.debounce(BI.bind(this._search, this), BI.EVENT_RESPONSE_TIME, true); |
|
this.editor.on(BI.Controller.EVENT_CHANGE, function (type) { |
|
switch (type) { |
|
case BI.Events.STARTEDIT: |
|
self._startSearch(); |
|
break; |
|
case BI.Events.EMPTY: |
|
self._stopSearch(); |
|
break; |
|
case BI.Events.CHANGE: |
|
search(); |
|
break; |
|
case BI.Events.PAUSE: |
|
self._pauseSearch(); |
|
break; |
|
} |
|
}) |
|
}, |
|
|
|
_assertPopupView: function () { |
|
var self = this, o = this.options; |
|
if (!BI.Maskers.has(this.getName())) { |
|
this.popupView = BI.createWidget(o.popup, { |
|
type: "bi.searcher_view", |
|
chooseType: o.chooseType |
|
}); |
|
BI.Maskers.create(this.getName(), o.adapter, BI.extend({ |
|
container: this, |
|
render: this.popupView |
|
}, o.masker)); |
|
|
|
this.popupView.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
if (o.isAutoSync) { |
|
var values = o.adapter && o.adapter.getValue(); |
|
if (!obj.isSelected()) { |
|
o.adapter && o.adapter.setValue(BI.deepWithout(values, obj.getValue())); |
|
} else { |
|
switch (o.chooseType) { |
|
case BI.ButtonGroup.CHOOSE_TYPE_SINGLE: |
|
o.adapter && o.adapter.setValue([obj.getValue()]); |
|
break; |
|
case BI.ButtonGroup.CHOOSE_TYPE_MULTI: |
|
values.push(obj.getValue()); |
|
o.adapter && o.adapter.setValue(values); |
|
break; |
|
} |
|
} |
|
} |
|
self.fireEvent(BI.Searcher.EVENT_CHANGE, value, obj); |
|
} |
|
}); |
|
BI.nextTick(function () { |
|
self.fireEvent(BI.Searcher.EVENT_AFTER_INIT); |
|
}); |
|
} |
|
}, |
|
|
|
_startSearch: function () { |
|
this._assertPopupView(); |
|
this._stop = false; |
|
this._isSearching = true; |
|
this.fireEvent(BI.Searcher.EVENT_START); |
|
this.popupView.startSearch && this.popupView.startSearch(); |
|
//搜索前先清空dom |
|
BI.Maskers.get(this.getName()).empty(); |
|
BI.nextTick(function (name) { |
|
BI.Maskers.show(name); |
|
}, this.getName()); |
|
}, |
|
|
|
_pauseSearch: function () { |
|
var o = this.options, name = this.getName(); |
|
this._stop = true; |
|
BI.nextTick(function (name) { |
|
BI.Maskers.hide(name); |
|
}, this.getName()); |
|
if (BI.Maskers.has(name) && this._isSearching === true) { |
|
this.popupView && this.popupView.pauseSearch && this.popupView.pauseSearch(); |
|
this.fireEvent(BI.Searcher.EVENT_PAUSE); |
|
} |
|
this._isSearching = false; |
|
}, |
|
|
|
_stopSearch: function () { |
|
var o = this.options, name = this.getName(); |
|
this._stop = true; |
|
BI.Maskers.hide(name); |
|
if (BI.Maskers.has(name) && this._isSearching === true) { |
|
//搜索后清空dom |
|
BI.nextTick(function () { |
|
BI.Maskers.has(name) && BI.Maskers.get(name).empty(); |
|
}); |
|
this.popupView && this.popupView.stopSearch && this.popupView.stopSearch(); |
|
this.fireEvent(BI.Searcher.EVENT_STOP); |
|
} |
|
this._isSearching = false; |
|
}, |
|
|
|
_search: function () { |
|
var self = this, o = this.options, keyword = this.editor.getValue(); |
|
if (keyword === "" || this._stop) { |
|
return; |
|
} |
|
if (o.isAutoSearch) { |
|
var items = (o.adapter.getItems && o.adapter.getItems()) || o.adapter.attr("items") || []; |
|
var finding = BI.Func.getSearchResult(items, keyword); |
|
var matched = finding.matched, finded = finding.finded; |
|
this.popupView.populate(finded, matched, keyword); |
|
o.isAutoSync && this.popupView.setValue(o.adapter.getValue()); |
|
self.fireEvent(BI.Searcher.EVENT_SEARCHING); |
|
return; |
|
} |
|
this.popupView.loading && this.popupView.loading(); |
|
o.onSearch({ |
|
times: 1, |
|
keyword: keyword, |
|
selectedValues: o.adapter && o.adapter.getValue() |
|
}, function (searchResult, matchResult) { |
|
if (!self._stop) { |
|
var args = [].slice.call(arguments); |
|
if (args.length > 0) { |
|
args.push(keyword); |
|
} |
|
BI.Maskers.show(self.getName()); |
|
self.popupView.populate.apply(self.popupView, args); |
|
o.isAutoSync && self.popupView.setValue(o.adapter && o.adapter.getValue()); |
|
self.popupView.loaded && self.popupView.loaded(); |
|
self.fireEvent(BI.Searcher.EVENT_SEARCHING); |
|
} |
|
}); |
|
}, |
|
|
|
setAdapter: function (adapter) { |
|
this.options.adapter = adapter; |
|
BI.Maskers.remove(this.getName()); |
|
}, |
|
|
|
doSearch: function () { |
|
if (this.isSearching()) { |
|
this._search(); |
|
} |
|
}, |
|
|
|
stopSearch: function () { |
|
this._stopSearch();//先停止搜索,然后再去设置editor为空 |
|
//important:停止搜索必须退出编辑状态,这里必须加上try(input框不显示时blur会抛异常) |
|
try { |
|
this.editor.blur(); |
|
} catch (e) { |
|
if (!this.editor.blur) { |
|
throw new Error("editor没有实现blur方法"); |
|
} |
|
} finally { |
|
this.editor.setValue(""); |
|
} |
|
}, |
|
|
|
isSearching: function () { |
|
return this._isSearching; |
|
}, |
|
|
|
isViewVisible: function () { |
|
return this.editor.isEnabled() && BI.Maskers.isVisible(this.getName()); |
|
}, |
|
|
|
getView: function () { |
|
return this.popupView; |
|
}, |
|
|
|
hasMatched: function () { |
|
this._assertPopupView(); |
|
return this.popupView.hasMatched(); |
|
}, |
|
|
|
adjustHeight: function () { |
|
if (BI.Maskers.has(this.getName()) && BI.Maskers.get(this.getName()).isVisible()) { |
|
BI.Maskers.show(this.getName()); |
|
} |
|
}, |
|
|
|
adjustView: function () { |
|
this.isViewVisible() && BI.Maskers.show(this.getName()); |
|
}, |
|
|
|
setValue: function (v) { |
|
this._assertPopupView(); |
|
this.popupView && this.popupView.setValue(v); |
|
}, |
|
|
|
getKeyword: function () { |
|
return this.editor.getValue(); |
|
}, |
|
|
|
getKeywords: function () { |
|
return this.editor.getKeywords(); |
|
}, |
|
|
|
getValue: function () { |
|
var o = this.options; |
|
if (o.isAutoSync) { |
|
return o.adapter.getValue(); |
|
} |
|
if (this.isSearching()) { |
|
return this.popupView.getValue(); |
|
} else { |
|
return o.adapter.getValue(); |
|
} |
|
}, |
|
|
|
populate: function (result, searchResult, keyword) { |
|
var o = this.options; |
|
this._assertPopupView(); |
|
this.popupView.populate.apply(this.popupView, arguments); |
|
if (o.isAutoSync) { |
|
this.popupView.setValue(o.adapter.getValue()); |
|
} |
|
}, |
|
|
|
empty: function () { |
|
this.popupView && this.popupView.empty(); |
|
}, |
|
|
|
destroy: function () { |
|
BI.Maskers.remove(this.getName()); |
|
BI.Searcher.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.Searcher.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.Searcher.EVENT_START = "EVENT_START"; |
|
BI.Searcher.EVENT_STOP = "EVENT_STOP"; |
|
BI.Searcher.EVENT_PAUSE = "EVENT_PAUSE"; |
|
BI.Searcher.EVENT_SEARCHING = "EVENT_SEARCHING"; |
|
BI.Searcher.EVENT_AFTER_INIT = "EVENT_AFTER_INIT"; |
|
|
|
$.shortcut("bi.searcher", BI.Searcher);/** |
|
* |
|
* 切换显示或隐藏面板 |
|
* |
|
* Created by GUY on 2015/11/2. |
|
* @class BI.Switcher |
|
* @extends BI.Widget |
|
*/ |
|
BI.Switcher = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.Switcher.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-switcher", |
|
direction: BI.Direction.Top, |
|
trigger: "click", |
|
toggle: true, |
|
el: {}, |
|
popup: {}, |
|
adapter: null, |
|
masker: {}, |
|
switcherClass: "bi-switcher-popup", |
|
hoverClass: "bi-switcher-hover" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Switcher.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this._initSwitcher(); |
|
this._initPullDownAction(); |
|
this.switcher.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
if (self.isEnabled() && this.isEnabled()) { |
|
if (type === BI.Events.EXPAND) { |
|
self._popupView(); |
|
} |
|
if (type === BI.Events.COLLAPSE) { |
|
self._hideView(); |
|
} |
|
if (type === BI.Events.EXPAND || type === BI.Events.COLLAPSE) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
} |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.Switcher.EVENT_TRIGGER_CHANGE, value, obj); |
|
} |
|
} |
|
}); |
|
|
|
this.element.hover(function () { |
|
if (self.isEnabled() && self.switcher.isEnabled()) { |
|
self.element.addClass(o.hoverClass); |
|
} |
|
}, function () { |
|
if (self.isEnabled() && self.switcher.isEnabled()) { |
|
self.element.removeClass(o.hoverClass); |
|
} |
|
}); |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
scrolly: false, |
|
element: this, |
|
items: [ |
|
{el: this.switcher} |
|
] |
|
}); |
|
o.isDefaultInit && (this._assertPopupView()); |
|
}, |
|
|
|
_toggle: function () { |
|
this._assertPopupView(); |
|
if (this.isExpanded()) { |
|
this._hideView(); |
|
} else { |
|
if (this.isEnabled()) { |
|
this._popupView(); |
|
} |
|
} |
|
}, |
|
|
|
_initPullDownAction: function () { |
|
var self = this, o = this.options; |
|
var evs = this.options.trigger.split(","); |
|
BI.each(evs, function (i, e) { |
|
switch (e) { |
|
case "hover": |
|
self.element[e](function (e) { |
|
if (self.isEnabled() && self.switcher.isEnabled()) { |
|
self._popupView(); |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EXPAND, '', self.switcher); |
|
self.fireEvent(BI.Switcher.EVENT_EXPAND); |
|
} |
|
}, function () { |
|
if (self.isEnabled() && self.switcher.isEnabled() && o.toggle) { |
|
self._hideView(); |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.COLLAPSE, '', self.switcher); |
|
self.fireEvent(BI.Switcher.EVENT_COLLAPSE); |
|
} |
|
}); |
|
break; |
|
default : |
|
if (e) { |
|
self.element.off(e + "." + self.getName()).on(e + "." + self.getName(), BI.debounce(function (e) { |
|
if (self.switcher.element.__isMouseInBounds__(e)) { |
|
if (self.isEnabled() && self.switcher.isEnabled()) { |
|
o.toggle ? self._toggle() : self._popupView(); |
|
if (self.isExpanded()) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EXPAND, '', self.switcher); |
|
self.fireEvent(BI.Switcher.EVENT_EXPAND); |
|
} else { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.COLLAPSE, '', self.switcher); |
|
self.fireEvent(BI.Switcher.EVENT_COLLAPSE); |
|
} |
|
} |
|
} |
|
}, BI.EVENT_RESPONSE_TIME, true)); |
|
} |
|
break; |
|
} |
|
}) |
|
}, |
|
|
|
_initSwitcher: function () { |
|
this.switcher = BI.createWidget(this.options.el); |
|
}, |
|
|
|
_assertPopupView: function () { |
|
var self = this, o = this.options; |
|
if (!this._created) { |
|
this.popupView = BI.createWidget(o.popup, { |
|
type: "bi.button_group", |
|
element: o.adapter && BI.Maskers.create(this.getName(), o.adapter, BI.extend({container: this}, o.masker)), |
|
cls: "switcher-popup", |
|
layouts: [{ |
|
type: "bi.vertical", |
|
hgap: 0, |
|
vgap: 0 |
|
}] |
|
}); |
|
this.popupView.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.Switcher.EVENT_CHANGE, value, obj); |
|
} |
|
}); |
|
if (o.direction !== BI.Direction.Custom && !o.adapter) { |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
scrolly: false, |
|
element: this, |
|
items: [ |
|
{el: this.popupView} |
|
] |
|
}); |
|
} |
|
this._created = true; |
|
BI.nextTick(function () { |
|
self.fireEvent(BI.Switcher.EVENT_AFTER_INIT); |
|
}); |
|
} |
|
}, |
|
|
|
_hideView: function () { |
|
this.fireEvent(BI.Switcher.EVENT_BEFORE_HIDEVIEW); |
|
var self = this, o = this.options; |
|
o.adapter ? BI.Maskers.hide(self.getName()) : (self.popupView && self.popupView.setVisible(false)); |
|
BI.nextTick(function () { |
|
o.adapter ? BI.Maskers.hide(self.getName()) : (self.popupView && self.popupView.setVisible(false)); |
|
self.element.removeClass(o.switcherClass); |
|
self.fireEvent(BI.Switcher.EVENT_AFTER_HIDEVIEW); |
|
}); |
|
}, |
|
|
|
_popupView: function () { |
|
var self = this, o = this.options; |
|
this._assertPopupView(); |
|
this.fireEvent(BI.Switcher.EVENT_BEFORE_POPUPVIEW); |
|
o.adapter ? BI.Maskers.show(this.getName()) : self.popupView.setVisible(true); |
|
BI.nextTick(function (name) { |
|
o.adapter ? BI.Maskers.show(name) : self.popupView.setVisible(true); |
|
self.element.addClass(o.switcherClass); |
|
self.fireEvent(BI.Switcher.EVENT_AFTER_POPUPVIEW); |
|
}, this.getName()); |
|
}, |
|
|
|
populate: function (items) { |
|
this._assertPopupView(); |
|
this.popupView.populate.apply(this.popupView, arguments); |
|
this.switcher.populate.apply(this.switcher, arguments); |
|
}, |
|
|
|
setEnable: function (arg) { |
|
BI.Switcher.superclass.setEnable.apply(this, arguments); |
|
this.switcher && this.switcher.setEnable(arg); |
|
this.popupView && this.popupView.setEnable(arg); |
|
!arg && this._hideView(); |
|
}, |
|
|
|
setValue: function (v) { |
|
this._assertPopupView(); |
|
this.switcher.setValue(v); |
|
this.popupView && this.popupView.setValue(v); |
|
}, |
|
|
|
getValue: function () { |
|
this._assertPopupView(); |
|
return this.popupView ? this.popupView.getValue() : []; |
|
}, |
|
|
|
isViewVisible: function () { |
|
return this.isEnabled() && this.switcher.isEnabled() && |
|
(this.options.adapter ? BI.Maskers.isVisible(this.getName()) : (this.popupView && this.popupView.isVisible())); |
|
}, |
|
|
|
isExpanded: function () { |
|
return this.isViewVisible(); |
|
}, |
|
|
|
showView: function () { |
|
if (this.isEnabled() && this.switcher.isEnabled()) { |
|
this._popupView(); |
|
} |
|
}, |
|
|
|
hideView: function () { |
|
this._hideView(); |
|
}, |
|
|
|
getView: function () { |
|
return this.popupView; |
|
}, |
|
|
|
adjustView: function () { |
|
this.isViewVisible() && BI.Maskers.show(this.getName()); |
|
}, |
|
|
|
doBehavior: function () { |
|
this._assertPopupView(); |
|
this.popupView && this.popupView.doBehavior.apply(this.popupView, arguments); |
|
}, |
|
|
|
getAllLeaves: function () { |
|
return this.popupView && this.popupView.getAllLeaves(); |
|
}, |
|
|
|
getNodeById: function (id) { |
|
if (this.switcher.attr("id") === id) { |
|
return this.switcher; |
|
} |
|
return this.popupView && this.popupView.getNodeById(id); |
|
}, |
|
|
|
getNodeByValue: function (value) { |
|
if (this.switcher.getValue() === value) { |
|
return this.switcher; |
|
} |
|
return this.popupView && this.popupView.getNodeByValue(value); |
|
}, |
|
|
|
empty: function () { |
|
this.popupView && this.popupView.empty(); |
|
}, |
|
|
|
destroy: function () { |
|
BI.Switcher.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.Switcher.EVENT_EXPAND = "EVENT_EXPAND"; |
|
BI.Switcher.EVENT_COLLAPSE = "EVENT_COLLAPSE"; |
|
BI.Switcher.EVENT_TRIGGER_CHANGE = "EVENT_TRIGGER_CHANGE"; |
|
BI.Switcher.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.Switcher.EVENT_AFTER_INIT = "EVENT_AFTER_INIT"; |
|
|
|
|
|
BI.Switcher.EVENT_BEFORE_POPUPVIEW = "EVENT_BEFORE_POPUPVIEW"; |
|
BI.Switcher.EVENT_AFTER_POPUPVIEW = "EVENT_AFTER_POPUPVIEW"; |
|
BI.Switcher.EVENT_BEFORE_HIDEVIEW = "EVENT_BEFORE_HIDEVIEW"; |
|
BI.Switcher.EVENT_AFTER_HIDEVIEW = "EVENT_AFTER_HIDEVIEW"; |
|
|
|
$.shortcut("bi.switcher", BI.Switcher);/** |
|
* Created by GUY on 2015/6/26. |
|
*/ |
|
|
|
BI.Tab = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.Tab.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-tab", |
|
direction: "top",//top, bottom, left, right, custom |
|
logic: { |
|
dynamic: false |
|
}, |
|
defaultShowIndex: 0, |
|
tab: false, |
|
cardCreator: function (v) { |
|
return BI.createWidget(); |
|
} |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Tab.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
if (BI.isObject(o.tab)) { |
|
this.tab = BI.createWidget(this.options.tab, {type: "bi.button_group"}); |
|
this.tab.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
}) |
|
} |
|
this.cardMap = {}; |
|
this.layout = BI.createWidget({ |
|
type: "bi.card" |
|
}); |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic(BI.LogicFactory.createLogicTypeByDirection(o.direction), BI.extend({}, o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection(o.direction, this.tab, this.layout) |
|
})))); |
|
|
|
var listener = new BI.ShowListener({ |
|
eventObj: this.tab, |
|
cardLayout: this.layout, |
|
cardCreator: function (v) { |
|
var card = o.cardCreator.apply(self, arguments); |
|
self.cardMap[v] = card; |
|
return card; |
|
}, |
|
afterCardShow: function (v) { |
|
self.curr = v; |
|
} |
|
}); |
|
listener.on(BI.ShowListener.EVENT_CHANGE, function (value) { |
|
self.fireEvent(BI.Tab.EVENT_CHANGE, value, self); |
|
}); |
|
}, |
|
|
|
_assertCard: function (v) { |
|
if (!this.layout.isCardExisted(v)) { |
|
var card = this.options.cardCreator(v); |
|
this.cardMap[v] = card; |
|
this.layout.addCardByName(v, card); |
|
} |
|
}, |
|
|
|
mounted: function () { |
|
var o = this.options; |
|
if (o.defaultShowIndex !== false) { |
|
this.setSelect(o.defaultShowIndex); |
|
} |
|
}, |
|
|
|
setSelect: function (v) { |
|
this.tab && this.tab.setValue(v); |
|
this._assertCard(v); |
|
this.layout.showCardByName(v); |
|
if (this.curr !== v) { |
|
this.curr = v; |
|
} |
|
}, |
|
|
|
getSelect: function () { |
|
return this.curr; |
|
}, |
|
|
|
getSelectedTab: function () { |
|
return this.layout.getShowingCard(); |
|
}, |
|
|
|
getTab: function (v) { |
|
this._assertCard(v); |
|
return this.layout.getCardByName(v); |
|
}, |
|
|
|
setValue: function (v) { |
|
var card = this.layout.getShowingCard(); |
|
if (card) { |
|
card.setValue(v); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
var card = this.layout.getShowingCard(); |
|
if (card) { |
|
return card.getValue(); |
|
} |
|
}, |
|
|
|
populate: function () { |
|
var card = this.layout.getShowingCard(); |
|
if (card) { |
|
return card.populate && card.populate.apply(card, arguments); |
|
} |
|
}, |
|
|
|
empty: function () { |
|
this.layout.deleteAllCard(); |
|
this.cardMap = {}; |
|
}, |
|
|
|
destroy: function () { |
|
this.cardMap = {}; |
|
BI.Tab.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
BI.Tab.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.tab", BI.Tab);/** |
|
* 表示当前对象 |
|
* |
|
* Created by GUY on 2015/9/7. |
|
* @class BI.EL |
|
* @extends BI.Widget |
|
*/ |
|
BI.EL = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.EL.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-el", |
|
el: {}, |
|
layout: {} |
|
}); |
|
}, |
|
_init: function () { |
|
BI.EL.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.ele = BI.createWidget(o.el); |
|
BI.createWidget(o.layout, { |
|
type: "bi.adaptive", |
|
element: this, |
|
items: [this.ele] |
|
}); |
|
this.ele.on(BI.Controller.EVENT_CHANGE, function () { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
}) |
|
}, |
|
|
|
setValue: function (v) { |
|
this.ele.setValue(v); |
|
}, |
|
|
|
getValue: function () { |
|
return this.ele.getValue(); |
|
}, |
|
|
|
populate: function () { |
|
this.ele.populate.apply(this, arguments); |
|
} |
|
}); |
|
$.shortcut('bi.el', BI.EL);/** |
|
* 选色控件 |
|
* |
|
* Created by GUY on 2015/11/16. |
|
* @class BI.Farbtastic |
|
* @extends BI.Widget |
|
*/ |
|
BI.Farbtastic = BI.inherit(BI.Widget, { |
|
|
|
_defaultConfig: function () { |
|
return BI.extend(BI.Farbtastic.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-farbtastic", |
|
width: 195, |
|
height: 195 |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Farbtastic.superclass._init.apply(this, arguments); |
|
var self = this; |
|
|
|
|
|
|
|
this.farbtastic = $.farbtastic(this.element, function (v) { |
|
self.fireEvent(BI.Farbtastic.EVENT_CHANGE, self.getValue(), self); |
|
}); |
|
}, |
|
|
|
setValue: function (color) { |
|
this.farbtastic.setColor(color); |
|
}, |
|
|
|
getValue: function () { |
|
return this.farbtastic.color; |
|
} |
|
}); |
|
BI.Farbtastic.EVENT_CHANGE = "Farbtastic.EVENT_CHANGE"; |
|
$.shortcut("bi.farbtastic", BI.Farbtastic);(function (mod) { |
|
mod(CodeMirror); |
|
})(function (CodeMirror) { |
|
var Pos = CodeMirror.Pos; |
|
|
|
function forEach(arr, f) { |
|
for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); |
|
} |
|
|
|
function arrayContains(arr, item) { |
|
if (!Array.prototype.indexOf) { |
|
var i = arr.length; |
|
while (i--) { |
|
if (arr[i] === item) { |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
return arr.indexOf(item) != -1; |
|
} |
|
|
|
function scriptHint(editor, keywords, getToken, options) { |
|
// Find the token at the cursor |
|
var cur = editor.getCursor(), token = getToken(editor, cur); |
|
if (/\b(?:string)\b/.test(token.type)) { |
|
return; |
|
} |
|
token.state = CodeMirror.innerMode(editor.getMode(), token.state).state; |
|
|
|
if (!/^[\w$_]*$/.test(token.string)) { |
|
token = { |
|
start: cur.ch, end: cur.ch, string: "", state: token.state, |
|
type: token.string == "." ? "property" : null |
|
}; |
|
} else if (token.end > cur.ch) { |
|
token.end = cur.ch; |
|
token.string = token.string.slice(0, cur.ch - token.start); |
|
} |
|
|
|
var tprop = token; |
|
// If it is a property, find out what it is a property of. |
|
while (tprop.type == "property") { |
|
tprop = getToken(editor, Pos(cur.line, tprop.start)); |
|
if (tprop.string != ".") return; |
|
tprop = getToken(editor, Pos(cur.line, tprop.start)); |
|
if (!context) var context = []; |
|
context.push(tprop); |
|
} |
|
return { |
|
list: getCompletions(token, context, keywords, options), |
|
from: Pos(cur.line, token.start), |
|
to: Pos(cur.line, token.end) |
|
}; |
|
} |
|
|
|
function getFormulaKeywords() { |
|
return FormulaCollections; |
|
} |
|
|
|
function formulaHint(editor, options) { |
|
return scriptHint(editor, getFormulaKeywords(), |
|
function (e, cur) { |
|
return e.getTokenAt(cur); |
|
}, |
|
options); |
|
}; |
|
CodeMirror.registerHelper("hint", "formula", formulaHint); |
|
|
|
function getCompletions(token, context, keywords, options) { |
|
var found = [], start = token.string; |
|
if (!start) { |
|
return found; |
|
} |
|
function maybeAdd(str) { |
|
if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) { |
|
found.push(str); |
|
} |
|
} |
|
|
|
if (context && context.length) { |
|
context.pop(); |
|
} else { |
|
forEach(keywords, maybeAdd); |
|
} |
|
return found; |
|
} |
|
});(function (mod) { |
|
mod(CodeMirror); |
|
})(function (CodeMirror) { |
|
"use strict"; |
|
|
|
CodeMirror.defineMode('formula', function () { |
|
function wordObj(words) { |
|
var o = {}; |
|
for (var i = 0, e = words.length; i < e; ++i) o[words[i]] = true; |
|
return o; |
|
} |
|
|
|
var atoms = wordObj(['false', 'true']); |
|
var keywords = wordObj(FormulaCollections); |
|
|
|
function tokenBase(stream, state) { |
|
if (stream.eatSpace()) { |
|
return null; |
|
} |
|
var ch = stream.next(); |
|
|
|
if (ch === '"' || ch === '\'') { |
|
nextUntilUnescaped(stream, ch); |
|
return "string"; |
|
} |
|
if (/[\[\],\(\)]/.test(ch)) { |
|
return 'bracket'; |
|
} |
|
|
|
// richie:暂时不需要解析操作符号 |
|
//if (/[+\-*\/=<>!&|]/.test(ch)) { |
|
// return 'operator'; |
|
//} |
|
//if (/\d|\d./.test(ch)) { |
|
// stream.eatWhile(/\d|\./); |
|
// if (stream.eol() || !/\w/.test(stream.peek())) { |
|
// return 'number'; |
|
// } |
|
//} |
|
|
|
|
|
|
|
stream.eatWhile(/[\w-]/); |
|
var word = stream.current(); |
|
if (atoms.hasOwnProperty(word)) { |
|
return "atom"; |
|
} |
|
if (keywords.hasOwnProperty(word)) { |
|
return "keyword"; |
|
} |
|
return null; |
|
} |
|
|
|
function nextUntilUnescaped(stream, end) { |
|
var escaped = false, next; |
|
while ((next = stream.next()) != null) { |
|
if (next === end && !escaped) { |
|
return false; |
|
} |
|
escaped = !escaped && next === "\\"; |
|
} |
|
return escaped; |
|
} |
|
|
|
function tokenize(stream, state) { |
|
return (state.tokens[0] || tokenBase)(stream, state); |
|
} |
|
|
|
return { |
|
startState: function () { |
|
return {tokens: []}; |
|
}, |
|
token: function (stream, state) { |
|
return tokenize(stream, state); |
|
}, |
|
fold: "brace" |
|
}; |
|
}); |
|
CodeMirror.defineMIME("text/fx-formula", "formula"); |
|
});// CodeMirror, copyright (c) by Marijn Haverbeke and others |
|
// Distributed under an MIT license: http://codemirror.net/LICENSE |
|
|
|
(function (mod) { |
|
mod(CodeMirror); |
|
})(function (CodeMirror) { |
|
"use strict"; |
|
|
|
var HINT_ELEMENT_CLASS = "CodeMirror-hint"; |
|
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; |
|
|
|
// This is the old interface, kept around for now to stay |
|
// backwards-compatible. |
|
CodeMirror.showHint = function (cm, getHints, options) { |
|
if (!getHints) return cm.showHint(options); |
|
if (options && options.async) getHints.async = true; |
|
var newOpts = {hint: getHints}; |
|
if (options) for (var prop in options) newOpts[prop] = options[prop]; |
|
return cm.showHint(newOpts); |
|
}; |
|
|
|
CodeMirror.defineExtension("showHint", function (options) { |
|
// We want a single cursor position. |
|
if (this.listSelections().length > 1 || this.somethingSelected()) return; |
|
|
|
if (this.state.completionActive) this.state.completionActive.close(); |
|
var completion = this.state.completionActive = new Completion(this, options); |
|
if (!completion.options.hint) return; |
|
|
|
CodeMirror.signal(this, "startCompletion", this); |
|
completion.update(true); |
|
}); |
|
|
|
function Completion(cm, options) { |
|
this.cm = cm; |
|
this.options = this.buildOptions(options); |
|
this.widget = null; |
|
this.debounce = 0; |
|
this.tick = 0; |
|
this.startPos = this.cm.getCursor(); |
|
this.startLen = this.cm.getLine(this.startPos.line).length; |
|
|
|
var self = this; |
|
cm.on("cursorActivity", this.activityFunc = function () { |
|
self.cursorActivity(); |
|
}); |
|
} |
|
|
|
var requestAnimationFrame = window.requestAnimationFrame || function (fn) { |
|
return setTimeout(fn, 1000 / 60); |
|
}; |
|
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; |
|
|
|
Completion.prototype = { |
|
close: function () { |
|
if (!this.active()) return; |
|
this.cm.state.completionActive = null; |
|
this.tick = null; |
|
this.cm.off("cursorActivity", this.activityFunc); |
|
|
|
if (this.widget && this.data) CodeMirror.signal(this.data, "close"); |
|
if (this.widget) this.widget.close(); |
|
CodeMirror.signal(this.cm, "endCompletion", this.cm); |
|
}, |
|
|
|
active: function () { |
|
return this.cm.state.completionActive == this; |
|
}, |
|
|
|
pick: function (data, i) { |
|
var completion = data.list[i]; |
|
if (completion.hint) completion.hint(this.cm, data, completion); |
|
else { |
|
this.cm.replaceRange(getText(completion), completion.from || data.from, |
|
completion.to || data.to, "complete"); |
|
var to = this.cm.getCursor(); |
|
this.cm.markText(completion.from || data.from, to, {className: "#function", atomic: true}); |
|
this.cm.replaceSelection("() "); |
|
to = this.cm.getCursor(); |
|
to.ch = to.ch - 2; |
|
this.cm.setCursor(to); |
|
this.cm.focus(); |
|
} |
|
CodeMirror.signal(data, "pick", completion); |
|
this.close(); |
|
}, |
|
|
|
cursorActivity: function () { |
|
if (this.debounce) { |
|
cancelAnimationFrame(this.debounce); |
|
this.debounce = 0; |
|
} |
|
|
|
var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line); |
|
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || |
|
pos.ch < this.startPos.ch || this.cm.somethingSelected() || |
|
(pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { |
|
this.close(); |
|
} else { |
|
var self = this; |
|
this.debounce = requestAnimationFrame(function () { |
|
self.update(); |
|
}); |
|
if (this.widget) this.widget.disable(); |
|
} |
|
}, |
|
|
|
update: function (first) { |
|
if (this.tick == null) return; |
|
if (this.data) CodeMirror.signal(this.data, "update"); |
|
if (!this.options.hint.async) { |
|
this.finishUpdate(this.options.hint(this.cm, this.options), first); |
|
} else { |
|
var myTick = ++this.tick, self = this; |
|
this.options.hint(this.cm, function (data) { |
|
if (self.tick == myTick) self.finishUpdate(data, first); |
|
}, this.options); |
|
} |
|
}, |
|
|
|
finishUpdate: function (data, first) { |
|
this.data = data; |
|
|
|
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); |
|
if (this.widget) this.widget.close(); |
|
if (data && data.list.length) { |
|
if (picked && data.list.length == 1) { |
|
this.pick(data, 0); |
|
} else { |
|
this.widget = new Widget(this, data); |
|
CodeMirror.signal(data, "shown"); |
|
} |
|
} |
|
}, |
|
|
|
buildOptions: function (options) { |
|
var editor = this.cm.options.hintOptions; |
|
var out = {}; |
|
for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; |
|
if (editor) for (var prop in editor) |
|
if (editor[prop] !== undefined) out[prop] = editor[prop]; |
|
if (options) for (var prop in options) |
|
if (options[prop] !== undefined) out[prop] = options[prop]; |
|
return out; |
|
} |
|
}; |
|
|
|
function getText(completion) { |
|
if (typeof completion == "string") return completion; |
|
else return completion.text; |
|
} |
|
|
|
function buildKeyMap(completion, handle) { |
|
var baseMap = { |
|
Up: function () { |
|
handle.moveFocus(-1); |
|
}, |
|
Down: function () { |
|
handle.moveFocus(1); |
|
}, |
|
PageUp: function () { |
|
handle.moveFocus(-handle.menuSize() + 1, true); |
|
}, |
|
PageDown: function () { |
|
handle.moveFocus(handle.menuSize() - 1, true); |
|
}, |
|
Home: function () { |
|
handle.setFocus(0); |
|
}, |
|
End: function () { |
|
handle.setFocus(handle.length - 1); |
|
}, |
|
Enter: handle.pick, |
|
Tab: handle.pick, |
|
Esc: handle.close |
|
}; |
|
var custom = completion.options.customKeys; |
|
var ourMap = custom ? {} : baseMap; |
|
|
|
function addBinding(key, val) { |
|
var bound; |
|
if (typeof val != "string") |
|
bound = function (cm) { |
|
return val(cm, handle); |
|
}; |
|
// This mechanism is deprecated |
|
else if (baseMap.hasOwnProperty(val)) |
|
bound = baseMap[val]; |
|
else |
|
bound = val; |
|
ourMap[key] = bound; |
|
} |
|
|
|
if (custom) |
|
for (var key in custom) if (custom.hasOwnProperty(key)) |
|
addBinding(key, custom[key]); |
|
var extra = completion.options.extraKeys; |
|
if (extra) |
|
for (var key in extra) if (extra.hasOwnProperty(key)) |
|
addBinding(key, extra[key]); |
|
return ourMap; |
|
} |
|
|
|
function getHintElement(hintsElement, el) { |
|
while (el && el != hintsElement) { |
|
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; |
|
el = el.parentNode; |
|
} |
|
} |
|
|
|
function Widget(completion, data) { |
|
this.completion = completion; |
|
this.data = data; |
|
this.picked = false; |
|
var widget = this, cm = completion.cm; |
|
|
|
var hints = this.hints = document.createElement("ul"); |
|
hints.className = "CodeMirror-hints"; |
|
this.selectedHint = data.selectedHint || 0; |
|
|
|
var completions = data.list; |
|
for (var i = 0; i < completions.length; ++i) { |
|
var elt = hints.appendChild(document.createElement("li")), cur = completions[i]; |
|
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); |
|
if (cur.className != null) className = cur.className + " " + className; |
|
elt.className = className; |
|
if (cur.render) cur.render(elt, data, cur); |
|
else elt.appendChild(document.createTextNode(cur.displayText || getText(cur))); |
|
elt.hintId = i; |
|
} |
|
|
|
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); |
|
var left = pos.left, top = pos.bottom, below = true; |
|
hints.style.left = left + "px"; |
|
hints.style.top = top + "px"; |
|
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. |
|
var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); |
|
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); |
|
(completion.options.container || document.body).appendChild(hints); |
|
var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; |
|
if (overlapY > 0) { |
|
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); |
|
if (curTop - height > 0) { // Fits above cursor |
|
hints.style.top = (top = pos.top - height) + "px"; |
|
below = false; |
|
} else if (height > winH) { |
|
hints.style.height = (winH - 5) + "px"; |
|
hints.style.top = (top = pos.bottom - box.top) + "px"; |
|
var cursor = cm.getCursor(); |
|
if (data.from.ch != cursor.ch) { |
|
pos = cm.cursorCoords(cursor); |
|
hints.style.left = (left = pos.left) + "px"; |
|
box = hints.getBoundingClientRect(); |
|
} |
|
} |
|
} |
|
var overlapX = box.right - winW; |
|
if (overlapX > 0) { |
|
if (box.right - box.left > winW) { |
|
hints.style.width = (winW - 5) + "px"; |
|
overlapX -= (box.right - box.left) - winW; |
|
} |
|
hints.style.left = (left = pos.left - overlapX) + "px"; |
|
} |
|
|
|
cm.addKeyMap(this.keyMap = buildKeyMap(completion, { |
|
moveFocus: function (n, avoidWrap) { |
|
widget.changeActive(widget.selectedHint + n, avoidWrap); |
|
}, |
|
setFocus: function (n) { |
|
widget.changeActive(n); |
|
}, |
|
menuSize: function () { |
|
return widget.screenAmount(); |
|
}, |
|
length: completions.length, |
|
close: function () { |
|
completion.close(); |
|
}, |
|
pick: function () { |
|
widget.pick(); |
|
}, |
|
data: data |
|
})); |
|
|
|
if (completion.options.closeOnUnfocus) { |
|
var closingOnBlur; |
|
cm.on("blur", this.onBlur = function () { |
|
closingOnBlur = setTimeout(function () { |
|
completion.close(); |
|
}, 100); |
|
}); |
|
cm.on("focus", this.onFocus = function () { |
|
clearTimeout(closingOnBlur); |
|
}); |
|
} |
|
|
|
var startScroll = cm.getScrollInfo(); |
|
cm.on("scroll", this.onScroll = function () { |
|
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); |
|
var newTop = top + startScroll.top - curScroll.top; |
|
var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop); |
|
if (!below) point += hints.offsetHeight; |
|
if (point <= editor.top || point >= editor.bottom) return completion.close(); |
|
hints.style.top = newTop + "px"; |
|
hints.style.left = (left + startScroll.left - curScroll.left) + "px"; |
|
}); |
|
|
|
CodeMirror.on(hints, "dblclick", function (e) { |
|
var t = getHintElement(hints, e.target || e.srcElement); |
|
if (t && t.hintId != null) { |
|
widget.changeActive(t.hintId); |
|
widget.pick(); |
|
} |
|
}); |
|
|
|
CodeMirror.on(hints, "click", function (e) { |
|
var t = getHintElement(hints, e.target || e.srcElement); |
|
if (t && t.hintId != null) { |
|
widget.changeActive(t.hintId); |
|
if (completion.options.completeOnSingleClick) widget.pick(); |
|
} |
|
}); |
|
|
|
CodeMirror.on(hints, "mousedown", function () { |
|
setTimeout(function () { |
|
cm.focus(); |
|
}, 20); |
|
}); |
|
|
|
CodeMirror.signal(data, "select", completions[0], hints.firstChild); |
|
return true; |
|
} |
|
|
|
Widget.prototype = { |
|
close: function () { |
|
if (this.completion.widget != this) return; |
|
this.completion.widget = null; |
|
this.hints.parentNode.removeChild(this.hints); |
|
this.completion.cm.removeKeyMap(this.keyMap); |
|
|
|
var cm = this.completion.cm; |
|
if (this.completion.options.closeOnUnfocus) { |
|
cm.off("blur", this.onBlur); |
|
cm.off("focus", this.onFocus); |
|
} |
|
cm.off("scroll", this.onScroll); |
|
}, |
|
|
|
disable: function () { |
|
this.completion.cm.removeKeyMap(this.keyMap); |
|
var widget = this; |
|
this.keyMap = { |
|
Enter: function () { |
|
widget.picked = true; |
|
} |
|
}; |
|
this.completion.cm.addKeyMap(this.keyMap); |
|
}, |
|
|
|
pick: function () { |
|
this.completion.pick(this.data, this.selectedHint); |
|
}, |
|
|
|
changeActive: function (i, avoidWrap) { |
|
if (i >= this.data.list.length) |
|
i = avoidWrap ? this.data.list.length - 1 : 0; |
|
else if (i < 0) |
|
i = avoidWrap ? 0 : this.data.list.length - 1; |
|
if (this.selectedHint == i) return; |
|
var node = this.hints.childNodes[this.selectedHint]; |
|
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); |
|
node = this.hints.childNodes[this.selectedHint = i]; |
|
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; |
|
if (node.offsetTop < this.hints.scrollTop) |
|
this.hints.scrollTop = node.offsetTop - 3; |
|
else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) |
|
this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; |
|
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); |
|
}, |
|
|
|
screenAmount: function () { |
|
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; |
|
} |
|
}; |
|
|
|
CodeMirror.registerHelper("hint", "auto", function (cm, options) { |
|
var helpers = cm.getHelpers(cm.getCursor(), "hint"), words; |
|
if (helpers.length) { |
|
for (var i = 0; i < helpers.length; i++) { |
|
var cur = helpers[i](cm, options); |
|
if (cur && cur.list.length) return cur; |
|
} |
|
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { |
|
if (words) return CodeMirror.hint.fromList(cm, {words: words}); |
|
} else if (CodeMirror.hint.anyword) { |
|
return CodeMirror.hint.anyword(cm, options); |
|
} |
|
}); |
|
|
|
CodeMirror.registerHelper("hint", "fromList", function (cm, options) { |
|
var cur = cm.getCursor(), token = cm.getTokenAt(cur); |
|
var found = []; |
|
for (var i = 0; i < options.words.length; i++) { |
|
var word = options.words[i]; |
|
if (word.slice(0, token.string.length) == token.string) |
|
found.push(word); |
|
} |
|
|
|
if (found.length) return { |
|
list: found, |
|
from: CodeMirror.Pos(cur.line, token.start), |
|
to: CodeMirror.Pos(cur.line, token.end) |
|
}; |
|
}); |
|
|
|
CodeMirror.commands.autocomplete = CodeMirror.showHint; |
|
|
|
var defaultOptions = { |
|
hint: CodeMirror.hint.auto, |
|
completeSingle: true, |
|
alignWithWord: true, |
|
closeCharacters: /[\s()\[\]{};:>,]/, |
|
closeOnUnfocus: true, |
|
completeOnSingleClick: true, |
|
container: null, |
|
customKeys: null, |
|
extraKeys: null |
|
}; |
|
|
|
CodeMirror.defineOption("hintOptions", null); |
|
});// CodeMirror, copyright (c) by Marijn Haverbeke and others |
|
// Distributed under an MIT license: http://codemirror.net/LICENSE |
|
|
|
// This is CodeMirror (http://codemirror.net), a code editor |
|
// implemented in JavaScript on top of the browser's DOM. |
|
// |
|
// You can find some technical background for some of the code below |
|
// at http://marijnhaverbeke.nl/blog/#cm-internals . |
|
|
|
(function(mod) { |
|
this.CodeMirror = mod(); |
|
})(function() { |
|
"use strict"; |
|
|
|
// BROWSER SNIFFING |
|
|
|
// Kludges for bugs and behavior differences that can't be feature |
|
// detected are enabled based on userAgent etc sniffing. |
|
|
|
var gecko = /gecko\/\d/i.test(navigator.userAgent); |
|
var ie_upto10 = /MSIE \d/.test(navigator.userAgent); |
|
var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); |
|
var ie = ie_upto10 || ie_11up; |
|
var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); |
|
var webkit = /WebKit\//.test(navigator.userAgent); |
|
var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); |
|
var chrome = /Chrome\//.test(navigator.userAgent); |
|
var presto = /Opera\//.test(navigator.userAgent); |
|
var safari = /Apple Computer/.test(navigator.vendor); |
|
var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); |
|
var phantom = /PhantomJS/.test(navigator.userAgent); |
|
|
|
var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); |
|
// This is woefully incomplete. Suggestions for alternative methods welcome. |
|
var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); |
|
var mac = ios || /Mac/.test(navigator.platform); |
|
var windows = /win/i.test(navigator.platform); |
|
|
|
var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); |
|
if (presto_version) presto_version = Number(presto_version[1]); |
|
if (presto_version && presto_version >= 15) { presto = false; webkit = true; } |
|
// Some browsers use the wrong event properties to signal cmd/ctrl on OS X |
|
var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); |
|
var captureRightClick = gecko || (ie && ie_version >= 9); |
|
|
|
// Optimize some code when these features are not used. |
|
var sawReadOnlySpans = false, sawCollapsedSpans = false; |
|
|
|
// EDITOR CONSTRUCTOR |
|
|
|
// A CodeMirror instance represents an editor. This is the object |
|
// that user code is usually dealing with. |
|
|
|
function CodeMirror(place, options) { |
|
if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); |
|
|
|
this.options = options = options ? copyObj(options) : {}; |
|
// Determine effective options based on given values and defaults. |
|
copyObj(defaults, options, false); |
|
setGuttersForLineNumbers(options); |
|
|
|
var doc = options.value; |
|
if (typeof doc == "string") doc = new Doc(doc, options.mode); |
|
this.doc = doc; |
|
|
|
var input = new CodeMirror.inputStyles[options.inputStyle](this); |
|
var display = this.display = new Display(place, doc, input); |
|
display.wrapper.CodeMirror = this; |
|
updateGutters(this); |
|
themeChanged(this); |
|
if (options.lineWrapping) |
|
this.display.wrapper.className += " CodeMirror-wrap"; |
|
if (options.autofocus && !mobile) display.input.focus(); |
|
initScrollbars(this); |
|
|
|
this.state = { |
|
keyMaps: [], // stores maps added by addKeyMap |
|
overlays: [], // highlighting overlays, as added by addOverlay |
|
modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info |
|
overwrite: false, |
|
delayingBlurEvent: false, |
|
focused: false, |
|
suppressEdits: false, // used to disable editing during key handlers when in readOnly mode |
|
pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll |
|
draggingText: false, |
|
highlight: new Delayed(), // stores highlight worker timeout |
|
keySeq: null, // Unfinished key sequence |
|
specialChars: null |
|
}; |
|
|
|
var cm = this; |
|
|
|
// Override magic textarea content restore that IE sometimes does |
|
// on our hidden textarea on reload |
|
if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20); |
|
|
|
registerEventHandlers(this); |
|
ensureGlobalHandlers(); |
|
|
|
startOperation(this); |
|
this.curOp.forceUpdate = true; |
|
attachDoc(this, doc); |
|
|
|
if ((options.autofocus && !mobile) || cm.hasFocus()) |
|
setTimeout(bind(onFocus, this), 20); |
|
else |
|
onBlur(this); |
|
|
|
for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) |
|
optionHandlers[opt](this, options[opt], Init); |
|
maybeUpdateLineNumberWidth(this); |
|
if (options.finishInit) options.finishInit(this); |
|
for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); |
|
endOperation(this); |
|
// Suppress optimizelegibility in Webkit, since it breaks text |
|
// measuring on line wrapping boundaries. |
|
if (webkit && options.lineWrapping && |
|
getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") |
|
display.lineDiv.style.textRendering = "auto"; |
|
} |
|
|
|
// DISPLAY CONSTRUCTOR |
|
|
|
// The display handles the DOM integration, both for input reading |
|
// and content drawing. It holds references to DOM nodes and |
|
// display-related state. |
|
|
|
function Display(place, doc, input) { |
|
var d = this; |
|
this.input = input; |
|
|
|
// Covers bottom-right square when both scrollbars are present. |
|
d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); |
|
d.scrollbarFiller.setAttribute("cm-not-content", "true"); |
|
// Covers bottom of gutter when coverGutterNextToScrollbar is on |
|
// and h scrollbar is present. |
|
d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); |
|
d.gutterFiller.setAttribute("cm-not-content", "true"); |
|
// Will contain the actual code, positioned to cover the viewport. |
|
d.lineDiv = elt("div", null, "CodeMirror-code"); |
|
// Elements are added to these to represent selection and cursors. |
|
d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); |
|
d.cursorDiv = elt("div", null, "CodeMirror-cursors"); |
|
// A visibility: hidden element used to find the size of things. |
|
d.measure = elt("div", null, "CodeMirror-measure"); |
|
// When lines outside of the viewport are measured, they are drawn in this. |
|
d.lineMeasure = elt("div", null, "CodeMirror-measure"); |
|
// Wraps everything that needs to exist inside the vertically-padded coordinate system |
|
d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], |
|
null, "position: relative; outline: none"); |
|
// Moved around its parent to cover visible view. |
|
d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); |
|
// Set to the height of the document, allowing scrolling. |
|
d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); |
|
d.sizerWidth = null; |
|
// Behavior of elts with overflow: auto and padding is |
|
// inconsistent across browsers. This is used to ensure the |
|
// scrollable area is big enough. |
|
d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); |
|
// Will contain the gutters, if any. |
|
d.gutters = elt("div", null, "CodeMirror-gutters"); |
|
d.lineGutter = null; |
|
// Actual scrollable element. |
|
d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); |
|
d.scroller.setAttribute("tabIndex", "-1"); |
|
// The element in which the editor lives. |
|
d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); |
|
|
|
// Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) |
|
if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } |
|
if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; |
|
|
|
if (place) { |
|
if (place.appendChild) place.appendChild(d.wrapper); |
|
else place(d.wrapper); |
|
} |
|
|
|
// Current rendered range (may be bigger than the view window). |
|
d.viewFrom = d.viewTo = doc.first; |
|
d.reportedViewFrom = d.reportedViewTo = doc.first; |
|
// Information about the rendered lines. |
|
d.view = []; |
|
d.renderedView = null; |
|
// Holds info about a single rendered line when it was rendered |
|
// for measurement, while not in view. |
|
d.externalMeasured = null; |
|
// Empty space (in pixels) above the view |
|
d.viewOffset = 0; |
|
d.lastWrapHeight = d.lastWrapWidth = 0; |
|
d.updateLineNumbers = null; |
|
|
|
d.nativeBarWidth = d.barHeight = d.barWidth = 0; |
|
d.scrollbarsClipped = false; |
|
|
|
// Used to only resize the line number gutter when necessary (when |
|
// the amount of lines crosses a boundary that makes its width change) |
|
d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; |
|
// Set to true when a non-horizontal-scrolling line widget is |
|
// added. As an optimization, line widget aligning is skipped when |
|
// this is false. |
|
d.alignWidgets = false; |
|
|
|
d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; |
|
|
|
// Tracks the maximum line length so that the horizontal scrollbar |
|
// can be kept static when scrolling. |
|
d.maxLine = null; |
|
d.maxLineLength = 0; |
|
d.maxLineChanged = false; |
|
|
|
// Used for measuring wheel scrolling granularity |
|
d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; |
|
|
|
// True when shift is held down. |
|
d.shift = false; |
|
|
|
// Used to track whether anything happened since the context menu |
|
// was opened. |
|
d.selForContextMenu = null; |
|
|
|
d.activeTouch = null; |
|
|
|
input.init(d); |
|
} |
|
|
|
// STATE UPDATES |
|
|
|
// Used to get the editor into a consistent state again when options change. |
|
|
|
function loadMode(cm) { |
|
cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); |
|
resetModeState(cm); |
|
} |
|
|
|
function resetModeState(cm) { |
|
cm.doc.iter(function(line) { |
|
if (line.stateAfter) line.stateAfter = null; |
|
if (line.styles) line.styles = null; |
|
}); |
|
cm.doc.frontier = cm.doc.first; |
|
startWorker(cm, 100); |
|
cm.state.modeGen++; |
|
if (cm.curOp) regChange(cm); |
|
} |
|
|
|
function wrappingChanged(cm) { |
|
if (cm.options.lineWrapping) { |
|
addClass(cm.display.wrapper, "CodeMirror-wrap"); |
|
cm.display.sizer.style.minWidth = ""; |
|
cm.display.sizerWidth = null; |
|
} else { |
|
rmClass(cm.display.wrapper, "CodeMirror-wrap"); |
|
findMaxLine(cm); |
|
} |
|
estimateLineHeights(cm); |
|
regChange(cm); |
|
clearCaches(cm); |
|
setTimeout(function(){updateScrollbars(cm);}, 100); |
|
} |
|
|
|
// Returns a function that estimates the height of a line, to use as |
|
// first approximation until the line becomes visible (and is thus |
|
// properly measurable). |
|
function estimateHeight(cm) { |
|
var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; |
|
var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); |
|
return function(line) { |
|
if (lineIsHidden(cm.doc, line)) return 0; |
|
|
|
var widgetsHeight = 0; |
|
if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { |
|
if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; |
|
} |
|
|
|
if (wrapping) |
|
return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; |
|
else |
|
return widgetsHeight + th; |
|
}; |
|
} |
|
|
|
function estimateLineHeights(cm) { |
|
var doc = cm.doc, est = estimateHeight(cm); |
|
doc.iter(function(line) { |
|
var estHeight = est(line); |
|
if (estHeight != line.height) updateLineHeight(line, estHeight); |
|
}); |
|
} |
|
|
|
function themeChanged(cm) { |
|
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + |
|
cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); |
|
clearCaches(cm); |
|
} |
|
|
|
function guttersChanged(cm) { |
|
updateGutters(cm); |
|
regChange(cm); |
|
setTimeout(function(){alignHorizontally(cm);}, 20); |
|
} |
|
|
|
// Rebuild the gutter elements, ensure the margin to the left of the |
|
// code matches their width. |
|
function updateGutters(cm) { |
|
var gutters = cm.display.gutters, specs = cm.options.gutters; |
|
removeChildren(gutters); |
|
for (var i = 0; i < specs.length; ++i) { |
|
var gutterClass = specs[i]; |
|
var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); |
|
if (gutterClass == "CodeMirror-linenumbers") { |
|
cm.display.lineGutter = gElt; |
|
gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; |
|
} |
|
} |
|
gutters.style.display = i ? "" : "none"; |
|
updateGutterSpace(cm); |
|
} |
|
|
|
function updateGutterSpace(cm) { |
|
var width = cm.display.gutters.offsetWidth; |
|
cm.display.sizer.style.marginLeft = width + "px"; |
|
} |
|
|
|
// Compute the character length of a line, taking into account |
|
// collapsed ranges (see markText) that might hide parts, and join |
|
// other lines onto it. |
|
function lineLength(line) { |
|
if (line.height == 0) return 0; |
|
var len = line.text.length, merged, cur = line; |
|
while (merged = collapsedSpanAtStart(cur)) { |
|
var found = merged.find(0, true); |
|
cur = found.from.line; |
|
len += found.from.ch - found.to.ch; |
|
} |
|
cur = line; |
|
while (merged = collapsedSpanAtEnd(cur)) { |
|
var found = merged.find(0, true); |
|
len -= cur.text.length - found.from.ch; |
|
cur = found.to.line; |
|
len += cur.text.length - found.to.ch; |
|
} |
|
return len; |
|
} |
|
|
|
// Find the longest line in the document. |
|
function findMaxLine(cm) { |
|
var d = cm.display, doc = cm.doc; |
|
d.maxLine = getLine(doc, doc.first); |
|
d.maxLineLength = lineLength(d.maxLine); |
|
d.maxLineChanged = true; |
|
doc.iter(function(line) { |
|
var len = lineLength(line); |
|
if (len > d.maxLineLength) { |
|
d.maxLineLength = len; |
|
d.maxLine = line; |
|
} |
|
}); |
|
} |
|
|
|
// Make sure the gutters options contains the element |
|
// "CodeMirror-linenumbers" when the lineNumbers option is true. |
|
function setGuttersForLineNumbers(options) { |
|
var found = indexOf(options.gutters, "CodeMirror-linenumbers"); |
|
if (found == -1 && options.lineNumbers) { |
|
options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); |
|
} else if (found > -1 && !options.lineNumbers) { |
|
options.gutters = options.gutters.slice(0); |
|
options.gutters.splice(found, 1); |
|
} |
|
} |
|
|
|
// SCROLLBARS |
|
|
|
// Prepare DOM reads needed to update the scrollbars. Done in one |
|
// shot to minimize update/measure roundtrips. |
|
function measureForScrollbars(cm) { |
|
var d = cm.display, gutterW = d.gutters.offsetWidth; |
|
var docH = Math.round(cm.doc.height + paddingVert(cm.display)); |
|
return { |
|
clientHeight: d.scroller.clientHeight, |
|
viewHeight: d.wrapper.clientHeight, |
|
scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, |
|
viewWidth: d.wrapper.clientWidth, |
|
barLeft: cm.options.fixedGutter ? gutterW : 0, |
|
docHeight: docH, |
|
scrollHeight: docH + scrollGap(cm) + d.barHeight, |
|
nativeBarWidth: d.nativeBarWidth, |
|
gutterWidth: gutterW |
|
}; |
|
} |
|
|
|
function NativeScrollbars(place, scroll, cm) { |
|
this.cm = cm; |
|
var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); |
|
var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); |
|
place(vert); place(horiz); |
|
|
|
on(vert, "scroll", function() { |
|
if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); |
|
}); |
|
on(horiz, "scroll", function() { |
|
if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); |
|
}); |
|
|
|
this.checkedOverlay = false; |
|
// Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). |
|
if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; |
|
} |
|
|
|
NativeScrollbars.prototype = copyObj({ |
|
update: function(measure) { |
|
var needsH = measure.scrollWidth > measure.clientWidth + 1; |
|
var needsV = measure.scrollHeight > measure.clientHeight + 1; |
|
var sWidth = measure.nativeBarWidth; |
|
|
|
if (needsV) { |
|
this.vert.style.display = "block"; |
|
this.vert.style.bottom = needsH ? sWidth + "px" : "0"; |
|
var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); |
|
// A bug in IE8 can cause this value to be negative, so guard it. |
|
this.vert.firstChild.style.height = |
|
Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; |
|
} else { |
|
this.vert.style.display = ""; |
|
this.vert.firstChild.style.height = "0"; |
|
} |
|
|
|
if (needsH) { |
|
this.horiz.style.display = "block"; |
|
this.horiz.style.right = needsV ? sWidth + "px" : "0"; |
|
this.horiz.style.left = measure.barLeft + "px"; |
|
var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); |
|
this.horiz.firstChild.style.width = |
|
(measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; |
|
} else { |
|
this.horiz.style.display = ""; |
|
this.horiz.firstChild.style.width = "0"; |
|
} |
|
|
|
if (!this.checkedOverlay && measure.clientHeight > 0) { |
|
if (sWidth == 0) this.overlayHack(); |
|
this.checkedOverlay = true; |
|
} |
|
|
|
return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; |
|
}, |
|
setScrollLeft: function(pos) { |
|
if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; |
|
}, |
|
setScrollTop: function(pos) { |
|
if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; |
|
}, |
|
overlayHack: function() { |
|
var w = mac && !mac_geMountainLion ? "12px" : "18px"; |
|
this.horiz.style.minHeight = this.vert.style.minWidth = w; |
|
var self = this; |
|
var barMouseDown = function(e) { |
|
if (e_target(e) != self.vert && e_target(e) != self.horiz) |
|
operation(self.cm, onMouseDown)(e); |
|
}; |
|
on(this.vert, "mousedown", barMouseDown); |
|
on(this.horiz, "mousedown", barMouseDown); |
|
}, |
|
clear: function() { |
|
var parent = this.horiz.parentNode; |
|
parent.removeChild(this.horiz); |
|
parent.removeChild(this.vert); |
|
} |
|
}, NativeScrollbars.prototype); |
|
|
|
function NullScrollbars() {} |
|
|
|
NullScrollbars.prototype = copyObj({ |
|
update: function() { return {bottom: 0, right: 0}; }, |
|
setScrollLeft: function() {}, |
|
setScrollTop: function() {}, |
|
clear: function() {} |
|
}, NullScrollbars.prototype); |
|
|
|
CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; |
|
|
|
function initScrollbars(cm) { |
|
if (cm.display.scrollbars) { |
|
cm.display.scrollbars.clear(); |
|
if (cm.display.scrollbars.addClass) |
|
rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); |
|
} |
|
|
|
cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { |
|
cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); |
|
// Prevent clicks in the scrollbars from killing focus |
|
on(node, "mousedown", function() { |
|
if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0); |
|
}); |
|
node.setAttribute("cm-not-content", "true"); |
|
}, function(pos, axis) { |
|
if (axis == "horizontal") setScrollLeft(cm, pos); |
|
else setScrollTop(cm, pos); |
|
}, cm); |
|
if (cm.display.scrollbars.addClass) |
|
addClass(cm.display.wrapper, cm.display.scrollbars.addClass); |
|
} |
|
|
|
function updateScrollbars(cm, measure) { |
|
if (!measure) measure = measureForScrollbars(cm); |
|
var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; |
|
updateScrollbarsInner(cm, measure); |
|
for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { |
|
if (startWidth != cm.display.barWidth && cm.options.lineWrapping) |
|
updateHeightsInViewport(cm); |
|
updateScrollbarsInner(cm, measureForScrollbars(cm)); |
|
startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; |
|
} |
|
} |
|
|
|
// Re-synchronize the fake scrollbars with the actual size of the |
|
// content. |
|
function updateScrollbarsInner(cm, measure) { |
|
var d = cm.display; |
|
var sizes = d.scrollbars.update(measure); |
|
|
|
d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; |
|
d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; |
|
|
|
if (sizes.right && sizes.bottom) { |
|
d.scrollbarFiller.style.display = "block"; |
|
d.scrollbarFiller.style.height = sizes.bottom + "px"; |
|
d.scrollbarFiller.style.width = sizes.right + "px"; |
|
} else d.scrollbarFiller.style.display = ""; |
|
if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { |
|
d.gutterFiller.style.display = "block"; |
|
d.gutterFiller.style.height = sizes.bottom + "px"; |
|
d.gutterFiller.style.width = measure.gutterWidth + "px"; |
|
} else d.gutterFiller.style.display = ""; |
|
} |
|
|
|
// Compute the lines that are visible in a given viewport (defaults |
|
// the the current scroll position). viewport may contain top, |
|
// height, and ensure (see op.scrollToPos) properties. |
|
function visibleLines(display, doc, viewport) { |
|
var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; |
|
top = Math.floor(top - paddingTop(display)); |
|
var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; |
|
|
|
var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); |
|
// Ensure is a {from: {line, ch}, to: {line, ch}} object, and |
|
// forces those lines into the viewport (if possible). |
|
if (viewport && viewport.ensure) { |
|
var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; |
|
if (ensureFrom < from) { |
|
from = ensureFrom; |
|
to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); |
|
} else if (Math.min(ensureTo, doc.lastLine()) >= to) { |
|
from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); |
|
to = ensureTo; |
|
} |
|
} |
|
return {from: from, to: Math.max(to, from + 1)}; |
|
} |
|
|
|
// LINE NUMBERS |
|
|
|
// Re-align line numbers and gutter marks to compensate for |
|
// horizontal scrolling. |
|
function alignHorizontally(cm) { |
|
var display = cm.display, view = display.view; |
|
if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; |
|
var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; |
|
var gutterW = display.gutters.offsetWidth, left = comp + "px"; |
|
for (var i = 0; i < view.length; i++) if (!view[i].hidden) { |
|
if (cm.options.fixedGutter && view[i].gutter) |
|
view[i].gutter.style.left = left; |
|
var align = view[i].alignable; |
|
if (align) for (var j = 0; j < align.length; j++) |
|
align[j].style.left = left; |
|
} |
|
if (cm.options.fixedGutter) |
|
display.gutters.style.left = (comp + gutterW) + "px"; |
|
} |
|
|
|
// Used to ensure that the line number gutter is still the right |
|
// size for the current document size. Returns true when an update |
|
// is needed. |
|
function maybeUpdateLineNumberWidth(cm) { |
|
if (!cm.options.lineNumbers) return false; |
|
var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; |
|
if (last.length != display.lineNumChars) { |
|
var test = display.measure.appendChild(elt("div", [elt("div", last)], |
|
"CodeMirror-linenumber CodeMirror-gutter-elt")); |
|
var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; |
|
display.lineGutter.style.width = ""; |
|
display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; |
|
display.lineNumWidth = display.lineNumInnerWidth + padding; |
|
display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; |
|
display.lineGutter.style.width = display.lineNumWidth + "px"; |
|
updateGutterSpace(cm); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
function lineNumberFor(options, i) { |
|
return String(options.lineNumberFormatter(i + options.firstLineNumber)); |
|
} |
|
|
|
// Computes display.scroller.scrollLeft + display.gutters.offsetWidth, |
|
// but using getBoundingClientRect to get a sub-pixel-accurate |
|
// result. |
|
function compensateForHScroll(display) { |
|
return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; |
|
} |
|
|
|
// DISPLAY DRAWING |
|
|
|
function DisplayUpdate(cm, viewport, force) { |
|
var display = cm.display; |
|
|
|
this.viewport = viewport; |
|
// Store some values that we'll need later (but don't want to force a relayout for) |
|
this.visible = visibleLines(display, cm.doc, viewport); |
|
this.editorIsHidden = !display.wrapper.offsetWidth; |
|
this.wrapperHeight = display.wrapper.clientHeight; |
|
this.wrapperWidth = display.wrapper.clientWidth; |
|
this.oldDisplayWidth = displayWidth(cm); |
|
this.force = force; |
|
this.dims = getDimensions(cm); |
|
this.events = []; |
|
} |
|
|
|
DisplayUpdate.prototype.signal = function(emitter, type) { |
|
if (hasHandler(emitter, type)) |
|
this.events.push(arguments); |
|
}; |
|
DisplayUpdate.prototype.finish = function() { |
|
for (var i = 0; i < this.events.length; i++) |
|
signal.apply(null, this.events[i]); |
|
}; |
|
|
|
function maybeClipScrollbars(cm) { |
|
var display = cm.display; |
|
if (!display.scrollbarsClipped && display.scroller.offsetWidth) { |
|
display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; |
|
display.heightForcer.style.height = scrollGap(cm) + "px"; |
|
display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; |
|
display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; |
|
display.scrollbarsClipped = true; |
|
} |
|
} |
|
|
|
// Does the actual updating of the line display. Bails out |
|
// (returning false) when there is nothing to be done and forced is |
|
// false. |
|
function updateDisplayIfNeeded(cm, update) { |
|
var display = cm.display, doc = cm.doc; |
|
|
|
if (update.editorIsHidden) { |
|
resetView(cm); |
|
return false; |
|
} |
|
|
|
// Bail out if the visible area is already rendered and nothing changed. |
|
if (!update.force && |
|
update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && |
|
(display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && |
|
display.renderedView == display.view && countDirtyView(cm) == 0) |
|
return false; |
|
|
|
if (maybeUpdateLineNumberWidth(cm)) { |
|
resetView(cm); |
|
update.dims = getDimensions(cm); |
|
} |
|
|
|
// Compute a suitable new viewport (from & to) |
|
var end = doc.first + doc.size; |
|
var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); |
|
var to = Math.min(end, update.visible.to + cm.options.viewportMargin); |
|
if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); |
|
if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); |
|
if (sawCollapsedSpans) { |
|
from = visualLineNo(cm.doc, from); |
|
to = visualLineEndNo(cm.doc, to); |
|
} |
|
|
|
var different = from != display.viewFrom || to != display.viewTo || |
|
display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; |
|
adjustView(cm, from, to); |
|
|
|
display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); |
|
// Position the mover div to align with the current scroll position |
|
cm.display.mover.style.top = display.viewOffset + "px"; |
|
|
|
var toUpdate = countDirtyView(cm); |
|
if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && |
|
(display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) |
|
return false; |
|
|
|
// For big changes, we hide the enclosing element during the |
|
// update, since that speeds up the operations on most browsers. |
|
var focused = activeElt(); |
|
if (toUpdate > 4) display.lineDiv.style.display = "none"; |
|
patchDisplay(cm, display.updateLineNumbers, update.dims); |
|
if (toUpdate > 4) display.lineDiv.style.display = ""; |
|
display.renderedView = display.view; |
|
// There might have been a widget with a focused element that got |
|
// hidden or updated, if so re-focus it. |
|
if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); |
|
|
|
// Prevent selection and cursors from interfering with the scroll |
|
// width and height. |
|
removeChildren(display.cursorDiv); |
|
removeChildren(display.selectionDiv); |
|
display.gutters.style.height = 0; |
|
|
|
if (different) { |
|
display.lastWrapHeight = update.wrapperHeight; |
|
display.lastWrapWidth = update.wrapperWidth; |
|
startWorker(cm, 400); |
|
} |
|
|
|
display.updateLineNumbers = null; |
|
|
|
return true; |
|
} |
|
|
|
function postUpdateDisplay(cm, update) { |
|
var viewport = update.viewport; |
|
for (var first = true;; first = false) { |
|
if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { |
|
// Clip forced viewport to actual scrollable area. |
|
if (viewport && viewport.top != null) |
|
viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; |
|
// Updated line heights might result in the drawn area not |
|
// actually covering the viewport. Keep looping until it does. |
|
update.visible = visibleLines(cm.display, cm.doc, viewport); |
|
if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) |
|
break; |
|
} |
|
if (!updateDisplayIfNeeded(cm, update)) break; |
|
updateHeightsInViewport(cm); |
|
var barMeasure = measureForScrollbars(cm); |
|
updateSelection(cm); |
|
setDocumentHeight(cm, barMeasure); |
|
updateScrollbars(cm, barMeasure); |
|
} |
|
|
|
update.signal(cm, "update", cm); |
|
if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { |
|
update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); |
|
cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; |
|
} |
|
} |
|
|
|
function updateDisplaySimple(cm, viewport) { |
|
var update = new DisplayUpdate(cm, viewport); |
|
if (updateDisplayIfNeeded(cm, update)) { |
|
updateHeightsInViewport(cm); |
|
postUpdateDisplay(cm, update); |
|
var barMeasure = measureForScrollbars(cm); |
|
updateSelection(cm); |
|
setDocumentHeight(cm, barMeasure); |
|
updateScrollbars(cm, barMeasure); |
|
update.finish(); |
|
} |
|
} |
|
|
|
function setDocumentHeight(cm, measure) { |
|
cm.display.sizer.style.minHeight = measure.docHeight + "px"; |
|
var total = measure.docHeight + cm.display.barHeight; |
|
cm.display.heightForcer.style.top = total + "px"; |
|
cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px"; |
|
} |
|
|
|
// Read the actual heights of the rendered lines, and update their |
|
// stored heights to match. |
|
function updateHeightsInViewport(cm) { |
|
var display = cm.display; |
|
var prevBottom = display.lineDiv.offsetTop; |
|
for (var i = 0; i < display.view.length; i++) { |
|
var cur = display.view[i], height; |
|
if (cur.hidden) continue; |
|
if (ie && ie_version < 8) { |
|
var bot = cur.node.offsetTop + cur.node.offsetHeight; |
|
height = bot - prevBottom; |
|
prevBottom = bot; |
|
} else { |
|
var box = cur.node.getBoundingClientRect(); |
|
height = box.bottom - box.top; |
|
} |
|
var diff = cur.line.height - height; |
|
if (height < 2) height = textHeight(display); |
|
if (diff > .001 || diff < -.001) { |
|
updateLineHeight(cur.line, height); |
|
updateWidgetHeight(cur.line); |
|
if (cur.rest) for (var j = 0; j < cur.rest.length; j++) |
|
updateWidgetHeight(cur.rest[j]); |
|
} |
|
} |
|
} |
|
|
|
// Read and store the height of line widgets associated with the |
|
// given line. |
|
function updateWidgetHeight(line) { |
|
if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) |
|
line.widgets[i].height = line.widgets[i].node.offsetHeight; |
|
} |
|
|
|
// Do a bulk-read of the DOM positions and sizes needed to draw the |
|
// view, so that we don't interleave reading and writing to the DOM. |
|
function getDimensions(cm) { |
|
var d = cm.display, left = {}, width = {}; |
|
var gutterLeft = d.gutters.clientLeft; |
|
for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { |
|
left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; |
|
width[cm.options.gutters[i]] = n.clientWidth; |
|
} |
|
return {fixedPos: compensateForHScroll(d), |
|
gutterTotalWidth: d.gutters.offsetWidth, |
|
gutterLeft: left, |
|
gutterWidth: width, |
|
wrapperWidth: d.wrapper.clientWidth}; |
|
} |
|
|
|
// Sync the actual display DOM structure with display.view, removing |
|
// nodes for lines that are no longer in view, and creating the ones |
|
// that are not there yet, and updating the ones that are out of |
|
// date. |
|
function patchDisplay(cm, updateNumbersFrom, dims) { |
|
var display = cm.display, lineNumbers = cm.options.lineNumbers; |
|
var container = display.lineDiv, cur = container.firstChild; |
|
|
|
function rm(node) { |
|
var next = node.nextSibling; |
|
// Works around a throw-scroll bug in OS X Webkit |
|
if (webkit && mac && cm.display.currentWheelTarget == node) |
|
node.style.display = "none"; |
|
else |
|
node.parentNode.removeChild(node); |
|
return next; |
|
} |
|
|
|
var view = display.view, lineN = display.viewFrom; |
|
// Loop over the elements in the view, syncing cur (the DOM nodes |
|
// in display.lineDiv) with the view as we go. |
|
for (var i = 0; i < view.length; i++) { |
|
var lineView = view[i]; |
|
if (lineView.hidden) { |
|
} else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet |
|
var node = buildLineElement(cm, lineView, lineN, dims); |
|
container.insertBefore(node, cur); |
|
} else { // Already drawn |
|
while (cur != lineView.node) cur = rm(cur); |
|
var updateNumber = lineNumbers && updateNumbersFrom != null && |
|
updateNumbersFrom <= lineN && lineView.lineNumber; |
|
if (lineView.changes) { |
|
if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; |
|
updateLineForChanges(cm, lineView, lineN, dims); |
|
} |
|
if (updateNumber) { |
|
removeChildren(lineView.lineNumber); |
|
lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); |
|
} |
|
cur = lineView.node.nextSibling; |
|
} |
|
lineN += lineView.size; |
|
} |
|
while (cur) cur = rm(cur); |
|
} |
|
|
|
// When an aspect of a line changes, a string is added to |
|
// lineView.changes. This updates the relevant part of the line's |
|
// DOM structure. |
|
function updateLineForChanges(cm, lineView, lineN, dims) { |
|
for (var j = 0; j < lineView.changes.length; j++) { |
|
var type = lineView.changes[j]; |
|
if (type == "text") updateLineText(cm, lineView); |
|
else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); |
|
else if (type == "class") updateLineClasses(lineView); |
|
else if (type == "widget") updateLineWidgets(cm, lineView, dims); |
|
} |
|
lineView.changes = null; |
|
} |
|
|
|
// Lines with gutter elements, widgets or a background class need to |
|
// be wrapped, and have the extra elements added to the wrapper div |
|
function ensureLineWrapped(lineView) { |
|
if (lineView.node == lineView.text) { |
|
lineView.node = elt("div", null, null, "position: relative"); |
|
if (lineView.text.parentNode) |
|
lineView.text.parentNode.replaceChild(lineView.node, lineView.text); |
|
lineView.node.appendChild(lineView.text); |
|
if (ie && ie_version < 8) lineView.node.style.zIndex = 2; |
|
} |
|
return lineView.node; |
|
} |
|
|
|
function updateLineBackground(lineView) { |
|
var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; |
|
if (cls) cls += " CodeMirror-linebackground"; |
|
if (lineView.background) { |
|
if (cls) lineView.background.className = cls; |
|
else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } |
|
} else if (cls) { |
|
var wrap = ensureLineWrapped(lineView); |
|
lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); |
|
} |
|
} |
|
|
|
// Wrapper around buildLineContent which will reuse the structure |
|
// in display.externalMeasured when possible. |
|
function getLineContent(cm, lineView) { |
|
var ext = cm.display.externalMeasured; |
|
if (ext && ext.line == lineView.line) { |
|
cm.display.externalMeasured = null; |
|
lineView.measure = ext.measure; |
|
return ext.built; |
|
} |
|
return buildLineContent(cm, lineView); |
|
} |
|
|
|
// Redraw the line's text. Interacts with the background and text |
|
// classes because the mode may output tokens that influence these |
|
// classes. |
|
function updateLineText(cm, lineView) { |
|
var cls = lineView.text.className; |
|
var built = getLineContent(cm, lineView); |
|
if (lineView.text == lineView.node) lineView.node = built.pre; |
|
lineView.text.parentNode.replaceChild(built.pre, lineView.text); |
|
lineView.text = built.pre; |
|
if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { |
|
lineView.bgClass = built.bgClass; |
|
lineView.textClass = built.textClass; |
|
updateLineClasses(lineView); |
|
} else if (cls) { |
|
lineView.text.className = cls; |
|
} |
|
} |
|
|
|
function updateLineClasses(lineView) { |
|
updateLineBackground(lineView); |
|
if (lineView.line.wrapClass) |
|
ensureLineWrapped(lineView).className = lineView.line.wrapClass; |
|
else if (lineView.node != lineView.text) |
|
lineView.node.className = ""; |
|
var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; |
|
lineView.text.className = textClass || ""; |
|
} |
|
|
|
function updateLineGutter(cm, lineView, lineN, dims) { |
|
if (lineView.gutter) { |
|
lineView.node.removeChild(lineView.gutter); |
|
lineView.gutter = null; |
|
} |
|
var markers = lineView.line.gutterMarkers; |
|
if (cm.options.lineNumbers || markers) { |
|
var wrap = ensureLineWrapped(lineView); |
|
var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + |
|
(cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + |
|
"px; width: " + dims.gutterTotalWidth + "px"); |
|
cm.display.input.setUneditable(gutterWrap); |
|
wrap.insertBefore(gutterWrap, lineView.text); |
|
if (lineView.line.gutterClass) |
|
gutterWrap.className += " " + lineView.line.gutterClass; |
|
if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) |
|
lineView.lineNumber = gutterWrap.appendChild( |
|
elt("div", lineNumberFor(cm.options, lineN), |
|
"CodeMirror-linenumber CodeMirror-gutter-elt", |
|
"left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " |
|
+ cm.display.lineNumInnerWidth + "px")); |
|
if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { |
|
var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; |
|
if (found) |
|
gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + |
|
dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); |
|
} |
|
} |
|
} |
|
|
|
function updateLineWidgets(cm, lineView, dims) { |
|
if (lineView.alignable) lineView.alignable = null; |
|
for (var node = lineView.node.firstChild, next; node; node = next) { |
|
var next = node.nextSibling; |
|
if (node.className == "CodeMirror-linewidget") |
|
lineView.node.removeChild(node); |
|
} |
|
insertLineWidgets(cm, lineView, dims); |
|
} |
|
|
|
// Build a line's DOM representation from scratch |
|
function buildLineElement(cm, lineView, lineN, dims) { |
|
var built = getLineContent(cm, lineView); |
|
lineView.text = lineView.node = built.pre; |
|
if (built.bgClass) lineView.bgClass = built.bgClass; |
|
if (built.textClass) lineView.textClass = built.textClass; |
|
|
|
updateLineClasses(lineView); |
|
updateLineGutter(cm, lineView, lineN, dims); |
|
insertLineWidgets(cm, lineView, dims); |
|
return lineView.node; |
|
} |
|
|
|
// A lineView may contain multiple logical lines (when merged by |
|
// collapsed spans). The widgets for all of them need to be drawn. |
|
function insertLineWidgets(cm, lineView, dims) { |
|
insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); |
|
if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) |
|
insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); |
|
} |
|
|
|
function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { |
|
if (!line.widgets) return; |
|
var wrap = ensureLineWrapped(lineView); |
|
for (var i = 0, ws = line.widgets; i < ws.length; ++i) { |
|
var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); |
|
if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); |
|
positionLineWidget(widget, node, lineView, dims); |
|
cm.display.input.setUneditable(node); |
|
if (allowAbove && widget.above) |
|
wrap.insertBefore(node, lineView.gutter || lineView.text); |
|
else |
|
wrap.appendChild(node); |
|
signalLater(widget, "redraw"); |
|
} |
|
} |
|
|
|
function positionLineWidget(widget, node, lineView, dims) { |
|
if (widget.noHScroll) { |
|
(lineView.alignable || (lineView.alignable = [])).push(node); |
|
var width = dims.wrapperWidth; |
|
node.style.left = dims.fixedPos + "px"; |
|
if (!widget.coverGutter) { |
|
width -= dims.gutterTotalWidth; |
|
node.style.paddingLeft = dims.gutterTotalWidth + "px"; |
|
} |
|
node.style.width = width + "px"; |
|
} |
|
if (widget.coverGutter) { |
|
node.style.zIndex = 5; |
|
node.style.position = "relative"; |
|
if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; |
|
} |
|
} |
|
|
|
// POSITION OBJECT |
|
|
|
// A Pos instance represents a position within the text. |
|
var Pos = CodeMirror.Pos = function(line, ch) { |
|
if (!(this instanceof Pos)) return new Pos(line, ch); |
|
this.line = line; this.ch = ch; |
|
}; |
|
|
|
// Compare two positions, return 0 if they are the same, a negative |
|
// number when a is less, and a positive number otherwise. |
|
var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; |
|
|
|
function copyPos(x) {return Pos(x.line, x.ch);} |
|
function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } |
|
function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } |
|
|
|
// INPUT HANDLING |
|
|
|
function ensureFocus(cm) { |
|
if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } |
|
} |
|
|
|
function isReadOnly(cm) { |
|
return cm.options.readOnly || cm.doc.cantEdit; |
|
} |
|
|
|
// This will be set to an array of strings when copying, so that, |
|
// when pasting, we know what kind of selections the copied text |
|
// was made out of. |
|
var lastCopied = null; |
|
|
|
function applyTextInput(cm, inserted, deleted, sel, origin) { |
|
var doc = cm.doc; |
|
cm.display.shift = false; |
|
if (!sel) sel = doc.sel; |
|
|
|
var paste = cm.state.pasteIncoming || origin == "paste"; |
|
var textLines = splitLines(inserted), multiPaste = null; |
|
// When pasing N lines into N selections, insert one line per selection |
|
if (paste && sel.ranges.length > 1) { |
|
if (lastCopied && lastCopied.join("\n") == inserted) |
|
multiPaste = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); |
|
else if (textLines.length == sel.ranges.length) |
|
multiPaste = map(textLines, function(l) { return [l]; }); |
|
} |
|
|
|
// Normal behavior is to insert the new text into every selection |
|
for (var i = sel.ranges.length - 1; i >= 0; i--) { |
|
var range = sel.ranges[i]; |
|
var from = range.from(), to = range.to(); |
|
if (range.empty()) { |
|
if (deleted && deleted > 0) // Handle deletion |
|
from = Pos(from.line, from.ch - deleted); |
|
else if (cm.state.overwrite && !paste) // Handle overwrite |
|
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); |
|
} |
|
var updateInput = cm.curOp.updateInput; |
|
var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, |
|
origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")}; |
|
makeChange(cm.doc, changeEvent); |
|
signalLater(cm, "inputRead", cm, changeEvent); |
|
} |
|
if (inserted && !paste) |
|
triggerElectric(cm, inserted); |
|
|
|
ensureCursorVisible(cm); |
|
cm.curOp.updateInput = updateInput; |
|
cm.curOp.typing = true; |
|
cm.state.pasteIncoming = cm.state.cutIncoming = false; |
|
} |
|
|
|
function handlePaste(e, cm) { |
|
var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); |
|
if (pasted) { |
|
e.preventDefault(); |
|
runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); }); |
|
return true; |
|
} |
|
} |
|
|
|
function triggerElectric(cm, inserted) { |
|
// When an 'electric' character is inserted, immediately trigger a reindent |
|
if (!cm.options.electricChars || !cm.options.smartIndent) return; |
|
var sel = cm.doc.sel; |
|
|
|
for (var i = sel.ranges.length - 1; i >= 0; i--) { |
|
var range = sel.ranges[i]; |
|
if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue; |
|
var mode = cm.getModeAt(range.head); |
|
var indented = false; |
|
if (mode.electricChars) { |
|
for (var j = 0; j < mode.electricChars.length; j++) |
|
if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { |
|
indented = indentLine(cm, range.head.line, "smart"); |
|
break; |
|
} |
|
} else if (mode.electricInput) { |
|
if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) |
|
indented = indentLine(cm, range.head.line, "smart"); |
|
} |
|
if (indented) signalLater(cm, "electricInput", cm, range.head.line); |
|
} |
|
} |
|
|
|
function copyableRanges(cm) { |
|
var text = [], ranges = []; |
|
for (var i = 0; i < cm.doc.sel.ranges.length; i++) { |
|
var line = cm.doc.sel.ranges[i].head.line; |
|
var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; |
|
ranges.push(lineRange); |
|
text.push(cm.getRange(lineRange.anchor, lineRange.head)); |
|
} |
|
return {text: text, ranges: ranges}; |
|
} |
|
|
|
function disableBrowserMagic(field) { |
|
field.setAttribute("autocorrect", "off"); |
|
field.setAttribute("autocapitalize", "off"); |
|
field.setAttribute("spellcheck", "false"); |
|
} |
|
|
|
// TEXTAREA INPUT STYLE |
|
|
|
function TextareaInput(cm) { |
|
this.cm = cm; |
|
// See input.poll and input.reset |
|
this.prevInput = ""; |
|
|
|
// Flag that indicates whether we expect input to appear real soon |
|
// now (after some event like 'keypress' or 'input') and are |
|
// polling intensively. |
|
this.pollingFast = false; |
|
// Self-resetting timeout for the poller |
|
this.polling = new Delayed(); |
|
// Tracks when input.reset has punted to just putting a short |
|
// string into the textarea instead of the full selection. |
|
this.inaccurateSelection = false; |
|
// Used to work around IE issue with selection being forgotten when focus moves away from textarea |
|
this.hasSelection = false; |
|
this.composing = null; |
|
}; |
|
|
|
function hiddenTextarea() { |
|
var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); |
|
var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); |
|
// The textarea is kept positioned near the cursor to prevent the |
|
// fact that it'll be scrolled into view on input from scrolling |
|
// our fake cursor out of view. On webkit, when wrap=off, paste is |
|
// very slow. So make the area wide instead. |
|
if (webkit) te.style.width = "1000px"; |
|
else te.setAttribute("wrap", "off"); |
|
// If border: 0; -- iOS fails to open keyboard (issue #1287) |
|
if (ios) te.style.border = "1px solid black"; |
|
disableBrowserMagic(te); |
|
return div; |
|
} |
|
|
|
TextareaInput.prototype = copyObj({ |
|
init: function(display) { |
|
var input = this, cm = this.cm; |
|
|
|
// Wraps and hides input textarea |
|
var div = this.wrapper = hiddenTextarea(); |
|
// The semihidden textarea that is focused when the editor is |
|
// focused, and receives input. |
|
var te = this.textarea = div.firstChild; |
|
display.wrapper.insertBefore(div, display.wrapper.firstChild); |
|
|
|
// Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) |
|
if (ios) te.style.width = "0px"; |
|
|
|
on(te, "input", function() { |
|
if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; |
|
input.poll(); |
|
}); |
|
|
|
on(te, "paste", function(e) { |
|
if (handlePaste(e, cm)) return true; |
|
|
|
cm.state.pasteIncoming = true; |
|
input.fastPoll(); |
|
}); |
|
|
|
function prepareCopyCut(e) { |
|
if (cm.somethingSelected()) { |
|
lastCopied = cm.getSelections(); |
|
if (input.inaccurateSelection) { |
|
input.prevInput = ""; |
|
input.inaccurateSelection = false; |
|
te.value = lastCopied.join("\n"); |
|
selectInput(te); |
|
} |
|
} else if (!cm.options.lineWiseCopyCut) { |
|
return; |
|
} else { |
|
var ranges = copyableRanges(cm); |
|
lastCopied = ranges.text; |
|
if (e.type == "cut") { |
|
cm.setSelections(ranges.ranges, null, sel_dontScroll); |
|
} else { |
|
input.prevInput = ""; |
|
te.value = ranges.text.join("\n"); |
|
selectInput(te); |
|
} |
|
} |
|
if (e.type == "cut") cm.state.cutIncoming = true; |
|
} |
|
on(te, "cut", prepareCopyCut); |
|
on(te, "copy", prepareCopyCut); |
|
|
|
on(display.scroller, "paste", function(e) { |
|
if (eventInWidget(display, e)) return; |
|
cm.state.pasteIncoming = true; |
|
input.focus(); |
|
}); |
|
|
|
// Prevent normal selection in the editor (we handle our own) |
|
on(display.lineSpace, "selectstart", function(e) { |
|
if (!eventInWidget(display, e)) e_preventDefault(e); |
|
}); |
|
|
|
on(te, "compositionstart", function() { |
|
var start = cm.getCursor("from"); |
|
input.composing = { |
|
start: start, |
|
range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) |
|
}; |
|
}); |
|
on(te, "compositionend", function() { |
|
if (input.composing) { |
|
input.poll(); |
|
input.composing.range.clear(); |
|
input.composing = null; |
|
} |
|
}); |
|
}, |
|
|
|
prepareSelection: function() { |
|
// Redraw the selection and/or cursor |
|
var cm = this.cm, display = cm.display, doc = cm.doc; |
|
var result = prepareSelection(cm); |
|
|
|
// Move the hidden textarea near the cursor to prevent scrolling artifacts |
|
if (cm.options.moveInputWithCursor) { |
|
var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); |
|
var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); |
|
result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, |
|
headPos.top + lineOff.top - wrapOff.top)); |
|
result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, |
|
headPos.left + lineOff.left - wrapOff.left)); |
|
} |
|
|
|
return result; |
|
}, |
|
|
|
showSelection: function(drawn) { |
|
var cm = this.cm, display = cm.display; |
|
removeChildrenAndAdd(display.cursorDiv, drawn.cursors); |
|
removeChildrenAndAdd(display.selectionDiv, drawn.selection); |
|
if (drawn.teTop != null) { |
|
this.wrapper.style.top = drawn.teTop + "px"; |
|
this.wrapper.style.left = drawn.teLeft + "px"; |
|
} |
|
}, |
|
|
|
// Reset the input to correspond to the selection (or to be empty, |
|
// when not typing and nothing is selected) |
|
reset: function(typing) { |
|
if (this.contextMenuPending) return; |
|
var minimal, selected, cm = this.cm, doc = cm.doc; |
|
if (cm.somethingSelected()) { |
|
this.prevInput = ""; |
|
var range = doc.sel.primary(); |
|
minimal = hasCopyEvent && |
|
(range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); |
|
var content = minimal ? "-" : selected || cm.getSelection(); |
|
this.textarea.value = content; |
|
if (cm.state.focused) selectInput(this.textarea); |
|
if (ie && ie_version >= 9) this.hasSelection = content; |
|
} else if (!typing) { |
|
this.prevInput = this.textarea.value = ""; |
|
if (ie && ie_version >= 9) this.hasSelection = null; |
|
} |
|
this.inaccurateSelection = minimal; |
|
}, |
|
|
|
getField: function() { return this.textarea; }, |
|
|
|
supportsTouch: function() { return false; }, |
|
|
|
focus: function() { |
|
if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { |
|
try { this.textarea.focus(); } |
|
catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM |
|
} |
|
}, |
|
|
|
blur: function() { this.textarea.blur(); }, |
|
|
|
resetPosition: function() { |
|
this.wrapper.style.top = this.wrapper.style.left = 0; |
|
}, |
|
|
|
receivedFocus: function() { this.slowPoll(); }, |
|
|
|
// Poll for input changes, using the normal rate of polling. This |
|
// runs as long as the editor is focused. |
|
slowPoll: function() { |
|
var input = this; |
|
if (input.pollingFast) return; |
|
input.polling.set(this.cm.options.pollInterval, function() { |
|
input.poll(); |
|
if (input.cm.state.focused) input.slowPoll(); |
|
}); |
|
}, |
|
|
|
// When an event has just come in that is likely to add or change |
|
// something in the input textarea, we poll faster, to ensure that |
|
// the change appears on the screen quickly. |
|
fastPoll: function() { |
|
var missed = false, input = this; |
|
input.pollingFast = true; |
|
function p() { |
|
var changed = input.poll(); |
|
if (!changed && !missed) {missed = true; input.polling.set(60, p);} |
|
else {input.pollingFast = false; input.slowPoll();} |
|
} |
|
input.polling.set(20, p); |
|
}, |
|
|
|
// Read input from the textarea, and update the document to match. |
|
// When something is selected, it is present in the textarea, and |
|
// selected (unless it is huge, in which case a placeholder is |
|
// used). When nothing is selected, the cursor sits after previously |
|
// seen text (can be empty), which is stored in prevInput (we must |
|
// not reset the textarea when typing, because that breaks IME). |
|
poll: function() { |
|
var cm = this.cm, input = this.textarea, prevInput = this.prevInput; |
|
// Since this is called a *lot*, try to bail out as cheaply as |
|
// possible when it is clear that nothing happened. hasSelection |
|
// will be the case when there is a lot of text in the textarea, |
|
// in which case reading its value would be expensive. |
|
if (this.contextMenuPending || !cm.state.focused || |
|
(hasSelection(input) && !prevInput) || |
|
isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) |
|
return false; |
|
|
|
var text = input.value; |
|
// If nothing changed, bail. |
|
if (text == prevInput && !cm.somethingSelected()) return false; |
|
// Work around nonsensical selection resetting in IE9/10, and |
|
// inexplicable appearance of private area unicode characters on |
|
// some key combos in Mac (#2689). |
|
if (ie && ie_version >= 9 && this.hasSelection === text || |
|
mac && /[\uf700-\uf7ff]/.test(text)) { |
|
cm.display.input.reset(); |
|
return false; |
|
} |
|
|
|
if (cm.doc.sel == cm.display.selForContextMenu) { |
|
var first = text.charCodeAt(0); |
|
if (first == 0x200b && !prevInput) prevInput = "\u200b"; |
|
if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); } |
|
} |
|
// Find the part of the input that is actually new |
|
var same = 0, l = Math.min(prevInput.length, text.length); |
|
while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; |
|
|
|
var self = this; |
|
runInOp(cm, function() { |
|
applyTextInput(cm, text.slice(same), prevInput.length - same, |
|
null, self.composing ? "*compose" : null); |
|
|
|
// Don't leave long text in the textarea, since it makes further polling slow |
|
if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = ""; |
|
else self.prevInput = text; |
|
|
|
if (self.composing) { |
|
self.composing.range.clear(); |
|
self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"), |
|
{className: "CodeMirror-composing"}); |
|
} |
|
}); |
|
return true; |
|
}, |
|
|
|
ensurePolled: function() { |
|
if (this.pollingFast && this.poll()) this.pollingFast = false; |
|
}, |
|
|
|
onKeyPress: function() { |
|
if (ie && ie_version >= 9) this.hasSelection = null; |
|
this.fastPoll(); |
|
}, |
|
|
|
onContextMenu: function(e) { |
|
var input = this, cm = input.cm, display = cm.display, te = input.textarea; |
|
var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; |
|
if (!pos || presto) return; // Opera is difficult. |
|
|
|
// Reset the current text selection only if the click is done outside of the selection |
|
// and 'resetSelectionOnContextMenu' option is true. |
|
var reset = cm.options.resetSelectionOnContextMenu; |
|
if (reset && cm.doc.sel.contains(pos) == -1) |
|
operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); |
|
|
|
var oldCSS = te.style.cssText; |
|
input.wrapper.style.position = "absolute"; |
|
te.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + |
|
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + |
|
(ie ? "rgba(255, 255, 255, .05)" : "transparent") + |
|
"; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; |
|
if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) |
|
display.input.focus(); |
|
if (webkit) window.scrollTo(null, oldScrollY); |
|
display.input.reset(); |
|
// Adds "Select all" to context menu in FF |
|
if (!cm.somethingSelected()) te.value = input.prevInput = " "; |
|
input.contextMenuPending = true; |
|
display.selForContextMenu = cm.doc.sel; |
|
clearTimeout(display.detectingSelectAll); |
|
|
|
// Select-all will be greyed out if there's nothing to select, so |
|
// this adds a zero-width space so that we can later check whether |
|
// it got selected. |
|
function prepareSelectAllHack() { |
|
if (te.selectionStart != null) { |
|
var selected = cm.somethingSelected(); |
|
var extval = "\u200b" + (selected ? te.value : ""); |
|
te.value = "\u21da"; // Used to catch context-menu undo |
|
te.value = extval; |
|
input.prevInput = selected ? "" : "\u200b"; |
|
te.selectionStart = 1; te.selectionEnd = extval.length; |
|
// Re-set this, in case some other handler touched the |
|
// selection in the meantime. |
|
display.selForContextMenu = cm.doc.sel; |
|
} |
|
} |
|
function rehide() { |
|
input.contextMenuPending = false; |
|
input.wrapper.style.position = "relative"; |
|
te.style.cssText = oldCSS; |
|
if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); |
|
|
|
// Try to detect the user choosing select-all |
|
if (te.selectionStart != null) { |
|
if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); |
|
var i = 0, poll = function() { |
|
if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && |
|
te.selectionEnd > 0 && input.prevInput == "\u200b") |
|
operation(cm, commands.selectAll)(cm); |
|
else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); |
|
else display.input.reset(); |
|
}; |
|
display.detectingSelectAll = setTimeout(poll, 200); |
|
} |
|
} |
|
|
|
if (ie && ie_version >= 9) prepareSelectAllHack(); |
|
if (captureRightClick) { |
|
e_stop(e); |
|
var mouseup = function() { |
|
off(window, "mouseup", mouseup); |
|
setTimeout(rehide, 20); |
|
}; |
|
on(window, "mouseup", mouseup); |
|
} else { |
|
setTimeout(rehide, 50); |
|
} |
|
}, |
|
|
|
setUneditable: nothing, |
|
|
|
needsContentAttribute: false |
|
}, TextareaInput.prototype); |
|
|
|
// CONTENTEDITABLE INPUT STYLE |
|
|
|
function ContentEditableInput(cm) { |
|
this.cm = cm; |
|
this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; |
|
this.polling = new Delayed(); |
|
this.gracePeriod = false; |
|
} |
|
|
|
ContentEditableInput.prototype = copyObj({ |
|
init: function(display) { |
|
var input = this, cm = input.cm; |
|
var div = input.div = display.lineDiv; |
|
div.contentEditable = "true"; |
|
disableBrowserMagic(div); |
|
|
|
on(div, "paste", function(e) { handlePaste(e, cm); }) |
|
|
|
on(div, "compositionstart", function(e) { |
|
var data = e.data; |
|
input.composing = {sel: cm.doc.sel, data: data, startData: data}; |
|
if (!data) return; |
|
var prim = cm.doc.sel.primary(); |
|
var line = cm.getLine(prim.head.line); |
|
var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length)); |
|
if (found > -1 && found <= prim.head.ch) |
|
input.composing.sel = simpleSelection(Pos(prim.head.line, found), |
|
Pos(prim.head.line, found + data.length)); |
|
}); |
|
on(div, "compositionupdate", function(e) { |
|
input.composing.data = e.data; |
|
}); |
|
on(div, "compositionend", function(e) { |
|
var ours = input.composing; |
|
if (!ours) return; |
|
if (e.data != ours.startData && !/\u200b/.test(e.data)) |
|
ours.data = e.data; |
|
// Need a small delay to prevent other code (input event, |
|
// selection polling) from doing damage when fired right after |
|
// compositionend. |
|
setTimeout(function() { |
|
if (!ours.handled) |
|
input.applyComposition(ours); |
|
if (input.composing == ours) |
|
input.composing = null; |
|
}, 50); |
|
}); |
|
|
|
on(div, "touchstart", function() { |
|
input.forceCompositionEnd(); |
|
}); |
|
|
|
on(div, "input", function() { |
|
if (input.composing) return; |
|
if (!input.pollContent()) |
|
runInOp(input.cm, function() {regChange(cm);}); |
|
}); |
|
|
|
function onCopyCut(e) { |
|
if (cm.somethingSelected()) { |
|
lastCopied = cm.getSelections(); |
|
if (e.type == "cut") cm.replaceSelection("", null, "cut"); |
|
} else if (!cm.options.lineWiseCopyCut) { |
|
return; |
|
} else { |
|
var ranges = copyableRanges(cm); |
|
lastCopied = ranges.text; |
|
if (e.type == "cut") { |
|
cm.operation(function() { |
|
cm.setSelections(ranges.ranges, 0, sel_dontScroll); |
|
cm.replaceSelection("", null, "cut"); |
|
}); |
|
} |
|
} |
|
// iOS exposes the clipboard API, but seems to discard content inserted into it |
|
if (e.clipboardData && !ios) { |
|
e.preventDefault(); |
|
e.clipboardData.clearData(); |
|
e.clipboardData.setData("text/plain", lastCopied.join("\n")); |
|
} else { |
|
// Old-fashioned briefly-focus-a-textarea hack |
|
var kludge = hiddenTextarea(), te = kludge.firstChild; |
|
cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); |
|
te.value = lastCopied.join("\n"); |
|
var hadFocus = document.activeElement; |
|
selectInput(te); |
|
setTimeout(function() { |
|
cm.display.lineSpace.removeChild(kludge); |
|
hadFocus.focus(); |
|
}, 50); |
|
} |
|
} |
|
on(div, "copy", onCopyCut); |
|
on(div, "cut", onCopyCut); |
|
}, |
|
|
|
prepareSelection: function() { |
|
var result = prepareSelection(this.cm, false); |
|
result.focus = this.cm.state.focused; |
|
return result; |
|
}, |
|
|
|
showSelection: function(info) { |
|
if (!info || !this.cm.display.view.length) return; |
|
if (info.focus) this.showPrimarySelection(); |
|
this.showMultipleSelections(info); |
|
}, |
|
|
|
showPrimarySelection: function() { |
|
var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); |
|
var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); |
|
var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); |
|
if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && |
|
cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && |
|
cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) |
|
return; |
|
|
|
var start = posToDOM(this.cm, prim.from()); |
|
var end = posToDOM(this.cm, prim.to()); |
|
if (!start && !end) return; |
|
|
|
var view = this.cm.display.view; |
|
var old = sel.rangeCount && sel.getRangeAt(0); |
|
if (!start) { |
|
start = {node: view[0].measure.map[2], offset: 0}; |
|
} else if (!end) { // FIXME dangerously hacky |
|
var measure = view[view.length - 1].measure; |
|
var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; |
|
end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; |
|
} |
|
|
|
try { var rng = range(start.node, start.offset, end.offset, end.node); } |
|
catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible |
|
if (rng) { |
|
sel.removeAllRanges(); |
|
sel.addRange(rng); |
|
if (old && sel.anchorNode == null) sel.addRange(old); |
|
else if (gecko) this.startGracePeriod(); |
|
} |
|
this.rememberSelection(); |
|
}, |
|
|
|
startGracePeriod: function() { |
|
var input = this; |
|
clearTimeout(this.gracePeriod); |
|
this.gracePeriod = setTimeout(function() { |
|
input.gracePeriod = false; |
|
if (input.selectionChanged()) |
|
input.cm.operation(function() { input.cm.curOp.selectionChanged = true; }); |
|
}, 20); |
|
}, |
|
|
|
showMultipleSelections: function(info) { |
|
removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); |
|
removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); |
|
}, |
|
|
|
rememberSelection: function() { |
|
var sel = window.getSelection(); |
|
this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; |
|
this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; |
|
}, |
|
|
|
selectionInEditor: function() { |
|
var sel = window.getSelection(); |
|
if (!sel.rangeCount) return false; |
|
var node = sel.getRangeAt(0).commonAncestorContainer; |
|
return contains(this.div, node); |
|
}, |
|
|
|
focus: function() { |
|
if (this.cm.options.readOnly != "nocursor") this.div.focus(); |
|
}, |
|
blur: function() { this.div.blur(); }, |
|
getField: function() { return this.div; }, |
|
|
|
supportsTouch: function() { return true; }, |
|
|
|
receivedFocus: function() { |
|
var input = this; |
|
if (this.selectionInEditor()) |
|
this.pollSelection(); |
|
else |
|
runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); |
|
|
|
function poll() { |
|
if (input.cm.state.focused) { |
|
input.pollSelection(); |
|
input.polling.set(input.cm.options.pollInterval, poll); |
|
} |
|
} |
|
this.polling.set(this.cm.options.pollInterval, poll); |
|
}, |
|
|
|
selectionChanged: function() { |
|
var sel = window.getSelection(); |
|
return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || |
|
sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset; |
|
}, |
|
|
|
pollSelection: function() { |
|
if (!this.composing && !this.gracePeriod && this.selectionChanged()) { |
|
var sel = window.getSelection(), cm = this.cm; |
|
this.rememberSelection(); |
|
var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); |
|
var head = domToPos(cm, sel.focusNode, sel.focusOffset); |
|
if (anchor && head) runInOp(cm, function() { |
|
setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); |
|
if (anchor.bad || head.bad) cm.curOp.selectionChanged = true; |
|
}); |
|
} |
|
}, |
|
|
|
pollContent: function() { |
|
var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); |
|
var from = sel.from(), to = sel.to(); |
|
if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false; |
|
|
|
var fromIndex; |
|
if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { |
|
var fromLine = lineNo(display.view[0].line); |
|
var fromNode = display.view[0].node; |
|
} else { |
|
var fromLine = lineNo(display.view[fromIndex].line); |
|
var fromNode = display.view[fromIndex - 1].node.nextSibling; |
|
} |
|
var toIndex = findViewIndex(cm, to.line); |
|
if (toIndex == display.view.length - 1) { |
|
var toLine = display.viewTo - 1; |
|
var toNode = display.lineDiv.lastChild; |
|
} else { |
|
var toLine = lineNo(display.view[toIndex + 1].line) - 1; |
|
var toNode = display.view[toIndex + 1].node.previousSibling; |
|
} |
|
|
|
var newText = splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); |
|
var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); |
|
while (newText.length > 1 && oldText.length > 1) { |
|
if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } |
|
else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } |
|
else break; |
|
} |
|
|
|
var cutFront = 0, cutEnd = 0; |
|
var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); |
|
while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) |
|
++cutFront; |
|
var newBot = lst(newText), oldBot = lst(oldText); |
|
var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), |
|
oldBot.length - (oldText.length == 1 ? cutFront : 0)); |
|
while (cutEnd < maxCutEnd && |
|
newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) |
|
++cutEnd; |
|
|
|
newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd); |
|
newText[0] = newText[0].slice(cutFront); |
|
|
|
var chFrom = Pos(fromLine, cutFront); |
|
var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); |
|
if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { |
|
replaceRange(cm.doc, newText, chFrom, chTo, "+input"); |
|
return true; |
|
} |
|
}, |
|
|
|
ensurePolled: function() { |
|
this.forceCompositionEnd(); |
|
}, |
|
reset: function() { |
|
this.forceCompositionEnd(); |
|
}, |
|
forceCompositionEnd: function() { |
|
if (!this.composing || this.composing.handled) return; |
|
this.applyComposition(this.composing); |
|
this.composing.handled = true; |
|
this.div.blur(); |
|
this.div.focus(); |
|
}, |
|
applyComposition: function(composing) { |
|
if (composing.data && composing.data != composing.startData) |
|
operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); |
|
}, |
|
|
|
setUneditable: function(node) { |
|
node.setAttribute("contenteditable", "false"); |
|
}, |
|
|
|
onKeyPress: function(e) { |
|
e.preventDefault(); |
|
operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); |
|
}, |
|
|
|
onContextMenu: nothing, |
|
resetPosition: nothing, |
|
|
|
needsContentAttribute: true |
|
}, ContentEditableInput.prototype); |
|
|
|
function posToDOM(cm, pos) { |
|
var view = findViewForLine(cm, pos.line); |
|
if (!view || view.hidden) return null; |
|
var line = getLine(cm.doc, pos.line); |
|
var info = mapFromLineView(view, line, pos.line); |
|
|
|
var order = getOrder(line), side = "left"; |
|
if (order) { |
|
var partPos = getBidiPartAt(order, pos.ch); |
|
side = partPos % 2 ? "right" : "left"; |
|
} |
|
var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); |
|
result.offset = result.collapse == "right" ? result.end : result.start; |
|
return result; |
|
} |
|
|
|
function badPos(pos, bad) { if (bad) pos.bad = true; return pos; } |
|
|
|
function domToPos(cm, node, offset) { |
|
var lineNode; |
|
if (node == cm.display.lineDiv) { |
|
lineNode = cm.display.lineDiv.childNodes[offset]; |
|
if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true); |
|
node = null; offset = 0; |
|
} else { |
|
for (lineNode = node;; lineNode = lineNode.parentNode) { |
|
if (!lineNode || lineNode == cm.display.lineDiv) return null; |
|
if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; |
|
} |
|
} |
|
for (var i = 0; i < cm.display.view.length; i++) { |
|
var lineView = cm.display.view[i]; |
|
if (lineView.node == lineNode) |
|
return locateNodeInLineView(lineView, node, offset); |
|
} |
|
} |
|
|
|
function locateNodeInLineView(lineView, node, offset) { |
|
var wrapper = lineView.text.firstChild, bad = false; |
|
if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true); |
|
if (node == wrapper) { |
|
bad = true; |
|
node = wrapper.childNodes[offset]; |
|
offset = 0; |
|
if (!node) { |
|
var line = lineView.rest ? lst(lineView.rest) : lineView.line; |
|
return badPos(Pos(lineNo(line), line.text.length), bad); |
|
} |
|
} |
|
|
|
var textNode = node.nodeType == 3 ? node : null, topNode = node; |
|
if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { |
|
textNode = node.firstChild; |
|
if (offset) offset = textNode.nodeValue.length; |
|
} |
|
while (topNode.parentNode != wrapper) topNode = topNode.parentNode; |
|
var measure = lineView.measure, maps = measure.maps; |
|
|
|
function find(textNode, topNode, offset) { |
|
for (var i = -1; i < (maps ? maps.length : 0); i++) { |
|
var map = i < 0 ? measure.map : maps[i]; |
|
for (var j = 0; j < map.length; j += 3) { |
|
var curNode = map[j + 2]; |
|
if (curNode == textNode || curNode == topNode) { |
|
var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); |
|
var ch = map[j] + offset; |
|
if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]; |
|
return Pos(line, ch); |
|
} |
|
} |
|
} |
|
} |
|
var found = find(textNode, topNode, offset); |
|
if (found) return badPos(found, bad); |
|
|
|
// FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems |
|
for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { |
|
found = find(after, after.firstChild, 0); |
|
if (found) |
|
return badPos(Pos(found.line, found.ch - dist), bad); |
|
else |
|
dist += after.textContent.length; |
|
} |
|
for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { |
|
found = find(before, before.firstChild, -1); |
|
if (found) |
|
return badPos(Pos(found.line, found.ch + dist), bad); |
|
else |
|
dist += after.textContent.length; |
|
} |
|
} |
|
|
|
function domTextBetween(cm, from, to, fromLine, toLine) { |
|
var text = "", closing = false; |
|
function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } |
|
function walk(node) { |
|
if (node.nodeType == 1) { |
|
var cmText = node.getAttribute("cm-text"); |
|
if (cmText != null) { |
|
if (cmText == "") cmText = node.textContent.replace(/\u200b/g, ""); |
|
text += cmText; |
|
return; |
|
} |
|
var markerID = node.getAttribute("cm-marker"), range; |
|
if (markerID) { |
|
var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); |
|
if (found.length && (range = found[0].find())) |
|
text += getBetween(cm.doc, range.from, range.to).join("\n"); |
|
return; |
|
} |
|
if (node.getAttribute("contenteditable") == "false") return; |
|
for (var i = 0; i < node.childNodes.length; i++) |
|
walk(node.childNodes[i]); |
|
if (/^(pre|div|p)$/i.test(node.nodeName)) |
|
closing = true; |
|
} else if (node.nodeType == 3) { |
|
var val = node.nodeValue; |
|
if (!val) return; |
|
if (closing) { |
|
text += "\n"; |
|
closing = false; |
|
} |
|
text += val; |
|
} |
|
} |
|
for (;;) { |
|
walk(from); |
|
if (from == to) break; |
|
from = from.nextSibling; |
|
} |
|
return text; |
|
} |
|
|
|
CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; |
|
|
|
// SELECTION / CURSOR |
|
|
|
// Selection objects are immutable. A new one is created every time |
|
// the selection changes. A selection is one or more non-overlapping |
|
// (and non-touching) ranges, sorted, and an integer that indicates |
|
// which one is the primary selection (the one that's scrolled into |
|
// view, that getCursor returns, etc). |
|
function Selection(ranges, primIndex) { |
|
this.ranges = ranges; |
|
this.primIndex = primIndex; |
|
} |
|
|
|
Selection.prototype = { |
|
primary: function() { return this.ranges[this.primIndex]; }, |
|
equals: function(other) { |
|
if (other == this) return true; |
|
if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; |
|
for (var i = 0; i < this.ranges.length; i++) { |
|
var here = this.ranges[i], there = other.ranges[i]; |
|
if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; |
|
} |
|
return true; |
|
}, |
|
deepCopy: function() { |
|
for (var out = [], i = 0; i < this.ranges.length; i++) |
|
out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); |
|
return new Selection(out, this.primIndex); |
|
}, |
|
somethingSelected: function() { |
|
for (var i = 0; i < this.ranges.length; i++) |
|
if (!this.ranges[i].empty()) return true; |
|
return false; |
|
}, |
|
contains: function(pos, end) { |
|
if (!end) end = pos; |
|
for (var i = 0; i < this.ranges.length; i++) { |
|
var range = this.ranges[i]; |
|
if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) |
|
return i; |
|
} |
|
return -1; |
|
} |
|
}; |
|
|
|
function Range(anchor, head) { |
|
this.anchor = anchor; this.head = head; |
|
} |
|
|
|
Range.prototype = { |
|
from: function() { return minPos(this.anchor, this.head); }, |
|
to: function() { return maxPos(this.anchor, this.head); }, |
|
empty: function() { |
|
return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; |
|
} |
|
}; |
|
|
|
// Take an unsorted, potentially overlapping set of ranges, and |
|
// build a selection out of it. 'Consumes' ranges array (modifying |
|
// it). |
|
function normalizeSelection(ranges, primIndex) { |
|
var prim = ranges[primIndex]; |
|
ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); |
|
primIndex = indexOf(ranges, prim); |
|
for (var i = 1; i < ranges.length; i++) { |
|
var cur = ranges[i], prev = ranges[i - 1]; |
|
if (cmp(prev.to(), cur.from()) >= 0) { |
|
var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); |
|
var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; |
|
if (i <= primIndex) --primIndex; |
|
ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); |
|
} |
|
} |
|
return new Selection(ranges, primIndex); |
|
} |
|
|
|
function simpleSelection(anchor, head) { |
|
return new Selection([new Range(anchor, head || anchor)], 0); |
|
} |
|
|
|
// Most of the external API clips given positions to make sure they |
|
// actually exist within the document. |
|
function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} |
|
function clipPos(doc, pos) { |
|
if (pos.line < doc.first) return Pos(doc.first, 0); |
|
var last = doc.first + doc.size - 1; |
|
if (pos.line > last) return Pos(last, getLine(doc, last).text.length); |
|
return clipToLen(pos, getLine(doc, pos.line).text.length); |
|
} |
|
function clipToLen(pos, linelen) { |
|
var ch = pos.ch; |
|
if (ch == null || ch > linelen) return Pos(pos.line, linelen); |
|
else if (ch < 0) return Pos(pos.line, 0); |
|
else return pos; |
|
} |
|
function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} |
|
function clipPosArray(doc, array) { |
|
for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); |
|
return out; |
|
} |
|
|
|
// SELECTION UPDATES |
|
|
|
// The 'scroll' parameter given to many of these indicated whether |
|
// the new cursor position should be scrolled into view after |
|
// modifying the selection. |
|
|
|
// If shift is held or the extend flag is set, extends a range to |
|
// include a given position (and optionally a second position). |
|
// Otherwise, simply returns the range between the given positions. |
|
// Used for cursor motion and such. |
|
function extendRange(doc, range, head, other) { |
|
if (doc.cm && doc.cm.display.shift || doc.extend) { |
|
var anchor = range.anchor; |
|
if (other) { |
|
var posBefore = cmp(head, anchor) < 0; |
|
if (posBefore != (cmp(other, anchor) < 0)) { |
|
anchor = head; |
|
head = other; |
|
} else if (posBefore != (cmp(head, other) < 0)) { |
|
head = other; |
|
} |
|
} |
|
return new Range(anchor, head); |
|
} else { |
|
return new Range(other || head, head); |
|
} |
|
} |
|
|
|
// Extend the primary selection range, discard the rest. |
|
function extendSelection(doc, head, other, options) { |
|
setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); |
|
} |
|
|
|
// Extend all selections (pos is an array of selections with length |
|
// equal the number of selections) |
|
function extendSelections(doc, heads, options) { |
|
for (var out = [], i = 0; i < doc.sel.ranges.length; i++) |
|
out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); |
|
var newSel = normalizeSelection(out, doc.sel.primIndex); |
|
setSelection(doc, newSel, options); |
|
} |
|
|
|
// Updates a single range in the selection. |
|
function replaceOneSelection(doc, i, range, options) { |
|
var ranges = doc.sel.ranges.slice(0); |
|
ranges[i] = range; |
|
setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); |
|
} |
|
|
|
// Reset the selection to a single range. |
|
function setSimpleSelection(doc, anchor, head, options) { |
|
setSelection(doc, simpleSelection(anchor, head), options); |
|
} |
|
|
|
// Give beforeSelectionChange handlers a change to influence a |
|
// selection update. |
|
function filterSelectionChange(doc, sel) { |
|
var obj = { |
|
ranges: sel.ranges, |
|
update: function(ranges) { |
|
this.ranges = []; |
|
for (var i = 0; i < ranges.length; i++) |
|
this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), |
|
clipPos(doc, ranges[i].head)); |
|
} |
|
}; |
|
signal(doc, "beforeSelectionChange", doc, obj); |
|
if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); |
|
if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); |
|
else return sel; |
|
} |
|
|
|
function setSelectionReplaceHistory(doc, sel, options) { |
|
var done = doc.history.done, last = lst(done); |
|
if (last && last.ranges) { |
|
done[done.length - 1] = sel; |
|
setSelectionNoUndo(doc, sel, options); |
|
} else { |
|
setSelection(doc, sel, options); |
|
} |
|
} |
|
|
|
// Set a new selection. |
|
function setSelection(doc, sel, options) { |
|
setSelectionNoUndo(doc, sel, options); |
|
addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); |
|
} |
|
|
|
function setSelectionNoUndo(doc, sel, options) { |
|
if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) |
|
sel = filterSelectionChange(doc, sel); |
|
|
|
var bias = options && options.bias || |
|
(cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); |
|
setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); |
|
|
|
if (!(options && options.scroll === false) && doc.cm) |
|
ensureCursorVisible(doc.cm); |
|
} |
|
|
|
function setSelectionInner(doc, sel) { |
|
if (sel.equals(doc.sel)) return; |
|
|
|
doc.sel = sel; |
|
|
|
if (doc.cm) { |
|
doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; |
|
signalCursorActivity(doc.cm); |
|
} |
|
signalLater(doc, "cursorActivity", doc); |
|
} |
|
|
|
// Verify that the selection does not partially select any atomic |
|
// marked ranges. |
|
function reCheckSelection(doc) { |
|
setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); |
|
} |
|
|
|
// Return a selection that does not partially select any atomic |
|
// ranges. |
|
function skipAtomicInSelection(doc, sel, bias, mayClear) { |
|
var out; |
|
for (var i = 0; i < sel.ranges.length; i++) { |
|
var range = sel.ranges[i]; |
|
var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); |
|
var newHead = skipAtomic(doc, range.head, bias, mayClear); |
|
if (out || newAnchor != range.anchor || newHead != range.head) { |
|
if (!out) out = sel.ranges.slice(0, i); |
|
out[i] = new Range(newAnchor, newHead); |
|
} |
|
} |
|
return out ? normalizeSelection(out, sel.primIndex) : sel; |
|
} |
|
|
|
// Ensure a given position is not inside an atomic range. |
|
function skipAtomic(doc, pos, bias, mayClear) { |
|
var flipped = false, curPos = pos; |
|
var dir = bias || 1; |
|
doc.cantEdit = false; |
|
search: for (;;) { |
|
var line = getLine(doc, curPos.line); |
|
if (line.markedSpans) { |
|
for (var i = 0; i < line.markedSpans.length; ++i) { |
|
var sp = line.markedSpans[i], m = sp.marker; |
|
if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && |
|
(sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { |
|
if (mayClear) { |
|
signal(m, "beforeCursorEnter"); |
|
if (m.explicitlyCleared) { |
|
if (!line.markedSpans) break; |
|
else {--i; continue;} |
|
} |
|
} |
|
if (!m.atomic) continue; |
|
var newPos = m.find(dir < 0 ? -1 : 1); |
|
if (cmp(newPos, curPos) == 0) { |
|
newPos.ch += dir; |
|
if (newPos.ch < 0) { |
|
if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); |
|
else newPos = null; |
|
} else if (newPos.ch > line.text.length) { |
|
if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); |
|
else newPos = null; |
|
} |
|
if (!newPos) { |
|
if (flipped) { |
|
// Driven in a corner -- no valid cursor position found at all |
|
// -- try again *with* clearing, if we didn't already |
|
if (!mayClear) return skipAtomic(doc, pos, bias, true); |
|
// Otherwise, turn off editing until further notice, and return the start of the doc |
|
doc.cantEdit = true; |
|
return Pos(doc.first, 0); |
|
} |
|
flipped = true; newPos = pos; dir = -dir; |
|
} |
|
} |
|
curPos = newPos; |
|
continue search; |
|
} |
|
} |
|
} |
|
return curPos; |
|
} |
|
} |
|
|
|
// SELECTION DRAWING |
|
|
|
function updateSelection(cm) { |
|
cm.display.input.showSelection(cm.display.input.prepareSelection()); |
|
} |
|
|
|
function prepareSelection(cm, primary) { |
|
var doc = cm.doc, result = {}; |
|
var curFragment = result.cursors = document.createDocumentFragment(); |
|
var selFragment = result.selection = document.createDocumentFragment(); |
|
|
|
for (var i = 0; i < doc.sel.ranges.length; i++) { |
|
if (primary === false && i == doc.sel.primIndex) continue; |
|
var range = doc.sel.ranges[i]; |
|
var collapsed = range.empty(); |
|
if (collapsed || cm.options.showCursorWhenSelecting) |
|
drawSelectionCursor(cm, range, curFragment); |
|
if (!collapsed) |
|
drawSelectionRange(cm, range, selFragment); |
|
} |
|
return result; |
|
} |
|
|
|
// Draws a cursor for the given range |
|
function drawSelectionCursor(cm, range, output) { |
|
var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); |
|
|
|
var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); |
|
cursor.style.left = pos.left + "px"; |
|
cursor.style.top = pos.top + "px"; |
|
cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; |
|
|
|
if (pos.other) { |
|
// Secondary cursor, shown when on a 'jump' in bi-directional text |
|
var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); |
|
otherCursor.style.display = ""; |
|
otherCursor.style.left = pos.other.left + "px"; |
|
otherCursor.style.top = pos.other.top + "px"; |
|
otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; |
|
} |
|
} |
|
|
|
// Draws the given range as a highlighted selection |
|
function drawSelectionRange(cm, range, output) { |
|
var display = cm.display, doc = cm.doc; |
|
var fragment = document.createDocumentFragment(); |
|
var padding = paddingH(cm.display), leftSide = padding.left; |
|
var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; |
|
|
|
function add(left, top, width, bottom) { |
|
if (top < 0) top = 0; |
|
top = Math.round(top); |
|
bottom = Math.round(bottom); |
|
fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + |
|
"px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + |
|
"px; height: " + (bottom - top) + "px")); |
|
} |
|
|
|
function drawForLine(line, fromArg, toArg) { |
|
var lineObj = getLine(doc, line); |
|
var lineLen = lineObj.text.length; |
|
var start, end; |
|
function coords(ch, bias) { |
|
return charCoords(cm, Pos(line, ch), "div", lineObj, bias); |
|
} |
|
|
|
iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { |
|
var leftPos = coords(from, "left"), rightPos, left, right; |
|
if (from == to) { |
|
rightPos = leftPos; |
|
left = right = leftPos.left; |
|
} else { |
|
rightPos = coords(to - 1, "right"); |
|
if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } |
|
left = leftPos.left; |
|
right = rightPos.right; |
|
} |
|
if (fromArg == null && from == 0) left = leftSide; |
|
if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part |
|
add(left, leftPos.top, null, leftPos.bottom); |
|
left = leftSide; |
|
if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); |
|
} |
|
if (toArg == null && to == lineLen) right = rightSide; |
|
if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) |
|
start = leftPos; |
|
if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) |
|
end = rightPos; |
|
if (left < leftSide + 1) left = leftSide; |
|
add(left, rightPos.top, right - left, rightPos.bottom); |
|
}); |
|
return {start: start, end: end}; |
|
} |
|
|
|
var sFrom = range.from(), sTo = range.to(); |
|
if (sFrom.line == sTo.line) { |
|
drawForLine(sFrom.line, sFrom.ch, sTo.ch); |
|
} else { |
|
var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); |
|
var singleVLine = visualLine(fromLine) == visualLine(toLine); |
|
var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; |
|
var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; |
|
if (singleVLine) { |
|
if (leftEnd.top < rightStart.top - 2) { |
|
add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); |
|
add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); |
|
} else { |
|
add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); |
|
} |
|
} |
|
if (leftEnd.bottom < rightStart.top) |
|
add(leftSide, leftEnd.bottom, null, rightStart.top); |
|
} |
|
|
|
output.appendChild(fragment); |
|
} |
|
|
|
// Cursor-blinking |
|
function restartBlink(cm) { |
|
if (!cm.state.focused) return; |
|
var display = cm.display; |
|
clearInterval(display.blinker); |
|
var on = true; |
|
display.cursorDiv.style.visibility = ""; |
|
if (cm.options.cursorBlinkRate > 0) |
|
display.blinker = setInterval(function() { |
|
display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; |
|
}, cm.options.cursorBlinkRate); |
|
else if (cm.options.cursorBlinkRate < 0) |
|
display.cursorDiv.style.visibility = "hidden"; |
|
} |
|
|
|
// HIGHLIGHT WORKER |
|
|
|
function startWorker(cm, time) { |
|
if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) |
|
cm.state.highlight.set(time, bind(highlightWorker, cm)); |
|
} |
|
|
|
function highlightWorker(cm) { |
|
var doc = cm.doc; |
|
if (doc.frontier < doc.first) doc.frontier = doc.first; |
|
if (doc.frontier >= cm.display.viewTo) return; |
|
var end = +new Date + cm.options.workTime; |
|
var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); |
|
var changedLines = []; |
|
|
|
doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { |
|
if (doc.frontier >= cm.display.viewFrom) { // Visible |
|
var oldStyles = line.styles; |
|
var highlighted = highlightLine(cm, line, state, true); |
|
line.styles = highlighted.styles; |
|
var oldCls = line.styleClasses, newCls = highlighted.classes; |
|
if (newCls) line.styleClasses = newCls; |
|
else if (oldCls) line.styleClasses = null; |
|
var ischange = !oldStyles || oldStyles.length != line.styles.length || |
|
oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); |
|
for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; |
|
if (ischange) changedLines.push(doc.frontier); |
|
line.stateAfter = copyState(doc.mode, state); |
|
} else { |
|
processLine(cm, line.text, state); |
|
line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; |
|
} |
|
++doc.frontier; |
|
if (+new Date > end) { |
|
startWorker(cm, cm.options.workDelay); |
|
return true; |
|
} |
|
}); |
|
if (changedLines.length) runInOp(cm, function() { |
|
for (var i = 0; i < changedLines.length; i++) |
|
regLineChange(cm, changedLines[i], "text"); |
|
}); |
|
} |
|
|
|
// Finds the line to start with when starting a parse. Tries to |
|
// find a line with a stateAfter, so that it can start with a |
|
// valid state. If that fails, it returns the line with the |
|
// smallest indentation, which tends to need the least context to |
|
// parse correctly. |
|
function findStartLine(cm, n, precise) { |
|
var minindent, minline, doc = cm.doc; |
|
var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); |
|
for (var search = n; search > lim; --search) { |
|
if (search <= doc.first) return doc.first; |
|
var line = getLine(doc, search - 1); |
|
if (line.stateAfter && (!precise || search <= doc.frontier)) return search; |
|
var indented = countColumn(line.text, null, cm.options.tabSize); |
|
if (minline == null || minindent > indented) { |
|
minline = search - 1; |
|
minindent = indented; |
|
} |
|
} |
|
return minline; |
|
} |
|
|
|
function getStateBefore(cm, n, precise) { |
|
var doc = cm.doc, display = cm.display; |
|
if (!doc.mode.startState) return true; |
|
var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; |
|
if (!state) state = startState(doc.mode); |
|
else state = copyState(doc.mode, state); |
|
doc.iter(pos, n, function(line) { |
|
processLine(cm, line.text, state); |
|
var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; |
|
line.stateAfter = save ? copyState(doc.mode, state) : null; |
|
++pos; |
|
}); |
|
if (precise) doc.frontier = pos; |
|
return state; |
|
} |
|
|
|
// POSITION MEASUREMENT |
|
|
|
function paddingTop(display) {return display.lineSpace.offsetTop;} |
|
function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} |
|
function paddingH(display) { |
|
if (display.cachedPaddingH) return display.cachedPaddingH; |
|
var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); |
|
var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; |
|
var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; |
|
if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; |
|
return data; |
|
} |
|
|
|
function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; } |
|
function displayWidth(cm) { |
|
return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth; |
|
} |
|
function displayHeight(cm) { |
|
return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight; |
|
} |
|
|
|
// Ensure the lineView.wrapping.heights array is populated. This is |
|
// an array of bottom offsets for the lines that make up a drawn |
|
// line. When lineWrapping is on, there might be more than one |
|
// height. |
|
function ensureLineHeights(cm, lineView, rect) { |
|
var wrapping = cm.options.lineWrapping; |
|
var curWidth = wrapping && displayWidth(cm); |
|
if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { |
|
var heights = lineView.measure.heights = []; |
|
if (wrapping) { |
|
lineView.measure.width = curWidth; |
|
var rects = lineView.text.firstChild.getClientRects(); |
|
for (var i = 0; i < rects.length - 1; i++) { |
|
var cur = rects[i], next = rects[i + 1]; |
|
if (Math.abs(cur.bottom - next.bottom) > 2) |
|
heights.push((cur.bottom + next.top) / 2 - rect.top); |
|
} |
|
} |
|
heights.push(rect.bottom - rect.top); |
|
} |
|
} |
|
|
|
// Find a line map (mapping character offsets to text nodes) and a |
|
// measurement cache for the given line number. (A line view might |
|
// contain multiple lines when collapsed ranges are present.) |
|
function mapFromLineView(lineView, line, lineN) { |
|
if (lineView.line == line) |
|
return {map: lineView.measure.map, cache: lineView.measure.cache}; |
|
for (var i = 0; i < lineView.rest.length; i++) |
|
if (lineView.rest[i] == line) |
|
return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; |
|
for (var i = 0; i < lineView.rest.length; i++) |
|
if (lineNo(lineView.rest[i]) > lineN) |
|
return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; |
|
} |
|
|
|
// Render a line into the hidden node display.externalMeasured. Used |
|
// when measurement is needed for a line that's not in the viewport. |
|
function updateExternalMeasurement(cm, line) { |
|
line = visualLine(line); |
|
var lineN = lineNo(line); |
|
var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); |
|
view.lineN = lineN; |
|
var built = view.built = buildLineContent(cm, view); |
|
view.text = built.pre; |
|
removeChildrenAndAdd(cm.display.lineMeasure, built.pre); |
|
return view; |
|
} |
|
|
|
// Get a {top, bottom, left, right} box (in line-local coordinates) |
|
// for a given character. |
|
function measureChar(cm, line, ch, bias) { |
|
return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); |
|
} |
|
|
|
// Find a line view that corresponds to the given line number. |
|
function findViewForLine(cm, lineN) { |
|
if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) |
|
return cm.display.view[findViewIndex(cm, lineN)]; |
|
var ext = cm.display.externalMeasured; |
|
if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) |
|
return ext; |
|
} |
|
|
|
// Measurement can be split in two steps, the set-up work that |
|
// applies to the whole line, and the measurement of the actual |
|
// character. Functions like coordsChar, that need to do a lot of |
|
// measurements in a row, can thus ensure that the set-up work is |
|
// only done once. |
|
function prepareMeasureForLine(cm, line) { |
|
var lineN = lineNo(line); |
|
var view = findViewForLine(cm, lineN); |
|
if (view && !view.text) |
|
view = null; |
|
else if (view && view.changes) |
|
updateLineForChanges(cm, view, lineN, getDimensions(cm)); |
|
if (!view) |
|
view = updateExternalMeasurement(cm, line); |
|
|
|
var info = mapFromLineView(view, line, lineN); |
|
return { |
|
line: line, view: view, rect: null, |
|
map: info.map, cache: info.cache, before: info.before, |
|
hasHeights: false |
|
}; |
|
} |
|
|
|
// Given a prepared measurement object, measures the position of an |
|
// actual character (or fetches it from the cache). |
|
function measureCharPrepared(cm, prepared, ch, bias, varHeight) { |
|
if (prepared.before) ch = -1; |
|
var key = ch + (bias || ""), found; |
|
if (prepared.cache.hasOwnProperty(key)) { |
|
found = prepared.cache[key]; |
|
} else { |
|
if (!prepared.rect) |
|
prepared.rect = prepared.view.text.getBoundingClientRect(); |
|
if (!prepared.hasHeights) { |
|
ensureLineHeights(cm, prepared.view, prepared.rect); |
|
prepared.hasHeights = true; |
|
} |
|
found = measureCharInner(cm, prepared, ch, bias); |
|
if (!found.bogus) prepared.cache[key] = found; |
|
} |
|
return {left: found.left, right: found.right, |
|
top: varHeight ? found.rtop : found.top, |
|
bottom: varHeight ? found.rbottom : found.bottom}; |
|
} |
|
|
|
var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; |
|
|
|
function nodeAndOffsetInLineMap(map, ch, bias) { |
|
var node, start, end, collapse; |
|
// First, search the line map for the text node corresponding to, |
|
// or closest to, the target character. |
|
for (var i = 0; i < map.length; i += 3) { |
|
var mStart = map[i], mEnd = map[i + 1]; |
|
if (ch < mStart) { |
|
start = 0; end = 1; |
|
collapse = "left"; |
|
} else if (ch < mEnd) { |
|
start = ch - mStart; |
|
end = start + 1; |
|
} else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { |
|
end = mEnd - mStart; |
|
start = end - 1; |
|
if (ch >= mEnd) collapse = "right"; |
|
} |
|
if (start != null) { |
|
node = map[i + 2]; |
|
if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) |
|
collapse = bias; |
|
if (bias == "left" && start == 0) |
|
while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { |
|
node = map[(i -= 3) + 2]; |
|
collapse = "left"; |
|
} |
|
if (bias == "right" && start == mEnd - mStart) |
|
while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { |
|
node = map[(i += 3) + 2]; |
|
collapse = "right"; |
|
} |
|
break; |
|
} |
|
} |
|
return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; |
|
} |
|
|
|
function measureCharInner(cm, prepared, ch, bias) { |
|
var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); |
|
var node = place.node, start = place.start, end = place.end, collapse = place.collapse; |
|
|
|
var rect; |
|
if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. |
|
for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned |
|
while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start; |
|
while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end; |
|
if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) { |
|
rect = node.parentNode.getBoundingClientRect(); |
|
} else if (ie && cm.options.lineWrapping) { |
|
var rects = range(node, start, end).getClientRects(); |
|
if (rects.length) |
|
rect = rects[bias == "right" ? rects.length - 1 : 0]; |
|
else |
|
rect = nullRect; |
|
} else { |
|
rect = range(node, start, end).getBoundingClientRect() || nullRect; |
|
} |
|
if (rect.left || rect.right || start == 0) break; |
|
end = start; |
|
start = start - 1; |
|
collapse = "right"; |
|
} |
|
if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); |
|
} else { // If it is a widget, simply get the box for the whole widget. |
|
if (start > 0) collapse = bias = "right"; |
|
var rects; |
|
if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) |
|
rect = rects[bias == "right" ? rects.length - 1 : 0]; |
|
else |
|
rect = node.getBoundingClientRect(); |
|
} |
|
if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { |
|
var rSpan = node.parentNode.getClientRects()[0]; |
|
if (rSpan) |
|
rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; |
|
else |
|
rect = nullRect; |
|
} |
|
|
|
var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; |
|
var mid = (rtop + rbot) / 2; |
|
var heights = prepared.view.measure.heights; |
|
for (var i = 0; i < heights.length - 1; i++) |
|
if (mid < heights[i]) break; |
|
var top = i ? heights[i - 1] : 0, bot = heights[i]; |
|
var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, |
|
right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, |
|
top: top, bottom: bot}; |
|
if (!rect.left && !rect.right) result.bogus = true; |
|
if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } |
|
|
|
return result; |
|
} |
|
|
|
// Work around problem with bounding client rects on ranges being |
|
// returned incorrectly when zoomed on IE10 and below. |
|
function maybeUpdateRectForZooming(measure, rect) { |
|
if (!window.screen || screen.logicalXDPI == null || |
|
screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) |
|
return rect; |
|
var scaleX = screen.logicalXDPI / screen.deviceXDPI; |
|
var scaleY = screen.logicalYDPI / screen.deviceYDPI; |
|
return {left: rect.left * scaleX, right: rect.right * scaleX, |
|
top: rect.top * scaleY, bottom: rect.bottom * scaleY}; |
|
} |
|
|
|
function clearLineMeasurementCacheFor(lineView) { |
|
if (lineView.measure) { |
|
lineView.measure.cache = {}; |
|
lineView.measure.heights = null; |
|
if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) |
|
lineView.measure.caches[i] = {}; |
|
} |
|
} |
|
|
|
function clearLineMeasurementCache(cm) { |
|
cm.display.externalMeasure = null; |
|
removeChildren(cm.display.lineMeasure); |
|
for (var i = 0; i < cm.display.view.length; i++) |
|
clearLineMeasurementCacheFor(cm.display.view[i]); |
|
} |
|
|
|
function clearCaches(cm) { |
|
clearLineMeasurementCache(cm); |
|
cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; |
|
if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; |
|
cm.display.lineNumChars = null; |
|
} |
|
|
|
function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } |
|
function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } |
|
|
|
// Converts a {top, bottom, left, right} box from line-local |
|
// coordinates into another coordinate system. Context may be one of |
|
// "line", "div" (display.lineDiv), "local"/null (editor), "window", |
|
// or "page". |
|
function intoCoordSystem(cm, lineObj, rect, context) { |
|
if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { |
|
var size = widgetHeight(lineObj.widgets[i]); |
|
rect.top += size; rect.bottom += size; |
|
} |
|
if (context == "line") return rect; |
|
if (!context) context = "local"; |
|
var yOff = heightAtLine(lineObj); |
|
if (context == "local") yOff += paddingTop(cm.display); |
|
else yOff -= cm.display.viewOffset; |
|
if (context == "page" || context == "window") { |
|
var lOff = cm.display.lineSpace.getBoundingClientRect(); |
|
yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); |
|
var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); |
|
rect.left += xOff; rect.right += xOff; |
|
} |
|
rect.top += yOff; rect.bottom += yOff; |
|
return rect; |
|
} |
|
|
|
// Coverts a box from "div" coords to another coordinate system. |
|
// Context may be "window", "page", "div", or "local"/null. |
|
function fromCoordSystem(cm, coords, context) { |
|
if (context == "div") return coords; |
|
var left = coords.left, top = coords.top; |
|
// First move into "page" coordinate system |
|
if (context == "page") { |
|
left -= pageScrollX(); |
|
top -= pageScrollY(); |
|
} else if (context == "local" || !context) { |
|
var localBox = cm.display.sizer.getBoundingClientRect(); |
|
left += localBox.left; |
|
top += localBox.top; |
|
} |
|
|
|
var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); |
|
return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; |
|
} |
|
|
|
function charCoords(cm, pos, context, lineObj, bias) { |
|
if (!lineObj) lineObj = getLine(cm.doc, pos.line); |
|
return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); |
|
} |
|
|
|
// Returns a box for a given cursor position, which may have an |
|
// 'other' property containing the position of the secondary cursor |
|
// on a bidi boundary. |
|
function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { |
|
lineObj = lineObj || getLine(cm.doc, pos.line); |
|
if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); |
|
function get(ch, right) { |
|
var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); |
|
if (right) m.left = m.right; else m.right = m.left; |
|
return intoCoordSystem(cm, lineObj, m, context); |
|
} |
|
function getBidi(ch, partPos) { |
|
var part = order[partPos], right = part.level % 2; |
|
if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { |
|
part = order[--partPos]; |
|
ch = bidiRight(part) - (part.level % 2 ? 0 : 1); |
|
right = true; |
|
} else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { |
|
part = order[++partPos]; |
|
ch = bidiLeft(part) - part.level % 2; |
|
right = false; |
|
} |
|
if (right && ch == part.to && ch > part.from) return get(ch - 1); |
|
return get(ch, right); |
|
} |
|
var order = getOrder(lineObj), ch = pos.ch; |
|
if (!order) return get(ch); |
|
var partPos = getBidiPartAt(order, ch); |
|
var val = getBidi(ch, partPos); |
|
if (bidiOther != null) val.other = getBidi(ch, bidiOther); |
|
return val; |
|
} |
|
|
|
// Used to cheaply estimate the coordinates for a position. Used for |
|
// intermediate scroll updates. |
|
function estimateCoords(cm, pos) { |
|
var left = 0, pos = clipPos(cm.doc, pos); |
|
if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; |
|
var lineObj = getLine(cm.doc, pos.line); |
|
var top = heightAtLine(lineObj) + paddingTop(cm.display); |
|
return {left: left, right: left, top: top, bottom: top + lineObj.height}; |
|
} |
|
|
|
// Positions returned by coordsChar contain some extra information. |
|
// xRel is the relative x position of the input coordinates compared |
|
// to the found position (so xRel > 0 means the coordinates are to |
|
// the right of the character position, for example). When outside |
|
// is true, that means the coordinates lie outside the line's |
|
// vertical range. |
|
function PosWithInfo(line, ch, outside, xRel) { |
|
var pos = Pos(line, ch); |
|
pos.xRel = xRel; |
|
if (outside) pos.outside = true; |
|
return pos; |
|
} |
|
|
|
// Compute the character position closest to the given coordinates. |
|
// Input must be lineSpace-local ("div" coordinate system). |
|
function coordsChar(cm, x, y) { |
|
var doc = cm.doc; |
|
y += cm.display.viewOffset; |
|
if (y < 0) return PosWithInfo(doc.first, 0, true, -1); |
|
var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; |
|
if (lineN > last) |
|
return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); |
|
if (x < 0) x = 0; |
|
|
|
var lineObj = getLine(doc, lineN); |
|
for (;;) { |
|
var found = coordsCharInner(cm, lineObj, lineN, x, y); |
|
var merged = collapsedSpanAtEnd(lineObj); |
|
var mergedPos = merged && merged.find(0, true); |
|
if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) |
|
lineN = lineNo(lineObj = mergedPos.to.line); |
|
else |
|
return found; |
|
} |
|
} |
|
|
|
function coordsCharInner(cm, lineObj, lineNo, x, y) { |
|
var innerOff = y - heightAtLine(lineObj); |
|
var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; |
|
var preparedMeasure = prepareMeasureForLine(cm, lineObj); |
|
|
|
function getX(ch) { |
|
var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); |
|
wrongLine = true; |
|
if (innerOff > sp.bottom) return sp.left - adjust; |
|
else if (innerOff < sp.top) return sp.left + adjust; |
|
else wrongLine = false; |
|
return sp.left; |
|
} |
|
|
|
var bidi = getOrder(lineObj), dist = lineObj.text.length; |
|
var from = lineLeft(lineObj), to = lineRight(lineObj); |
|
var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; |
|
|
|
if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); |
|
// Do a binary search between these bounds. |
|
for (;;) { |
|
if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { |
|
var ch = x < fromX || x - fromX <= toX - x ? from : to; |
|
var xDiff = x - (ch == from ? fromX : toX); |
|
while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; |
|
var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, |
|
xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); |
|
return pos; |
|
} |
|
var step = Math.ceil(dist / 2), middle = from + step; |
|
if (bidi) { |
|
middle = from; |
|
for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); |
|
} |
|
var middleX = getX(middle); |
|
if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} |
|
else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} |
|
} |
|
} |
|
|
|
var measureText; |
|
// Compute the default text height. |
|
function textHeight(display) { |
|
if (display.cachedTextHeight != null) return display.cachedTextHeight; |
|
if (measureText == null) { |
|
measureText = elt("pre"); |
|
// Measure a bunch of lines, for browsers that compute |
|
// fractional heights. |
|
for (var i = 0; i < 49; ++i) { |
|
measureText.appendChild(document.createTextNode("x")); |
|
measureText.appendChild(elt("br")); |
|
} |
|
measureText.appendChild(document.createTextNode("x")); |
|
} |
|
removeChildrenAndAdd(display.measure, measureText); |
|
var height = measureText.offsetHeight / 50; |
|
if (height > 3) display.cachedTextHeight = height; |
|
removeChildren(display.measure); |
|
return height || 1; |
|
} |
|
|
|
// Compute the default character width. |
|
function charWidth(display) { |
|
if (display.cachedCharWidth != null) return display.cachedCharWidth; |
|
var anchor = elt("span", "xxxxxxxxxx"); |
|
var pre = elt("pre", [anchor]); |
|
removeChildrenAndAdd(display.measure, pre); |
|
var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; |
|
if (width > 2) display.cachedCharWidth = width; |
|
return width || 10; |
|
} |
|
|
|
// OPERATIONS |
|
|
|
// Operations are used to wrap a series of changes to the editor |
|
// state in such a way that each change won't have to update the |
|
// cursor and display (which would be awkward, slow, and |
|
// error-prone). Instead, display updates are batched and then all |
|
// combined and executed at once. |
|
|
|
var operationGroup = null; |
|
|
|
var nextOpId = 0; |
|
// Start a new operation. |
|
function startOperation(cm) { |
|
cm.curOp = { |
|
cm: cm, |
|
viewChanged: false, // Flag that indicates that lines might need to be redrawn |
|
startHeight: cm.doc.height, // Used to detect need to update scrollbar |
|
forceUpdate: false, // Used to force a redraw |
|
updateInput: null, // Whether to reset the input textarea |
|
typing: false, // Whether this reset should be careful to leave existing text (for compositing) |
|
changeObjs: null, // Accumulated changes, for firing change events |
|
cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on |
|
cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already |
|
selectionChanged: false, // Whether the selection needs to be redrawn |
|
updateMaxLine: false, // Set when the widest line needs to be determined anew |
|
scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet |
|
scrollToPos: null, // Used to scroll to a specific position |
|
focus: false, |
|
id: ++nextOpId // Unique ID |
|
}; |
|
if (operationGroup) { |
|
operationGroup.ops.push(cm.curOp); |
|
} else { |
|
cm.curOp.ownsGroup = operationGroup = { |
|
ops: [cm.curOp], |
|
delayedCallbacks: [] |
|
}; |
|
} |
|
} |
|
|
|
function fireCallbacksForOps(group) { |
|
// Calls delayed callbacks and cursorActivity handlers until no |
|
// new ones appear |
|
var callbacks = group.delayedCallbacks, i = 0; |
|
do { |
|
for (; i < callbacks.length; i++) |
|
callbacks[i](); |
|
for (var j = 0; j < group.ops.length; j++) { |
|
var op = group.ops[j]; |
|
if (op.cursorActivityHandlers) |
|
while (op.cursorActivityCalled < op.cursorActivityHandlers.length) |
|
op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); |
|
} |
|
} while (i < callbacks.length); |
|
} |
|
|
|
// Finish an operation, updating the display and signalling delayed events |
|
function endOperation(cm) { |
|
var op = cm.curOp, group = op.ownsGroup; |
|
if (!group) return; |
|
|
|
try { fireCallbacksForOps(group); } |
|
finally { |
|
operationGroup = null; |
|
for (var i = 0; i < group.ops.length; i++) |
|
group.ops[i].cm.curOp = null; |
|
endOperations(group); |
|
} |
|
} |
|
|
|
// The DOM updates done when an operation finishes are batched so |
|
// that the minimum number of relayouts are required. |
|
function endOperations(group) { |
|
var ops = group.ops; |
|
for (var i = 0; i < ops.length; i++) // Read DOM |
|
endOperation_R1(ops[i]); |
|
for (var i = 0; i < ops.length; i++) // Write DOM (maybe) |
|
endOperation_W1(ops[i]); |
|
for (var i = 0; i < ops.length; i++) // Read DOM |
|
endOperation_R2(ops[i]); |
|
for (var i = 0; i < ops.length; i++) // Write DOM (maybe) |
|
endOperation_W2(ops[i]); |
|
for (var i = 0; i < ops.length; i++) // Read DOM |
|
endOperation_finish(ops[i]); |
|
} |
|
|
|
function endOperation_R1(op) { |
|
var cm = op.cm, display = cm.display; |
|
maybeClipScrollbars(cm); |
|
if (op.updateMaxLine) findMaxLine(cm); |
|
|
|
op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || |
|
op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || |
|
op.scrollToPos.to.line >= display.viewTo) || |
|
display.maxLineChanged && cm.options.lineWrapping; |
|
op.update = op.mustUpdate && |
|
new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); |
|
} |
|
|
|
function endOperation_W1(op) { |
|
op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); |
|
} |
|
|
|
function endOperation_R2(op) { |
|
var cm = op.cm, display = cm.display; |
|
if (op.updatedDisplay) updateHeightsInViewport(cm); |
|
|
|
op.barMeasure = measureForScrollbars(cm); |
|
|
|
// If the max line changed since it was last measured, measure it, |
|
// and ensure the document's width matches it. |
|
// updateDisplay_W2 will use these properties to do the actual resizing |
|
if (display.maxLineChanged && !cm.options.lineWrapping) { |
|
op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; |
|
cm.display.sizerWidth = op.adjustWidthTo; |
|
op.barMeasure.scrollWidth = |
|
Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); |
|
op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); |
|
} |
|
|
|
if (op.updatedDisplay || op.selectionChanged) |
|
op.preparedSelection = display.input.prepareSelection(); |
|
} |
|
|
|
function endOperation_W2(op) { |
|
var cm = op.cm; |
|
|
|
if (op.adjustWidthTo != null) { |
|
cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; |
|
if (op.maxScrollLeft < cm.doc.scrollLeft) |
|
setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); |
|
cm.display.maxLineChanged = false; |
|
} |
|
|
|
if (op.preparedSelection) |
|
cm.display.input.showSelection(op.preparedSelection); |
|
if (op.updatedDisplay) |
|
setDocumentHeight(cm, op.barMeasure); |
|
if (op.updatedDisplay || op.startHeight != cm.doc.height) |
|
updateScrollbars(cm, op.barMeasure); |
|
|
|
if (op.selectionChanged) restartBlink(cm); |
|
|
|
if (cm.state.focused && op.updateInput) |
|
cm.display.input.reset(op.typing); |
|
if (op.focus && op.focus == activeElt()) ensureFocus(op.cm); |
|
} |
|
|
|
function endOperation_finish(op) { |
|
var cm = op.cm, display = cm.display, doc = cm.doc; |
|
|
|
if (op.updatedDisplay) postUpdateDisplay(cm, op.update); |
|
|
|
// Abort mouse wheel delta measurement, when scrolling explicitly |
|
if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) |
|
display.wheelStartX = display.wheelStartY = null; |
|
|
|
// Propagate the scroll position to the actual DOM scroller |
|
if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { |
|
doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); |
|
display.scrollbars.setScrollTop(doc.scrollTop); |
|
display.scroller.scrollTop = doc.scrollTop; |
|
} |
|
if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { |
|
doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); |
|
display.scrollbars.setScrollLeft(doc.scrollLeft); |
|
display.scroller.scrollLeft = doc.scrollLeft; |
|
alignHorizontally(cm); |
|
} |
|
// If we need to scroll a specific position into view, do so. |
|
if (op.scrollToPos) { |
|
var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), |
|
clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); |
|
if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); |
|
} |
|
|
|
// Fire events for markers that are hidden/unidden by editing or |
|
// undoing |
|
var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; |
|
if (hidden) for (var i = 0; i < hidden.length; ++i) |
|
if (!hidden[i].lines.length) signal(hidden[i], "hide"); |
|
if (unhidden) for (var i = 0; i < unhidden.length; ++i) |
|
if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); |
|
|
|
if (display.wrapper.offsetHeight) |
|
doc.scrollTop = cm.display.scroller.scrollTop; |
|
|
|
// Fire change events, and delayed event handlers |
|
if (op.changeObjs) |
|
signal(cm, "changes", cm, op.changeObjs); |
|
if (op.update) |
|
op.update.finish(); |
|
} |
|
|
|
// Run the given function in an operation |
|
function runInOp(cm, f) { |
|
if (cm.curOp) return f(); |
|
startOperation(cm); |
|
try { return f(); } |
|
finally { endOperation(cm); } |
|
} |
|
// Wraps a function in an operation. Returns the wrapped function. |
|
function operation(cm, f) { |
|
return function() { |
|
if (cm.curOp) return f.apply(cm, arguments); |
|
startOperation(cm); |
|
try { return f.apply(cm, arguments); } |
|
finally { endOperation(cm); } |
|
}; |
|
} |
|
// Used to add methods to editor and doc instances, wrapping them in |
|
// operations. |
|
function methodOp(f) { |
|
return function() { |
|
if (this.curOp) return f.apply(this, arguments); |
|
startOperation(this); |
|
try { return f.apply(this, arguments); } |
|
finally { endOperation(this); } |
|
}; |
|
} |
|
function docMethodOp(f) { |
|
return function() { |
|
var cm = this.cm; |
|
if (!cm || cm.curOp) return f.apply(this, arguments); |
|
startOperation(cm); |
|
try { return f.apply(this, arguments); } |
|
finally { endOperation(cm); } |
|
}; |
|
} |
|
|
|
// VIEW TRACKING |
|
|
|
// These objects are used to represent the visible (currently drawn) |
|
// part of the document. A LineView may correspond to multiple |
|
// logical lines, if those are connected by collapsed ranges. |
|
function LineView(doc, line, lineN) { |
|
// The starting line |
|
this.line = line; |
|
// Continuing lines, if any |
|
this.rest = visualLineContinued(line); |
|
// Number of logical lines in this visual line |
|
this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; |
|
this.node = this.text = null; |
|
this.hidden = lineIsHidden(doc, line); |
|
} |
|
|
|
// Create a range of LineView objects for the given lines. |
|
function buildViewArray(cm, from, to) { |
|
var array = [], nextPos; |
|
for (var pos = from; pos < to; pos = nextPos) { |
|
var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); |
|
nextPos = pos + view.size; |
|
array.push(view); |
|
} |
|
return array; |
|
} |
|
|
|
// Updates the display.view data structure for a given change to the |
|
// document. From and to are in pre-change coordinates. Lendiff is |
|
// the amount of lines added or subtracted by the change. This is |
|
// used for changes that span multiple lines, or change the way |
|
// lines are divided into visual lines. regLineChange (below) |
|
// registers single-line changes. |
|
function regChange(cm, from, to, lendiff) { |
|
if (from == null) from = cm.doc.first; |
|
if (to == null) to = cm.doc.first + cm.doc.size; |
|
if (!lendiff) lendiff = 0; |
|
|
|
var display = cm.display; |
|
if (lendiff && to < display.viewTo && |
|
(display.updateLineNumbers == null || display.updateLineNumbers > from)) |
|
display.updateLineNumbers = from; |
|
|
|
cm.curOp.viewChanged = true; |
|
|
|
if (from >= display.viewTo) { // Change after |
|
if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) |
|
resetView(cm); |
|
} else if (to <= display.viewFrom) { // Change before |
|
if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { |
|
resetView(cm); |
|
} else { |
|
display.viewFrom += lendiff; |
|
display.viewTo += lendiff; |
|
} |
|
} else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap |
|
resetView(cm); |
|
} else if (from <= display.viewFrom) { // Top overlap |
|
var cut = viewCuttingPoint(cm, to, to + lendiff, 1); |
|
if (cut) { |
|
display.view = display.view.slice(cut.index); |
|
display.viewFrom = cut.lineN; |
|
display.viewTo += lendiff; |
|
} else { |
|
resetView(cm); |
|
} |
|
} else if (to >= display.viewTo) { // Bottom overlap |
|
var cut = viewCuttingPoint(cm, from, from, -1); |
|
if (cut) { |
|
display.view = display.view.slice(0, cut.index); |
|
display.viewTo = cut.lineN; |
|
} else { |
|
resetView(cm); |
|
} |
|
} else { // Gap in the middle |
|
var cutTop = viewCuttingPoint(cm, from, from, -1); |
|
var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); |
|
if (cutTop && cutBot) { |
|
display.view = display.view.slice(0, cutTop.index) |
|
.concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) |
|
.concat(display.view.slice(cutBot.index)); |
|
display.viewTo += lendiff; |
|
} else { |
|
resetView(cm); |
|
} |
|
} |
|
|
|
var ext = display.externalMeasured; |
|
if (ext) { |
|
if (to < ext.lineN) |
|
ext.lineN += lendiff; |
|
else if (from < ext.lineN + ext.size) |
|
display.externalMeasured = null; |
|
} |
|
} |
|
|
|
// Register a change to a single line. Type must be one of "text", |
|
// "gutter", "class", "widget" |
|
function regLineChange(cm, line, type) { |
|
cm.curOp.viewChanged = true; |
|
var display = cm.display, ext = cm.display.externalMeasured; |
|
if (ext && line >= ext.lineN && line < ext.lineN + ext.size) |
|
display.externalMeasured = null; |
|
|
|
if (line < display.viewFrom || line >= display.viewTo) return; |
|
var lineView = display.view[findViewIndex(cm, line)]; |
|
if (lineView.node == null) return; |
|
var arr = lineView.changes || (lineView.changes = []); |
|
if (indexOf(arr, type) == -1) arr.push(type); |
|
} |
|
|
|
// Clear the view. |
|
function resetView(cm) { |
|
cm.display.viewFrom = cm.display.viewTo = cm.doc.first; |
|
cm.display.view = []; |
|
cm.display.viewOffset = 0; |
|
} |
|
|
|
// Find the view element corresponding to a given line. Return null |
|
// when the line isn't visible. |
|
function findViewIndex(cm, n) { |
|
if (n >= cm.display.viewTo) return null; |
|
n -= cm.display.viewFrom; |
|
if (n < 0) return null; |
|
var view = cm.display.view; |
|
for (var i = 0; i < view.length; i++) { |
|
n -= view[i].size; |
|
if (n < 0) return i; |
|
} |
|
} |
|
|
|
function viewCuttingPoint(cm, oldN, newN, dir) { |
|
var index = findViewIndex(cm, oldN), diff, view = cm.display.view; |
|
if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) |
|
return {index: index, lineN: newN}; |
|
for (var i = 0, n = cm.display.viewFrom; i < index; i++) |
|
n += view[i].size; |
|
if (n != oldN) { |
|
if (dir > 0) { |
|
if (index == view.length - 1) return null; |
|
diff = (n + view[index].size) - oldN; |
|
index++; |
|
} else { |
|
diff = n - oldN; |
|
} |
|
oldN += diff; newN += diff; |
|
} |
|
while (visualLineNo(cm.doc, newN) != newN) { |
|
if (index == (dir < 0 ? 0 : view.length - 1)) return null; |
|
newN += dir * view[index - (dir < 0 ? 1 : 0)].size; |
|
index += dir; |
|
} |
|
return {index: index, lineN: newN}; |
|
} |
|
|
|
// Force the view to cover a given range, adding empty view element |
|
// or clipping off existing ones as needed. |
|
function adjustView(cm, from, to) { |
|
var display = cm.display, view = display.view; |
|
if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { |
|
display.view = buildViewArray(cm, from, to); |
|
display.viewFrom = from; |
|
} else { |
|
if (display.viewFrom > from) |
|
display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); |
|
else if (display.viewFrom < from) |
|
display.view = display.view.slice(findViewIndex(cm, from)); |
|
display.viewFrom = from; |
|
if (display.viewTo < to) |
|
display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); |
|
else if (display.viewTo > to) |
|
display.view = display.view.slice(0, findViewIndex(cm, to)); |
|
} |
|
display.viewTo = to; |
|
} |
|
|
|
// Count the number of lines in the view whose DOM representation is |
|
// out of date (or nonexistent). |
|
function countDirtyView(cm) { |
|
var view = cm.display.view, dirty = 0; |
|
for (var i = 0; i < view.length; i++) { |
|
var lineView = view[i]; |
|
if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; |
|
} |
|
return dirty; |
|
} |
|
|
|
// EVENT HANDLERS |
|
|
|
// Attach the necessary event handlers when initializing the editor |
|
function registerEventHandlers(cm) { |
|
var d = cm.display; |
|
on(d.scroller, "mousedown", operation(cm, onMouseDown)); |
|
// Older IE's will not fire a second mousedown for a double click |
|
if (ie && ie_version < 11) |
|
on(d.scroller, "dblclick", operation(cm, function(e) { |
|
if (signalDOMEvent(cm, e)) return; |
|
var pos = posFromMouse(cm, e); |
|
if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; |
|
e_preventDefault(e); |
|
var word = cm.findWordAt(pos); |
|
extendSelection(cm.doc, word.anchor, word.head); |
|
})); |
|
else |
|
on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); |
|
// Some browsers fire contextmenu *after* opening the menu, at |
|
// which point we can't mess with it anymore. Context menu is |
|
// handled in onMouseDown for these browsers. |
|
if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); |
|
|
|
// Used to suppress mouse event handling when a touch happens |
|
var touchFinished, prevTouch = {end: 0}; |
|
function finishTouch() { |
|
if (d.activeTouch) { |
|
touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000); |
|
prevTouch = d.activeTouch; |
|
prevTouch.end = +new Date; |
|
} |
|
}; |
|
function isMouseLikeTouchEvent(e) { |
|
if (e.touches.length != 1) return false; |
|
var touch = e.touches[0]; |
|
return touch.radiusX <= 1 && touch.radiusY <= 1; |
|
} |
|
function farAway(touch, other) { |
|
if (other.left == null) return true; |
|
var dx = other.left - touch.left, dy = other.top - touch.top; |
|
return dx * dx + dy * dy > 20 * 20; |
|
} |
|
on(d.scroller, "touchstart", function(e) { |
|
if (!isMouseLikeTouchEvent(e)) { |
|
clearTimeout(touchFinished); |
|
var now = +new Date; |
|
d.activeTouch = {start: now, moved: false, |
|
prev: now - prevTouch.end <= 300 ? prevTouch : null}; |
|
if (e.touches.length == 1) { |
|
d.activeTouch.left = e.touches[0].pageX; |
|
d.activeTouch.top = e.touches[0].pageY; |
|
} |
|
} |
|
}); |
|
on(d.scroller, "touchmove", function() { |
|
if (d.activeTouch) d.activeTouch.moved = true; |
|
}); |
|
on(d.scroller, "touchend", function(e) { |
|
var touch = d.activeTouch; |
|
if (touch && !eventInWidget(d, e) && touch.left != null && |
|
!touch.moved && new Date - touch.start < 300) { |
|
var pos = cm.coordsChar(d.activeTouch, "page"), range; |
|
if (!touch.prev || farAway(touch, touch.prev)) // Single tap |
|
range = new Range(pos, pos); |
|
else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap |
|
range = cm.findWordAt(pos); |
|
else // Triple tap |
|
range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); |
|
cm.setSelection(range.anchor, range.head); |
|
cm.focus(); |
|
e_preventDefault(e); |
|
} |
|
finishTouch(); |
|
}); |
|
on(d.scroller, "touchcancel", finishTouch); |
|
|
|
// Sync scrolling between fake scrollbars and real scrollable |
|
// area, ensure viewport is updated when scrolling. |
|
on(d.scroller, "scroll", function() { |
|
if (d.scroller.clientHeight) { |
|
setScrollTop(cm, d.scroller.scrollTop); |
|
setScrollLeft(cm, d.scroller.scrollLeft, true); |
|
signal(cm, "scroll", cm); |
|
} |
|
}); |
|
|
|
// Listen to wheel events in order to try and update the viewport on time. |
|
on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); |
|
on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); |
|
|
|
// Prevent wrapper from ever scrolling |
|
on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); |
|
|
|
d.dragFunctions = { |
|
simple: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, |
|
start: function(e){onDragStart(cm, e);}, |
|
drop: operation(cm, onDrop) |
|
}; |
|
|
|
var inp = d.input.getField(); |
|
on(inp, "keyup", function(e) { onKeyUp.call(cm, e); }); |
|
on(inp, "keydown", operation(cm, onKeyDown)); |
|
on(inp, "keypress", operation(cm, onKeyPress)); |
|
on(inp, "focus", bind(onFocus, cm)); |
|
on(inp, "blur", bind(onBlur, cm)); |
|
} |
|
|
|
function dragDropChanged(cm, value, old) { |
|
var wasOn = old && old != CodeMirror.Init; |
|
if (!value != !wasOn) { |
|
var funcs = cm.display.dragFunctions; |
|
var toggle = value ? on : off; |
|
toggle(cm.display.scroller, "dragstart", funcs.start); |
|
toggle(cm.display.scroller, "dragenter", funcs.simple); |
|
toggle(cm.display.scroller, "dragover", funcs.simple); |
|
toggle(cm.display.scroller, "drop", funcs.drop); |
|
} |
|
} |
|
|
|
// Called when the window resizes |
|
function onResize(cm) { |
|
var d = cm.display; |
|
if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) |
|
return; |
|
// Might be a text scaling operation, clear size caches. |
|
d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; |
|
d.scrollbarsClipped = false; |
|
cm.setSize(); |
|
} |
|
|
|
// MOUSE EVENTS |
|
|
|
// Return true when the given mouse event happened in a widget |
|
function eventInWidget(display, e) { |
|
for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { |
|
if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || |
|
(n.parentNode == display.sizer && n != display.mover)) |
|
return true; |
|
} |
|
} |
|
|
|
// Given a mouse event, find the corresponding position. If liberal |
|
// is false, it checks whether a gutter or scrollbar was clicked, |
|
// and returns null if it was. forRect is used by rectangular |
|
// selections, and tries to estimate a character position even for |
|
// coordinates beyond the right of the text. |
|
function posFromMouse(cm, e, liberal, forRect) { |
|
var display = cm.display; |
|
if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null; |
|
|
|
var x, y, space = display.lineSpace.getBoundingClientRect(); |
|
// Fails unpredictably on IE[67] when mouse is dragged around quickly. |
|
try { x = e.clientX - space.left; y = e.clientY - space.top; } |
|
catch (e) { return null; } |
|
var coords = coordsChar(cm, x, y), line; |
|
if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { |
|
var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; |
|
coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); |
|
} |
|
return coords; |
|
} |
|
|
|
// A mouse down can be a single click, double click, triple click, |
|
// start of selection drag, start of text drag, new cursor |
|
// (ctrl-click), rectangle drag (alt-drag), or xwin |
|
// middle-click-paste. Or it might be a click on something we should |
|
// not interfere with, such as a scrollbar or widget. |
|
function onMouseDown(e) { |
|
var cm = this, display = cm.display; |
|
if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return; |
|
display.shift = e.shiftKey; |
|
|
|
if (eventInWidget(display, e)) { |
|
if (!webkit) { |
|
// Briefly turn off draggability, to allow widgets to do |
|
// normal dragging things. |
|
display.scroller.draggable = false; |
|
setTimeout(function(){display.scroller.draggable = true;}, 100); |
|
} |
|
return; |
|
} |
|
if (clickInGutter(cm, e)) return; |
|
var start = posFromMouse(cm, e); |
|
window.focus(); |
|
|
|
switch (e_button(e)) { |
|
case 1: |
|
if (start) |
|
leftButtonDown(cm, e, start); |
|
else if (e_target(e) == display.scroller) |
|
e_preventDefault(e); |
|
break; |
|
case 2: |
|
if (webkit) cm.state.lastMiddleDown = +new Date; |
|
if (start) extendSelection(cm.doc, start); |
|
setTimeout(function() {display.input.focus();}, 20); |
|
e_preventDefault(e); |
|
break; |
|
case 3: |
|
if (captureRightClick) onContextMenu(cm, e); |
|
else delayBlurEvent(cm); |
|
break; |
|
} |
|
} |
|
|
|
var lastClick, lastDoubleClick; |
|
function leftButtonDown(cm, e, start) { |
|
if (ie) setTimeout(bind(ensureFocus, cm), 0); |
|
else cm.curOp.focus = activeElt(); |
|
|
|
var now = +new Date, type; |
|
if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { |
|
type = "triple"; |
|
} else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { |
|
type = "double"; |
|
lastDoubleClick = {time: now, pos: start}; |
|
} else { |
|
type = "single"; |
|
lastClick = {time: now, pos: start}; |
|
} |
|
|
|
var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; |
|
if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && |
|
type == "single" && (contained = sel.contains(start)) > -1 && |
|
(cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) && |
|
(cmp(contained.to(), start) > 0 || start.xRel < 0)) |
|
leftButtonStartDrag(cm, e, start, modifier); |
|
else |
|
leftButtonSelect(cm, e, start, type, modifier); |
|
} |
|
|
|
// Start a text drag. When it ends, see if any dragging actually |
|
// happen, and treat as a click if it didn't. |
|
function leftButtonStartDrag(cm, e, start, modifier) { |
|
var display = cm.display, startTime = +new Date; |
|
var dragEnd = operation(cm, function(e2) { |
|
if (webkit) display.scroller.draggable = false; |
|
cm.state.draggingText = false; |
|
off(document, "mouseup", dragEnd); |
|
off(display.scroller, "drop", dragEnd); |
|
if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { |
|
e_preventDefault(e2); |
|
if (!modifier && +new Date - 200 < startTime) |
|
extendSelection(cm.doc, start); |
|
// Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) |
|
if (webkit || ie && ie_version == 9) |
|
setTimeout(function() {document.body.focus(); display.input.focus();}, 20); |
|
else |
|
display.input.focus(); |
|
} |
|
}); |
|
// Let the drag handler handle this. |
|
if (webkit) display.scroller.draggable = true; |
|
cm.state.draggingText = dragEnd; |
|
// IE's approach to draggable |
|
if (display.scroller.dragDrop) display.scroller.dragDrop(); |
|
on(document, "mouseup", dragEnd); |
|
on(display.scroller, "drop", dragEnd); |
|
} |
|
|
|
// Normal selection, as opposed to text dragging. |
|
function leftButtonSelect(cm, e, start, type, addNew) { |
|
var display = cm.display, doc = cm.doc; |
|
e_preventDefault(e); |
|
|
|
var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; |
|
if (addNew && !e.shiftKey) { |
|
ourIndex = doc.sel.contains(start); |
|
if (ourIndex > -1) |
|
ourRange = ranges[ourIndex]; |
|
else |
|
ourRange = new Range(start, start); |
|
} else { |
|
ourRange = doc.sel.primary(); |
|
ourIndex = doc.sel.primIndex; |
|
} |
|
|
|
if (e.altKey) { |
|
type = "rect"; |
|
if (!addNew) ourRange = new Range(start, start); |
|
start = posFromMouse(cm, e, true, true); |
|
ourIndex = -1; |
|
} else if (type == "double") { |
|
var word = cm.findWordAt(start); |
|
if (cm.display.shift || doc.extend) |
|
ourRange = extendRange(doc, ourRange, word.anchor, word.head); |
|
else |
|
ourRange = word; |
|
} else if (type == "triple") { |
|
var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); |
|
if (cm.display.shift || doc.extend) |
|
ourRange = extendRange(doc, ourRange, line.anchor, line.head); |
|
else |
|
ourRange = line; |
|
} else { |
|
ourRange = extendRange(doc, ourRange, start); |
|
} |
|
|
|
if (!addNew) { |
|
ourIndex = 0; |
|
setSelection(doc, new Selection([ourRange], 0), sel_mouse); |
|
startSel = doc.sel; |
|
} else if (ourIndex == -1) { |
|
ourIndex = ranges.length; |
|
setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), |
|
{scroll: false, origin: "*mouse"}); |
|
} else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { |
|
setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); |
|
startSel = doc.sel; |
|
} else { |
|
replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); |
|
} |
|
|
|
var lastPos = start; |
|
function extendTo(pos) { |
|
if (cmp(lastPos, pos) == 0) return; |
|
lastPos = pos; |
|
|
|
if (type == "rect") { |
|
var ranges = [], tabSize = cm.options.tabSize; |
|
var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); |
|
var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); |
|
var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); |
|
for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); |
|
line <= end; line++) { |
|
var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); |
|
if (left == right) |
|
ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); |
|
else if (text.length > leftPos) |
|
ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); |
|
} |
|
if (!ranges.length) ranges.push(new Range(start, start)); |
|
setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), |
|
{origin: "*mouse", scroll: false}); |
|
cm.scrollIntoView(pos); |
|
} else { |
|
var oldRange = ourRange; |
|
var anchor = oldRange.anchor, head = pos; |
|
if (type != "single") { |
|
if (type == "double") |
|
var range = cm.findWordAt(pos); |
|
else |
|
var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); |
|
if (cmp(range.anchor, anchor) > 0) { |
|
head = range.head; |
|
anchor = minPos(oldRange.from(), range.anchor); |
|
} else { |
|
head = range.anchor; |
|
anchor = maxPos(oldRange.to(), range.head); |
|
} |
|
} |
|
var ranges = startSel.ranges.slice(0); |
|
ranges[ourIndex] = new Range(clipPos(doc, anchor), head); |
|
setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); |
|
} |
|
} |
|
|
|
var editorSize = display.wrapper.getBoundingClientRect(); |
|
// Used to ensure timeout re-tries don't fire when another extend |
|
// happened in the meantime (clearTimeout isn't reliable -- at |
|
// least on Chrome, the timeouts still happen even when cleared, |
|
// if the clear happens after their scheduled firing time). |
|
var counter = 0; |
|
|
|
function extend(e) { |
|
var curCount = ++counter; |
|
var cur = posFromMouse(cm, e, true, type == "rect"); |
|
if (!cur) return; |
|
if (cmp(cur, lastPos) != 0) { |
|
cm.curOp.focus = activeElt(); |
|
extendTo(cur); |
|
var visible = visibleLines(display, doc); |
|
if (cur.line >= visible.to || cur.line < visible.from) |
|
setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); |
|
} else { |
|
var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; |
|
if (outside) setTimeout(operation(cm, function() { |
|
if (counter != curCount) return; |
|
display.scroller.scrollTop += outside; |
|
extend(e); |
|
}), 50); |
|
} |
|
} |
|
|
|
function done(e) { |
|
counter = Infinity; |
|
e_preventDefault(e); |
|
display.input.focus(); |
|
off(document, "mousemove", move); |
|
off(document, "mouseup", up); |
|
doc.history.lastSelOrigin = null; |
|
} |
|
|
|
var move = operation(cm, function(e) { |
|
if (!e_button(e)) done(e); |
|
else extend(e); |
|
}); |
|
var up = operation(cm, done); |
|
on(document, "mousemove", move); |
|
on(document, "mouseup", up); |
|
} |
|
|
|
// Determines whether an event happened in the gutter, and fires the |
|
// handlers for the corresponding event. |
|
function gutterEvent(cm, e, type, prevent, signalfn) { |
|
try { var mX = e.clientX, mY = e.clientY; } |
|
catch(e) { return false; } |
|
if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; |
|
if (prevent) e_preventDefault(e); |
|
|
|
var display = cm.display; |
|
var lineBox = display.lineDiv.getBoundingClientRect(); |
|
|
|
if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); |
|
mY -= lineBox.top - display.viewOffset; |
|
|
|
for (var i = 0; i < cm.options.gutters.length; ++i) { |
|
var g = display.gutters.childNodes[i]; |
|
if (g && g.getBoundingClientRect().right >= mX) { |
|
var line = lineAtHeight(cm.doc, mY); |
|
var gutter = cm.options.gutters[i]; |
|
signalfn(cm, type, cm, line, gutter, e); |
|
return e_defaultPrevented(e); |
|
} |
|
} |
|
} |
|
|
|
function clickInGutter(cm, e) { |
|
return gutterEvent(cm, e, "gutterClick", true, signalLater); |
|
} |
|
|
|
// Kludge to work around strange IE behavior where it'll sometimes |
|
// re-fire a series of drag-related events right after the drop (#1551) |
|
var lastDrop = 0; |
|
|
|
function onDrop(e) { |
|
var cm = this; |
|
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) |
|
return; |
|
e_preventDefault(e); |
|
if (ie) lastDrop = +new Date; |
|
var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; |
|
if (!pos || isReadOnly(cm)) return; |
|
// Might be a file drop, in which case we simply extract the text |
|
// and insert it. |
|
if (files && files.length && window.FileReader && window.File) { |
|
var n = files.length, text = Array(n), read = 0; |
|
var loadFile = function(file, i) { |
|
var reader = new FileReader; |
|
reader.onload = operation(cm, function() { |
|
text[i] = reader.result; |
|
if (++read == n) { |
|
pos = clipPos(cm.doc, pos); |
|
var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; |
|
makeChange(cm.doc, change); |
|
setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); |
|
} |
|
}); |
|
reader.readAsText(file); |
|
}; |
|
for (var i = 0; i < n; ++i) loadFile(files[i], i); |
|
} else { // Normal drop |
|
// Don't do a replace if the drop happened inside of the selected text. |
|
if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { |
|
cm.state.draggingText(e); |
|
// Ensure the editor is re-focused |
|
setTimeout(function() {cm.display.input.focus();}, 20); |
|
return; |
|
} |
|
try { |
|
var text = e.dataTransfer.getData("Text"); |
|
if (text) { |
|
if (cm.state.draggingText && !(mac ? e.altKey : e.ctrlKey)) |
|
var selected = cm.listSelections(); |
|
setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); |
|
if (selected) for (var i = 0; i < selected.length; ++i) |
|
replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); |
|
cm.replaceSelection(text, "around", "paste"); |
|
cm.display.input.focus(); |
|
} |
|
} |
|
catch(e){} |
|
} |
|
} |
|
|
|
function onDragStart(cm, e) { |
|
if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } |
|
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; |
|
|
|
e.dataTransfer.setData("Text", cm.getSelection()); |
|
|
|
// Use dummy image instead of default browsers image. |
|
// Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. |
|
if (e.dataTransfer.setDragImage && !safari) { |
|
var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); |
|
img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; |
|
if (presto) { |
|
img.width = img.height = 1; |
|
cm.display.wrapper.appendChild(img); |
|
// Force a relayout, or Opera won't use our image for some obscure reason |
|
img._top = img.offsetTop; |
|
} |
|
e.dataTransfer.setDragImage(img, 0, 0); |
|
if (presto) img.parentNode.removeChild(img); |
|
} |
|
} |
|
|
|
// SCROLL EVENTS |
|
|
|
// Sync the scrollable area and scrollbars, ensure the viewport |
|
// covers the visible area. |
|
function setScrollTop(cm, val) { |
|
if (Math.abs(cm.doc.scrollTop - val) < 2) return; |
|
cm.doc.scrollTop = val; |
|
if (!gecko) updateDisplaySimple(cm, {top: val}); |
|
if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; |
|
cm.display.scrollbars.setScrollTop(val); |
|
if (gecko) updateDisplaySimple(cm); |
|
startWorker(cm, 100); |
|
} |
|
// Sync scroller and scrollbar, ensure the gutter elements are |
|
// aligned. |
|
function setScrollLeft(cm, val, isScroller) { |
|
if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; |
|
val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); |
|
cm.doc.scrollLeft = val; |
|
alignHorizontally(cm); |
|
if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; |
|
cm.display.scrollbars.setScrollLeft(val); |
|
} |
|
|
|
// Since the delta values reported on mouse wheel events are |
|
// unstandardized between browsers and even browser versions, and |
|
// generally horribly unpredictable, this code starts by measuring |
|
// the scroll effect that the first few mouse wheel events have, |
|
// and, from that, detects the way it can convert deltas to pixel |
|
// offsets afterwards. |
|
// |
|
// The reason we want to know the amount a wheel event will scroll |
|
// is that it gives us a chance to update the display before the |
|
// actual scrolling happens, reducing flickering. |
|
|
|
var wheelSamples = 0, wheelPixelsPerUnit = null; |
|
// Fill in a browser-detected starting value on browsers where we |
|
// know one. These don't have to be accurate -- the result of them |
|
// being wrong would just be a slight flicker on the first wheel |
|
// scroll (if it is large enough). |
|
if (ie) wheelPixelsPerUnit = -.53; |
|
else if (gecko) wheelPixelsPerUnit = 15; |
|
else if (chrome) wheelPixelsPerUnit = -.7; |
|
else if (safari) wheelPixelsPerUnit = -1/3; |
|
|
|
var wheelEventDelta = function(e) { |
|
var dx = e.wheelDeltaX, dy = e.wheelDeltaY; |
|
if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; |
|
if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; |
|
else if (dy == null) dy = e.wheelDelta; |
|
return {x: dx, y: dy}; |
|
}; |
|
CodeMirror.wheelEventPixels = function(e) { |
|
var delta = wheelEventDelta(e); |
|
delta.x *= wheelPixelsPerUnit; |
|
delta.y *= wheelPixelsPerUnit; |
|
return delta; |
|
}; |
|
|
|
function onScrollWheel(cm, e) { |
|
var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; |
|
|
|
var display = cm.display, scroll = display.scroller; |
|
// Quit if there's nothing to scroll here |
|
if (!(dx && scroll.scrollWidth > scroll.clientWidth || |
|
dy && scroll.scrollHeight > scroll.clientHeight)) return; |
|
|
|
// Webkit browsers on OS X abort momentum scrolls when the target |
|
// of the scroll event is removed from the scrollable element. |
|
// This hack (see related code in patchDisplay) makes sure the |
|
// element is kept around. |
|
if (dy && mac && webkit) { |
|
outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { |
|
for (var i = 0; i < view.length; i++) { |
|
if (view[i].node == cur) { |
|
cm.display.currentWheelTarget = cur; |
|
break outer; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// On some browsers, horizontal scrolling will cause redraws to |
|
// happen before the gutter has been realigned, causing it to |
|
// wriggle around in a most unseemly way. When we have an |
|
// estimated pixels/delta value, we just handle horizontal |
|
// scrolling entirely here. It'll be slightly off from native, but |
|
// better than glitching out. |
|
if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { |
|
if (dy) |
|
setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); |
|
setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); |
|
e_preventDefault(e); |
|
display.wheelStartX = null; // Abort measurement, if in progress |
|
return; |
|
} |
|
|
|
// 'Project' the visible viewport to cover the area that is being |
|
// scrolled into view (if we know enough to estimate it). |
|
if (dy && wheelPixelsPerUnit != null) { |
|
var pixels = dy * wheelPixelsPerUnit; |
|
var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; |
|
if (pixels < 0) top = Math.max(0, top + pixels - 50); |
|
else bot = Math.min(cm.doc.height, bot + pixels + 50); |
|
updateDisplaySimple(cm, {top: top, bottom: bot}); |
|
} |
|
|
|
if (wheelSamples < 20) { |
|
if (display.wheelStartX == null) { |
|
display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; |
|
display.wheelDX = dx; display.wheelDY = dy; |
|
setTimeout(function() { |
|
if (display.wheelStartX == null) return; |
|
var movedX = scroll.scrollLeft - display.wheelStartX; |
|
var movedY = scroll.scrollTop - display.wheelStartY; |
|
var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || |
|
(movedX && display.wheelDX && movedX / display.wheelDX); |
|
display.wheelStartX = display.wheelStartY = null; |
|
if (!sample) return; |
|
wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); |
|
++wheelSamples; |
|
}, 200); |
|
} else { |
|
display.wheelDX += dx; display.wheelDY += dy; |
|
} |
|
} |
|
} |
|
|
|
// KEY EVENTS |
|
|
|
// Run a handler that was bound to a key. |
|
function doHandleBinding(cm, bound, dropShift) { |
|
if (typeof bound == "string") { |
|
bound = commands[bound]; |
|
if (!bound) return false; |
|
} |
|
// Ensure previous input has been read, so that the handler sees a |
|
// consistent view of the document |
|
cm.display.input.ensurePolled(); |
|
var prevShift = cm.display.shift, done = false; |
|
try { |
|
if (isReadOnly(cm)) cm.state.suppressEdits = true; |
|
if (dropShift) cm.display.shift = false; |
|
done = bound(cm) != Pass; |
|
} finally { |
|
cm.display.shift = prevShift; |
|
cm.state.suppressEdits = false; |
|
} |
|
return done; |
|
} |
|
|
|
function lookupKeyForEditor(cm, name, handle) { |
|
for (var i = 0; i < cm.state.keyMaps.length; i++) { |
|
var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); |
|
if (result) return result; |
|
} |
|
return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) |
|
|| lookupKey(name, cm.options.keyMap, handle, cm); |
|
} |
|
|
|
var stopSeq = new Delayed; |
|
function dispatchKey(cm, name, e, handle) { |
|
var seq = cm.state.keySeq; |
|
if (seq) { |
|
if (isModifierKey(name)) return "handled"; |
|
stopSeq.set(50, function() { |
|
if (cm.state.keySeq == seq) { |
|
cm.state.keySeq = null; |
|
cm.display.input.reset(); |
|
} |
|
}); |
|
name = seq + " " + name; |
|
} |
|
var result = lookupKeyForEditor(cm, name, handle); |
|
|
|
if (result == "multi") |
|
cm.state.keySeq = name; |
|
if (result == "handled") |
|
signalLater(cm, "keyHandled", cm, name, e); |
|
|
|
if (result == "handled" || result == "multi") { |
|
e_preventDefault(e); |
|
restartBlink(cm); |
|
} |
|
|
|
if (seq && !result && /\'$/.test(name)) { |
|
e_preventDefault(e); |
|
return true; |
|
} |
|
return !!result; |
|
} |
|
|
|
// Handle a key from the keydown event. |
|
function handleKeyBinding(cm, e) { |
|
var name = keyName(e, true); |
|
if (!name) return false; |
|
|
|
if (e.shiftKey && !cm.state.keySeq) { |
|
// First try to resolve full name (including 'Shift-'). Failing |
|
// that, see if there is a cursor-motion command (starting with |
|
// 'go') bound to the keyname without 'Shift-'. |
|
return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);}) |
|
|| dispatchKey(cm, name, e, function(b) { |
|
if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) |
|
return doHandleBinding(cm, b); |
|
}); |
|
} else { |
|
return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); }); |
|
} |
|
} |
|
|
|
// Handle a key from the keypress event |
|
function handleCharBinding(cm, e, ch) { |
|
return dispatchKey(cm, "'" + ch + "'", e, |
|
function(b) { return doHandleBinding(cm, b, true); }); |
|
} |
|
|
|
var lastStoppedKey = null; |
|
function onKeyDown(e) { |
|
var cm = this; |
|
cm.curOp.focus = activeElt(); |
|
if (signalDOMEvent(cm, e)) return; |
|
// IE does strange things with escape. |
|
if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; |
|
var code = e.keyCode; |
|
cm.display.shift = code == 16 || e.shiftKey; |
|
var handled = handleKeyBinding(cm, e); |
|
if (presto) { |
|
lastStoppedKey = handled ? code : null; |
|
// Opera has no cut event... we try to at least catch the key combo |
|
if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) |
|
cm.replaceSelection("", null, "cut"); |
|
} |
|
|
|
// Turn mouse into crosshair when Alt is held on Mac. |
|
if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) |
|
showCrossHair(cm); |
|
} |
|
|
|
function showCrossHair(cm) { |
|
var lineDiv = cm.display.lineDiv; |
|
addClass(lineDiv, "CodeMirror-crosshair"); |
|
|
|
function up(e) { |
|
if (e.keyCode == 18 || !e.altKey) { |
|
rmClass(lineDiv, "CodeMirror-crosshair"); |
|
off(document, "keyup", up); |
|
off(document, "mouseover", up); |
|
} |
|
} |
|
on(document, "keyup", up); |
|
on(document, "mouseover", up); |
|
} |
|
|
|
function onKeyUp(e) { |
|
if (e.keyCode == 16) this.doc.sel.shift = false; |
|
signalDOMEvent(this, e); |
|
} |
|
|
|
function onKeyPress(e) { |
|
var cm = this; |
|
if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; |
|
var keyCode = e.keyCode, charCode = e.charCode; |
|
if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} |
|
if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return; |
|
var ch = String.fromCharCode(charCode == null ? keyCode : charCode); |
|
if (handleCharBinding(cm, e, ch)) return; |
|
cm.display.input.onKeyPress(e); |
|
} |
|
|
|
// FOCUS/BLUR EVENTS |
|
|
|
function delayBlurEvent(cm) { |
|
cm.state.delayingBlurEvent = true; |
|
setTimeout(function() { |
|
if (cm.state.delayingBlurEvent) { |
|
cm.state.delayingBlurEvent = false; |
|
onBlur(cm); |
|
} |
|
}, 100); |
|
} |
|
|
|
function onFocus(cm) { |
|
if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false; |
|
|
|
if (cm.options.readOnly == "nocursor") return; |
|
if (!cm.state.focused) { |
|
signal(cm, "focus", cm); |
|
cm.state.focused = true; |
|
addClass(cm.display.wrapper, "CodeMirror-focused"); |
|
// This test prevents this from firing when a context |
|
// menu is closed (since the input reset would kill the |
|
// select-all detection hack) |
|
if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { |
|
cm.display.input.reset(); |
|
if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 |
|
} |
|
cm.display.input.receivedFocus(); |
|
} |
|
restartBlink(cm); |
|
} |
|
function onBlur(cm) { |
|
if (cm.state.delayingBlurEvent) return; |
|
|
|
if (cm.state.focused) { |
|
signal(cm, "blur", cm); |
|
cm.state.focused = false; |
|
rmClass(cm.display.wrapper, "CodeMirror-focused"); |
|
} |
|
clearInterval(cm.display.blinker); |
|
setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); |
|
} |
|
|
|
// CONTEXT MENU HANDLING |
|
|
|
// To make the context menu work, we need to briefly unhide the |
|
// textarea (making it as unobtrusive as possible) to let the |
|
// right-click take effect on it. |
|
function onContextMenu(cm, e) { |
|
if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; |
|
cm.display.input.onContextMenu(e); |
|
} |
|
|
|
function contextMenuInGutter(cm, e) { |
|
if (!hasHandler(cm, "gutterContextMenu")) return false; |
|
return gutterEvent(cm, e, "gutterContextMenu", false, signal); |
|
} |
|
|
|
// UPDATING |
|
|
|
// Compute the position of the end of a change (its 'to' property |
|
// refers to the pre-change end). |
|
var changeEnd = CodeMirror.changeEnd = function(change) { |
|
if (!change.text) return change.to; |
|
return Pos(change.from.line + change.text.length - 1, |
|
lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); |
|
}; |
|
|
|
// Adjust a position to refer to the post-change position of the |
|
// same text, or the end of the change if the change covers it. |
|
function adjustForChange(pos, change) { |
|
if (cmp(pos, change.from) < 0) return pos; |
|
if (cmp(pos, change.to) <= 0) return changeEnd(change); |
|
|
|
var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; |
|
if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; |
|
return Pos(line, ch); |
|
} |
|
|
|
function computeSelAfterChange(doc, change) { |
|
var out = []; |
|
for (var i = 0; i < doc.sel.ranges.length; i++) { |
|
var range = doc.sel.ranges[i]; |
|
out.push(new Range(adjustForChange(range.anchor, change), |
|
adjustForChange(range.head, change))); |
|
} |
|
return normalizeSelection(out, doc.sel.primIndex); |
|
} |
|
|
|
function offsetPos(pos, old, nw) { |
|
if (pos.line == old.line) |
|
return Pos(nw.line, pos.ch - old.ch + nw.ch); |
|
else |
|
return Pos(nw.line + (pos.line - old.line), pos.ch); |
|
} |
|
|
|
// Used by replaceSelections to allow moving the selection to the |
|
// start or around the replaced test. Hint may be "start" or "around". |
|
function computeReplacedSel(doc, changes, hint) { |
|
var out = []; |
|
var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; |
|
for (var i = 0; i < changes.length; i++) { |
|
var change = changes[i]; |
|
var from = offsetPos(change.from, oldPrev, newPrev); |
|
var to = offsetPos(changeEnd(change), oldPrev, newPrev); |
|
oldPrev = change.to; |
|
newPrev = to; |
|
if (hint == "around") { |
|
var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; |
|
out[i] = new Range(inv ? to : from, inv ? from : to); |
|
} else { |
|
out[i] = new Range(from, from); |
|
} |
|
} |
|
return new Selection(out, doc.sel.primIndex); |
|
} |
|
|
|
// Allow "beforeChange" event handlers to influence a change |
|
function filterChange(doc, change, update) { |
|
var obj = { |
|
canceled: false, |
|
from: change.from, |
|
to: change.to, |
|
text: change.text, |
|
origin: change.origin, |
|
cancel: function() { this.canceled = true; } |
|
}; |
|
if (update) obj.update = function(from, to, text, origin) { |
|
if (from) this.from = clipPos(doc, from); |
|
if (to) this.to = clipPos(doc, to); |
|
if (text) this.text = text; |
|
if (origin !== undefined) this.origin = origin; |
|
}; |
|
signal(doc, "beforeChange", doc, obj); |
|
if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); |
|
|
|
if (obj.canceled) return null; |
|
return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; |
|
} |
|
|
|
// Apply a change to a document, and add it to the document's |
|
// history, and propagating it to all linked documents. |
|
function makeChange(doc, change, ignoreReadOnly) { |
|
if (doc.cm) { |
|
if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); |
|
if (doc.cm.state.suppressEdits) return; |
|
} |
|
|
|
if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { |
|
change = filterChange(doc, change, true); |
|
if (!change) return; |
|
} |
|
|
|
// Possibly split or suppress the update based on the presence |
|
// of read-only spans in its range. |
|
var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); |
|
if (split) { |
|
for (var i = split.length - 1; i >= 0; --i) |
|
makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); |
|
} else { |
|
makeChangeInner(doc, change); |
|
} |
|
} |
|
|
|
function makeChangeInner(doc, change) { |
|
if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; |
|
var selAfter = computeSelAfterChange(doc, change); |
|
addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); |
|
|
|
makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); |
|
var rebased = []; |
|
|
|
linkedDocs(doc, function(doc, sharedHist) { |
|
if (!sharedHist && indexOf(rebased, doc.history) == -1) { |
|
rebaseHist(doc.history, change); |
|
rebased.push(doc.history); |
|
} |
|
makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); |
|
}); |
|
} |
|
|
|
// Revert a change stored in a document's history. |
|
function makeChangeFromHistory(doc, type, allowSelectionOnly) { |
|
if (doc.cm && doc.cm.state.suppressEdits) return; |
|
|
|
var hist = doc.history, event, selAfter = doc.sel; |
|
var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; |
|
|
|
// Verify that there is a useable event (so that ctrl-z won't |
|
// needlessly clear selection events) |
|
for (var i = 0; i < source.length; i++) { |
|
event = source[i]; |
|
if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) |
|
break; |
|
} |
|
if (i == source.length) return; |
|
hist.lastOrigin = hist.lastSelOrigin = null; |
|
|
|
for (;;) { |
|
event = source.pop(); |
|
if (event.ranges) { |
|
pushSelectionToHistory(event, dest); |
|
if (allowSelectionOnly && !event.equals(doc.sel)) { |
|
setSelection(doc, event, {clearRedo: false}); |
|
return; |
|
} |
|
selAfter = event; |
|
} |
|
else break; |
|
} |
|
|
|
// Build up a reverse change object to add to the opposite history |
|
// stack (redo when undoing, and vice versa). |
|
var antiChanges = []; |
|
pushSelectionToHistory(selAfter, dest); |
|
dest.push({changes: antiChanges, generation: hist.generation}); |
|
hist.generation = event.generation || ++hist.maxGeneration; |
|
|
|
var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); |
|
|
|
for (var i = event.changes.length - 1; i >= 0; --i) { |
|
var change = event.changes[i]; |
|
change.origin = type; |
|
if (filter && !filterChange(doc, change, false)) { |
|
source.length = 0; |
|
return; |
|
} |
|
|
|
antiChanges.push(historyChangeFromChange(doc, change)); |
|
|
|
var after = i ? computeSelAfterChange(doc, change) : lst(source); |
|
makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); |
|
if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); |
|
var rebased = []; |
|
|
|
// Propagate to the linked documents |
|
linkedDocs(doc, function(doc, sharedHist) { |
|
if (!sharedHist && indexOf(rebased, doc.history) == -1) { |
|
rebaseHist(doc.history, change); |
|
rebased.push(doc.history); |
|
} |
|
makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); |
|
}); |
|
} |
|
} |
|
|
|
// Sub-views need their line numbers shifted when text is added |
|
// above or below them in the parent document. |
|
function shiftDoc(doc, distance) { |
|
if (distance == 0) return; |
|
doc.first += distance; |
|
doc.sel = new Selection(map(doc.sel.ranges, function(range) { |
|
return new Range(Pos(range.anchor.line + distance, range.anchor.ch), |
|
Pos(range.head.line + distance, range.head.ch)); |
|
}), doc.sel.primIndex); |
|
if (doc.cm) { |
|
regChange(doc.cm, doc.first, doc.first - distance, distance); |
|
for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) |
|
regLineChange(doc.cm, l, "gutter"); |
|
} |
|
} |
|
|
|
// More lower-level change function, handling only a single document |
|
// (not linked ones). |
|
function makeChangeSingleDoc(doc, change, selAfter, spans) { |
|
if (doc.cm && !doc.cm.curOp) |
|
return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); |
|
|
|
if (change.to.line < doc.first) { |
|
shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); |
|
return; |
|
} |
|
if (change.from.line > doc.lastLine()) return; |
|
|
|
// Clip the change to the size of this doc |
|
if (change.from.line < doc.first) { |
|
var shift = change.text.length - 1 - (doc.first - change.from.line); |
|
shiftDoc(doc, shift); |
|
change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), |
|
text: [lst(change.text)], origin: change.origin}; |
|
} |
|
var last = doc.lastLine(); |
|
if (change.to.line > last) { |
|
change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), |
|
text: [change.text[0]], origin: change.origin}; |
|
} |
|
|
|
change.removed = getBetween(doc, change.from, change.to); |
|
|
|
if (!selAfter) selAfter = computeSelAfterChange(doc, change); |
|
if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); |
|
else updateDoc(doc, change, spans); |
|
setSelectionNoUndo(doc, selAfter, sel_dontScroll); |
|
} |
|
|
|
// Handle the interaction of a change to a document with the editor |
|
// that this document is part of. |
|
function makeChangeSingleDocInEditor(cm, change, spans) { |
|
var doc = cm.doc, display = cm.display, from = change.from, to = change.to; |
|
|
|
var recomputeMaxLength = false, checkWidthStart = from.line; |
|
if (!cm.options.lineWrapping) { |
|
checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); |
|
doc.iter(checkWidthStart, to.line + 1, function(line) { |
|
if (line == display.maxLine) { |
|
recomputeMaxLength = true; |
|
return true; |
|
} |
|
}); |
|
} |
|
|
|
if (doc.sel.contains(change.from, change.to) > -1) |
|
signalCursorActivity(cm); |
|
|
|
updateDoc(doc, change, spans, estimateHeight(cm)); |
|
|
|
if (!cm.options.lineWrapping) { |
|
doc.iter(checkWidthStart, from.line + change.text.length, function(line) { |
|
var len = lineLength(line); |
|
if (len > display.maxLineLength) { |
|
display.maxLine = line; |
|
display.maxLineLength = len; |
|
display.maxLineChanged = true; |
|
recomputeMaxLength = false; |
|
} |
|
}); |
|
if (recomputeMaxLength) cm.curOp.updateMaxLine = true; |
|
} |
|
|
|
// Adjust frontier, schedule worker |
|
doc.frontier = Math.min(doc.frontier, from.line); |
|
startWorker(cm, 400); |
|
|
|
var lendiff = change.text.length - (to.line - from.line) - 1; |
|
// Remember that these lines changed, for updating the display |
|
if (change.full) |
|
regChange(cm); |
|
else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) |
|
regLineChange(cm, from.line, "text"); |
|
else |
|
regChange(cm, from.line, to.line + 1, lendiff); |
|
|
|
var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); |
|
if (changeHandler || changesHandler) { |
|
var obj = { |
|
from: from, to: to, |
|
text: change.text, |
|
removed: change.removed, |
|
origin: change.origin |
|
}; |
|
if (changeHandler) signalLater(cm, "change", cm, obj); |
|
if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); |
|
} |
|
cm.display.selForContextMenu = null; |
|
} |
|
|
|
function replaceRange(doc, code, from, to, origin) { |
|
if (!to) to = from; |
|
if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } |
|
if (typeof code == "string") code = splitLines(code); |
|
makeChange(doc, {from: from, to: to, text: code, origin: origin}); |
|
} |
|
|
|
// SCROLLING THINGS INTO VIEW |
|
|
|
// If an editor sits on the top or bottom of the window, partially |
|
// scrolled out of view, this ensures that the cursor is visible. |
|
function maybeScrollWindow(cm, coords) { |
|
if (signalDOMEvent(cm, "scrollCursorIntoView")) return; |
|
|
|
var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; |
|
if (coords.top + box.top < 0) doScroll = true; |
|
else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; |
|
if (doScroll != null && !phantom) { |
|
var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + |
|
(coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + |
|
(coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " + |
|
coords.left + "px; width: 2px;"); |
|
cm.display.lineSpace.appendChild(scrollNode); |
|
scrollNode.scrollIntoView(doScroll); |
|
cm.display.lineSpace.removeChild(scrollNode); |
|
} |
|
} |
|
|
|
// Scroll a given position into view (immediately), verifying that |
|
// it actually became visible (as line heights are accurately |
|
// measured, the position of something may 'drift' during drawing). |
|
function scrollPosIntoView(cm, pos, end, margin) { |
|
if (margin == null) margin = 0; |
|
for (var limit = 0; limit < 5; limit++) { |
|
var changed = false, coords = cursorCoords(cm, pos); |
|
var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); |
|
var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), |
|
Math.min(coords.top, endCoords.top) - margin, |
|
Math.max(coords.left, endCoords.left), |
|
Math.max(coords.bottom, endCoords.bottom) + margin); |
|
var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; |
|
if (scrollPos.scrollTop != null) { |
|
setScrollTop(cm, scrollPos.scrollTop); |
|
if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; |
|
} |
|
if (scrollPos.scrollLeft != null) { |
|
setScrollLeft(cm, scrollPos.scrollLeft); |
|
if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; |
|
} |
|
if (!changed) break; |
|
} |
|
return coords; |
|
} |
|
|
|
// Scroll a given set of coordinates into view (immediately). |
|
function scrollIntoView(cm, x1, y1, x2, y2) { |
|
var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); |
|
if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); |
|
if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); |
|
} |
|
|
|
// Calculate a new scroll position needed to scroll the given |
|
// rectangle into view. Returns an object with scrollTop and |
|
// scrollLeft properties. When these are undefined, the |
|
// vertical/horizontal position does not need to be adjusted. |
|
function calculateScrollPos(cm, x1, y1, x2, y2) { |
|
var display = cm.display, snapMargin = textHeight(cm.display); |
|
if (y1 < 0) y1 = 0; |
|
var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; |
|
var screen = displayHeight(cm), result = {}; |
|
if (y2 - y1 > screen) y2 = y1 + screen; |
|
var docBottom = cm.doc.height + paddingVert(display); |
|
var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; |
|
if (y1 < screentop) { |
|
result.scrollTop = atTop ? 0 : y1; |
|
} else if (y2 > screentop + screen) { |
|
var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); |
|
if (newTop != screentop) result.scrollTop = newTop; |
|
} |
|
|
|
var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; |
|
var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); |
|
var tooWide = x2 - x1 > screenw; |
|
if (tooWide) x2 = x1 + screenw; |
|
if (x1 < 10) |
|
result.scrollLeft = 0; |
|
else if (x1 < screenleft) |
|
result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); |
|
else if (x2 > screenw + screenleft - 3) |
|
result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; |
|
return result; |
|
} |
|
|
|
// Store a relative adjustment to the scroll position in the current |
|
// operation (to be applied when the operation finishes). |
|
function addToScrollPos(cm, left, top) { |
|
if (left != null || top != null) resolveScrollToPos(cm); |
|
if (left != null) |
|
cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; |
|
if (top != null) |
|
cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; |
|
} |
|
|
|
// Make sure that at the end of the operation the current cursor is |
|
// shown. |
|
function ensureCursorVisible(cm) { |
|
resolveScrollToPos(cm); |
|
var cur = cm.getCursor(), from = cur, to = cur; |
|
if (!cm.options.lineWrapping) { |
|
from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; |
|
to = Pos(cur.line, cur.ch + 1); |
|
} |
|
cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; |
|
} |
|
|
|
// When an operation has its scrollToPos property set, and another |
|
// scroll action is applied before the end of the operation, this |
|
// 'simulates' scrolling that position into view in a cheap way, so |
|
// that the effect of intermediate scroll commands is not ignored. |
|
function resolveScrollToPos(cm) { |
|
var range = cm.curOp.scrollToPos; |
|
if (range) { |
|
cm.curOp.scrollToPos = null; |
|
var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); |
|
var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), |
|
Math.min(from.top, to.top) - range.margin, |
|
Math.max(from.right, to.right), |
|
Math.max(from.bottom, to.bottom) + range.margin); |
|
cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); |
|
} |
|
} |
|
|
|
// API UTILITIES |
|
|
|
// Indent the given line. The how parameter can be "smart", |
|
// "add"/null, "subtract", or "prev". When aggressive is false |
|
// (typically set to true for forced single-line indents), empty |
|
// lines are not indented, and places where the mode returns Pass |
|
// are left alone. |
|
function indentLine(cm, n, how, aggressive) { |
|
var doc = cm.doc, state; |
|
if (how == null) how = "add"; |
|
if (how == "smart") { |
|
// Fall back to "prev" when the mode doesn't have an indentation |
|
// method. |
|
if (!doc.mode.indent) how = "prev"; |
|
else state = getStateBefore(cm, n); |
|
} |
|
|
|
var tabSize = cm.options.tabSize; |
|
var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); |
|
if (line.stateAfter) line.stateAfter = null; |
|
var curSpaceString = line.text.match(/^\s*/)[0], indentation; |
|
if (!aggressive && !/\S/.test(line.text)) { |
|
indentation = 0; |
|
how = "not"; |
|
} else if (how == "smart") { |
|
indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); |
|
if (indentation == Pass || indentation > 150) { |
|
if (!aggressive) return; |
|
how = "prev"; |
|
} |
|
} |
|
if (how == "prev") { |
|
if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); |
|
else indentation = 0; |
|
} else if (how == "add") { |
|
indentation = curSpace + cm.options.indentUnit; |
|
} else if (how == "subtract") { |
|
indentation = curSpace - cm.options.indentUnit; |
|
} else if (typeof how == "number") { |
|
indentation = curSpace + how; |
|
} |
|
indentation = Math.max(0, indentation); |
|
|
|
var indentString = "", pos = 0; |
|
if (cm.options.indentWithTabs) |
|
for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} |
|
if (pos < indentation) indentString += spaceStr(indentation - pos); |
|
|
|
if (indentString != curSpaceString) { |
|
replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); |
|
line.stateAfter = null; |
|
return true; |
|
} else { |
|
// Ensure that, if the cursor was in the whitespace at the start |
|
// of the line, it is moved to the end of that space. |
|
for (var i = 0; i < doc.sel.ranges.length; i++) { |
|
var range = doc.sel.ranges[i]; |
|
if (range.head.line == n && range.head.ch < curSpaceString.length) { |
|
var pos = Pos(n, curSpaceString.length); |
|
replaceOneSelection(doc, i, new Range(pos, pos)); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Utility for applying a change to a line by handle or number, |
|
// returning the number and optionally registering the line as |
|
// changed. |
|
function changeLine(doc, handle, changeType, op) { |
|
var no = handle, line = handle; |
|
if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); |
|
else no = lineNo(handle); |
|
if (no == null) return null; |
|
if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); |
|
return line; |
|
} |
|
|
|
// Helper for deleting text near the selection(s), used to implement |
|
// backspace, delete, and similar functionality. |
|
function deleteNearSelection(cm, compute) { |
|
var ranges = cm.doc.sel.ranges, kill = []; |
|
// Build up a set of ranges to kill first, merging overlapping |
|
// ranges. |
|
for (var i = 0; i < ranges.length; i++) { |
|
var toKill = compute(ranges[i]); |
|
while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { |
|
var replaced = kill.pop(); |
|
if (cmp(replaced.from, toKill.from) < 0) { |
|
toKill.from = replaced.from; |
|
break; |
|
} |
|
} |
|
kill.push(toKill); |
|
} |
|
// Next, remove those actual ranges. |
|
runInOp(cm, function() { |
|
for (var i = kill.length - 1; i >= 0; i--) |
|
replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); |
|
ensureCursorVisible(cm); |
|
}); |
|
} |
|
|
|
// Used for horizontal relative motion. Dir is -1 or 1 (left or |
|
// right), unit can be "char", "column" (like char, but doesn't |
|
// cross line boundaries), "word" (across next word), or "group" (to |
|
// the start of next group of word or non-word-non-whitespace |
|
// chars). The visually param controls whether, in right-to-left |
|
// text, direction 1 means to move towards the next index in the |
|
// string, or towards the character to the right of the current |
|
// position. The resulting position will have a hitSide=true |
|
// property if it reached the end of the document. |
|
function findPosH(doc, pos, dir, unit, visually) { |
|
var line = pos.line, ch = pos.ch, origDir = dir; |
|
var lineObj = getLine(doc, line); |
|
var possible = true; |
|
function findNextLine() { |
|
var l = line + dir; |
|
if (l < doc.first || l >= doc.first + doc.size) return (possible = false); |
|
line = l; |
|
return lineObj = getLine(doc, l); |
|
} |
|
function moveOnce(boundToLine) { |
|
var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); |
|
if (next == null) { |
|
if (!boundToLine && findNextLine()) { |
|
if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); |
|
else ch = dir < 0 ? lineObj.text.length : 0; |
|
} else return (possible = false); |
|
} else ch = next; |
|
return true; |
|
} |
|
|
|
if (unit == "char") moveOnce(); |
|
else if (unit == "column") moveOnce(true); |
|
else if (unit == "word" || unit == "group") { |
|
var sawType = null, group = unit == "group"; |
|
var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); |
|
for (var first = true;; first = false) { |
|
if (dir < 0 && !moveOnce(!first)) break; |
|
var cur = lineObj.text.charAt(ch) || "\n"; |
|
var type = isWordChar(cur, helper) ? "w" |
|
: group && cur == "\n" ? "n" |
|
: !group || /\s/.test(cur) ? null |
|
: "p"; |
|
if (group && !first && !type) type = "s"; |
|
if (sawType && sawType != type) { |
|
if (dir < 0) {dir = 1; moveOnce();} |
|
break; |
|
} |
|
|
|
if (type) sawType = type; |
|
if (dir > 0 && !moveOnce(!first)) break; |
|
} |
|
} |
|
var result = skipAtomic(doc, Pos(line, ch), origDir, true); |
|
if (!possible) result.hitSide = true; |
|
return result; |
|
} |
|
|
|
// For relative vertical movement. Dir may be -1 or 1. Unit can be |
|
// "page" or "line". The resulting position will have a hitSide=true |
|
// property if it reached the end of the document. |
|
function findPosV(cm, pos, dir, unit) { |
|
var doc = cm.doc, x = pos.left, y; |
|
if (unit == "page") { |
|
var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); |
|
y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); |
|
} else if (unit == "line") { |
|
y = dir > 0 ? pos.bottom + 3 : pos.top - 3; |
|
} |
|
for (;;) { |
|
var target = coordsChar(cm, x, y); |
|
if (!target.outside) break; |
|
if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } |
|
y += dir * 5; |
|
} |
|
return target; |
|
} |
|
|
|
// EDITOR METHODS |
|
|
|
// The publicly visible API. Note that methodOp(f) means |
|
// 'wrap f in an operation, performed on its `this` parameter'. |
|
|
|
// This is not the complete set of editor methods. Most of the |
|
// methods defined on the Doc type are also injected into |
|
// CodeMirror.prototype, for backwards compatibility and |
|
// convenience. |
|
|
|
CodeMirror.prototype = { |
|
constructor: CodeMirror, |
|
focus: function(){window.focus(); this.display.input.focus();}, |
|
|
|
setOption: function(option, value) { |
|
var options = this.options, old = options[option]; |
|
if (options[option] == value && option != "mode") return; |
|
options[option] = value; |
|
if (optionHandlers.hasOwnProperty(option)) |
|
operation(this, optionHandlers[option])(this, value, old); |
|
}, |
|
|
|
getOption: function(option) {return this.options[option];}, |
|
getDoc: function() {return this.doc;}, |
|
|
|
addKeyMap: function(map, bottom) { |
|
this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); |
|
}, |
|
removeKeyMap: function(map) { |
|
var maps = this.state.keyMaps; |
|
for (var i = 0; i < maps.length; ++i) |
|
if (maps[i] == map || maps[i].name == map) { |
|
maps.splice(i, 1); |
|
return true; |
|
} |
|
}, |
|
|
|
addOverlay: methodOp(function(spec, options) { |
|
var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); |
|
if (mode.startState) throw new Error("Overlays may not be stateful."); |
|
this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); |
|
this.state.modeGen++; |
|
regChange(this); |
|
}), |
|
removeOverlay: methodOp(function(spec) { |
|
var overlays = this.state.overlays; |
|
for (var i = 0; i < overlays.length; ++i) { |
|
var cur = overlays[i].modeSpec; |
|
if (cur == spec || typeof spec == "string" && cur.name == spec) { |
|
overlays.splice(i, 1); |
|
this.state.modeGen++; |
|
regChange(this); |
|
return; |
|
} |
|
} |
|
}), |
|
|
|
indentLine: methodOp(function(n, dir, aggressive) { |
|
if (typeof dir != "string" && typeof dir != "number") { |
|
if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; |
|
else dir = dir ? "add" : "subtract"; |
|
} |
|
if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); |
|
}), |
|
indentSelection: methodOp(function(how) { |
|
var ranges = this.doc.sel.ranges, end = -1; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var range = ranges[i]; |
|
if (!range.empty()) { |
|
var from = range.from(), to = range.to(); |
|
var start = Math.max(end, from.line); |
|
end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; |
|
for (var j = start; j < end; ++j) |
|
indentLine(this, j, how); |
|
var newRanges = this.doc.sel.ranges; |
|
if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) |
|
replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); |
|
} else if (range.head.line > end) { |
|
indentLine(this, range.head.line, how, true); |
|
end = range.head.line; |
|
if (i == this.doc.sel.primIndex) ensureCursorVisible(this); |
|
} |
|
} |
|
}), |
|
|
|
// Fetch the parser token for a given character. Useful for hacks |
|
// that want to inspect the mode state (say, for completion). |
|
getTokenAt: function(pos, precise) { |
|
return takeToken(this, pos, precise); |
|
}, |
|
|
|
getLineTokens: function(line, precise) { |
|
return takeToken(this, Pos(line), precise, true); |
|
}, |
|
|
|
getTokenTypeAt: function(pos) { |
|
pos = clipPos(this.doc, pos); |
|
var styles = getLineStyles(this, getLine(this.doc, pos.line)); |
|
var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; |
|
var type; |
|
if (ch == 0) type = styles[2]; |
|
else for (;;) { |
|
var mid = (before + after) >> 1; |
|
if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; |
|
else if (styles[mid * 2 + 1] < ch) before = mid + 1; |
|
else { type = styles[mid * 2 + 2]; break; } |
|
} |
|
var cut = type ? type.indexOf("cm-overlay ") : -1; |
|
return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); |
|
}, |
|
|
|
getModeAt: function(pos) { |
|
var mode = this.doc.mode; |
|
if (!mode.innerMode) return mode; |
|
return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; |
|
}, |
|
|
|
getHelper: function(pos, type) { |
|
return this.getHelpers(pos, type)[0]; |
|
}, |
|
|
|
getHelpers: function(pos, type) { |
|
var found = []; |
|
if (!helpers.hasOwnProperty(type)) return found; |
|
var help = helpers[type], mode = this.getModeAt(pos); |
|
if (typeof mode[type] == "string") { |
|
if (help[mode[type]]) found.push(help[mode[type]]); |
|
} else if (mode[type]) { |
|
for (var i = 0; i < mode[type].length; i++) { |
|
var val = help[mode[type][i]]; |
|
if (val) found.push(val); |
|
} |
|
} else if (mode.helperType && help[mode.helperType]) { |
|
found.push(help[mode.helperType]); |
|
} else if (help[mode.name]) { |
|
found.push(help[mode.name]); |
|
} |
|
for (var i = 0; i < help._global.length; i++) { |
|
var cur = help._global[i]; |
|
if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) |
|
found.push(cur.val); |
|
} |
|
return found; |
|
}, |
|
|
|
getStateAfter: function(line, precise) { |
|
var doc = this.doc; |
|
line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); |
|
return getStateBefore(this, line + 1, precise); |
|
}, |
|
|
|
cursorCoords: function(start, mode) { |
|
var pos, range = this.doc.sel.primary(); |
|
if (start == null) pos = range.head; |
|
else if (typeof start == "object") pos = clipPos(this.doc, start); |
|
else pos = start ? range.from() : range.to(); |
|
return cursorCoords(this, pos, mode || "page"); |
|
}, |
|
|
|
charCoords: function(pos, mode) { |
|
return charCoords(this, clipPos(this.doc, pos), mode || "page"); |
|
}, |
|
|
|
coordsChar: function(coords, mode) { |
|
coords = fromCoordSystem(this, coords, mode || "page"); |
|
return coordsChar(this, coords.left, coords.top); |
|
}, |
|
|
|
lineAtHeight: function(height, mode) { |
|
height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; |
|
return lineAtHeight(this.doc, height + this.display.viewOffset); |
|
}, |
|
heightAtLine: function(line, mode) { |
|
var end = false, lineObj; |
|
if (typeof line == "number") { |
|
var last = this.doc.first + this.doc.size - 1; |
|
if (line < this.doc.first) line = this.doc.first; |
|
else if (line > last) { line = last; end = true; } |
|
lineObj = getLine(this.doc, line); |
|
} else { |
|
lineObj = line; |
|
} |
|
return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + |
|
(end ? this.doc.height - heightAtLine(lineObj) : 0); |
|
}, |
|
|
|
defaultTextHeight: function() { return textHeight(this.display); }, |
|
defaultCharWidth: function() { return charWidth(this.display); }, |
|
|
|
setGutterMarker: methodOp(function(line, gutterID, value) { |
|
return changeLine(this.doc, line, "gutter", function(line) { |
|
var markers = line.gutterMarkers || (line.gutterMarkers = {}); |
|
markers[gutterID] = value; |
|
if (!value && isEmpty(markers)) line.gutterMarkers = null; |
|
return true; |
|
}); |
|
}), |
|
|
|
clearGutter: methodOp(function(gutterID) { |
|
var cm = this, doc = cm.doc, i = doc.first; |
|
doc.iter(function(line) { |
|
if (line.gutterMarkers && line.gutterMarkers[gutterID]) { |
|
line.gutterMarkers[gutterID] = null; |
|
regLineChange(cm, i, "gutter"); |
|
if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; |
|
} |
|
++i; |
|
}); |
|
}), |
|
|
|
lineInfo: function(line) { |
|
if (typeof line == "number") { |
|
if (!isLine(this.doc, line)) return null; |
|
var n = line; |
|
line = getLine(this.doc, line); |
|
if (!line) return null; |
|
} else { |
|
var n = lineNo(line); |
|
if (n == null) return null; |
|
} |
|
return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, |
|
textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, |
|
widgets: line.widgets}; |
|
}, |
|
|
|
getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, |
|
|
|
addWidget: function(pos, node, scroll, vert, horiz) { |
|
var display = this.display; |
|
pos = cursorCoords(this, clipPos(this.doc, pos)); |
|
var top = pos.bottom, left = pos.left; |
|
node.style.position = "absolute"; |
|
node.setAttribute("cm-ignore-events", "true"); |
|
this.display.input.setUneditable(node); |
|
display.sizer.appendChild(node); |
|
if (vert == "over") { |
|
top = pos.top; |
|
} else if (vert == "above" || vert == "near") { |
|
var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), |
|
hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); |
|
// Default to positioning above (if specified and possible); otherwise default to positioning below |
|
if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) |
|
top = pos.top - node.offsetHeight; |
|
else if (pos.bottom + node.offsetHeight <= vspace) |
|
top = pos.bottom; |
|
if (left + node.offsetWidth > hspace) |
|
left = hspace - node.offsetWidth; |
|
} |
|
node.style.top = top + "px"; |
|
node.style.left = node.style.right = ""; |
|
if (horiz == "right") { |
|
left = display.sizer.clientWidth - node.offsetWidth; |
|
node.style.right = "0px"; |
|
} else { |
|
if (horiz == "left") left = 0; |
|
else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; |
|
node.style.left = left + "px"; |
|
} |
|
if (scroll) |
|
scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); |
|
}, |
|
|
|
triggerOnKeyDown: methodOp(onKeyDown), |
|
triggerOnKeyPress: methodOp(onKeyPress), |
|
triggerOnKeyUp: onKeyUp, |
|
|
|
execCommand: function(cmd) { |
|
if (commands.hasOwnProperty(cmd)) |
|
return commands[cmd](this); |
|
}, |
|
|
|
triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), |
|
|
|
findPosH: function(from, amount, unit, visually) { |
|
var dir = 1; |
|
if (amount < 0) { dir = -1; amount = -amount; } |
|
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { |
|
cur = findPosH(this.doc, cur, dir, unit, visually); |
|
if (cur.hitSide) break; |
|
} |
|
return cur; |
|
}, |
|
|
|
moveH: methodOp(function(dir, unit) { |
|
var cm = this; |
|
cm.extendSelectionsBy(function(range) { |
|
if (cm.display.shift || cm.doc.extend || range.empty()) |
|
return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); |
|
else |
|
return dir < 0 ? range.from() : range.to(); |
|
}, sel_move); |
|
}), |
|
|
|
deleteH: methodOp(function(dir, unit) { |
|
var sel = this.doc.sel, doc = this.doc; |
|
if (sel.somethingSelected()) |
|
doc.replaceSelection("", null, "+delete"); |
|
else |
|
deleteNearSelection(this, function(range) { |
|
var other = findPosH(doc, range.head, dir, unit, false); |
|
return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; |
|
}); |
|
}), |
|
|
|
findPosV: function(from, amount, unit, goalColumn) { |
|
var dir = 1, x = goalColumn; |
|
if (amount < 0) { dir = -1; amount = -amount; } |
|
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { |
|
var coords = cursorCoords(this, cur, "div"); |
|
if (x == null) x = coords.left; |
|
else coords.left = x; |
|
cur = findPosV(this, coords, dir, unit); |
|
if (cur.hitSide) break; |
|
} |
|
return cur; |
|
}, |
|
|
|
moveV: methodOp(function(dir, unit) { |
|
var cm = this, doc = this.doc, goals = []; |
|
var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); |
|
doc.extendSelectionsBy(function(range) { |
|
if (collapse) |
|
return dir < 0 ? range.from() : range.to(); |
|
var headPos = cursorCoords(cm, range.head, "div"); |
|
if (range.goalColumn != null) headPos.left = range.goalColumn; |
|
goals.push(headPos.left); |
|
var pos = findPosV(cm, headPos, dir, unit); |
|
if (unit == "page" && range == doc.sel.primary()) |
|
addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); |
|
return pos; |
|
}, sel_move); |
|
if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) |
|
doc.sel.ranges[i].goalColumn = goals[i]; |
|
}), |
|
|
|
// Find the word at the given position (as returned by coordsChar). |
|
findWordAt: function(pos) { |
|
var doc = this.doc, line = getLine(doc, pos.line).text; |
|
var start = pos.ch, end = pos.ch; |
|
if (line) { |
|
var helper = this.getHelper(pos, "wordChars"); |
|
if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; |
|
var startChar = line.charAt(start); |
|
var check = isWordChar(startChar, helper) |
|
? function(ch) { return isWordChar(ch, helper); } |
|
: /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} |
|
: function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; |
|
while (start > 0 && check(line.charAt(start - 1))) --start; |
|
while (end < line.length && check(line.charAt(end))) ++end; |
|
} |
|
return new Range(Pos(pos.line, start), Pos(pos.line, end)); |
|
}, |
|
|
|
toggleOverwrite: function(value) { |
|
if (value != null && value == this.state.overwrite) return; |
|
if (this.state.overwrite = !this.state.overwrite) |
|
addClass(this.display.cursorDiv, "CodeMirror-overwrite"); |
|
else |
|
rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); |
|
|
|
signal(this, "overwriteToggle", this, this.state.overwrite); |
|
}, |
|
hasFocus: function() { return this.display.input.getField() == activeElt(); }, |
|
|
|
scrollTo: methodOp(function(x, y) { |
|
if (x != null || y != null) resolveScrollToPos(this); |
|
if (x != null) this.curOp.scrollLeft = x; |
|
if (y != null) this.curOp.scrollTop = y; |
|
}), |
|
getScrollInfo: function() { |
|
var scroller = this.display.scroller; |
|
return {left: scroller.scrollLeft, top: scroller.scrollTop, |
|
height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, |
|
width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, |
|
clientHeight: displayHeight(this), clientWidth: displayWidth(this)}; |
|
}, |
|
|
|
scrollIntoView: methodOp(function(range, margin) { |
|
if (range == null) { |
|
range = {from: this.doc.sel.primary().head, to: null}; |
|
if (margin == null) margin = this.options.cursorScrollMargin; |
|
} else if (typeof range == "number") { |
|
range = {from: Pos(range, 0), to: null}; |
|
} else if (range.from == null) { |
|
range = {from: range, to: null}; |
|
} |
|
if (!range.to) range.to = range.from; |
|
range.margin = margin || 0; |
|
|
|
if (range.from.line != null) { |
|
resolveScrollToPos(this); |
|
this.curOp.scrollToPos = range; |
|
} else { |
|
var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), |
|
Math.min(range.from.top, range.to.top) - range.margin, |
|
Math.max(range.from.right, range.to.right), |
|
Math.max(range.from.bottom, range.to.bottom) + range.margin); |
|
this.scrollTo(sPos.scrollLeft, sPos.scrollTop); |
|
} |
|
}), |
|
|
|
setSize: methodOp(function(width, height) { |
|
var cm = this; |
|
function interpret(val) { |
|
return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; |
|
} |
|
if (width != null) cm.display.wrapper.style.width = interpret(width); |
|
if (height != null) cm.display.wrapper.style.height = interpret(height); |
|
if (cm.options.lineWrapping) clearLineMeasurementCache(this); |
|
var lineNo = cm.display.viewFrom; |
|
cm.doc.iter(lineNo, cm.display.viewTo, function(line) { |
|
if (line.widgets) for (var i = 0; i < line.widgets.length; i++) |
|
if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } |
|
++lineNo; |
|
}); |
|
cm.curOp.forceUpdate = true; |
|
signal(cm, "refresh", this); |
|
}), |
|
|
|
operation: function(f){return runInOp(this, f);}, |
|
|
|
refresh: methodOp(function() { |
|
var oldHeight = this.display.cachedTextHeight; |
|
regChange(this); |
|
this.curOp.forceUpdate = true; |
|
clearCaches(this); |
|
this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); |
|
updateGutterSpace(this); |
|
if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) |
|
estimateLineHeights(this); |
|
signal(this, "refresh", this); |
|
}), |
|
|
|
swapDoc: methodOp(function(doc) { |
|
var old = this.doc; |
|
old.cm = null; |
|
attachDoc(this, doc); |
|
clearCaches(this); |
|
this.display.input.reset(); |
|
this.scrollTo(doc.scrollLeft, doc.scrollTop); |
|
this.curOp.forceScroll = true; |
|
signalLater(this, "swapDoc", this, old); |
|
return old; |
|
}), |
|
|
|
getInputField: function(){return this.display.input.getField();}, |
|
getWrapperElement: function(){return this.display.wrapper;}, |
|
getScrollerElement: function(){return this.display.scroller;}, |
|
getGutterElement: function(){return this.display.gutters;} |
|
}; |
|
eventMixin(CodeMirror); |
|
|
|
// OPTION DEFAULTS |
|
|
|
// The default configuration options. |
|
var defaults = CodeMirror.defaults = {}; |
|
// Functions to run when options are changed. |
|
var optionHandlers = CodeMirror.optionHandlers = {}; |
|
|
|
function option(name, deflt, handle, notOnInit) { |
|
CodeMirror.defaults[name] = deflt; |
|
if (handle) optionHandlers[name] = |
|
notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; |
|
} |
|
|
|
// Passed to option handlers when there is no old value. |
|
var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; |
|
|
|
// These two are, on init, called from the constructor because they |
|
// have to be initialized before the editor can start at all. |
|
option("value", "", function(cm, val) { |
|
cm.setValue(val); |
|
}, true); |
|
option("mode", null, function(cm, val) { |
|
cm.doc.modeOption = val; |
|
loadMode(cm); |
|
}, true); |
|
|
|
option("indentUnit", 2, loadMode, true); |
|
option("indentWithTabs", false); |
|
option("smartIndent", true); |
|
option("tabSize", 4, function(cm) { |
|
resetModeState(cm); |
|
clearCaches(cm); |
|
regChange(cm); |
|
}, true); |
|
option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { |
|
cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); |
|
if (old != CodeMirror.Init) cm.refresh(); |
|
}); |
|
option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); |
|
option("electricChars", true); |
|
option("inputStyle", mobile ? "contenteditable" : "textarea", function() { |
|
throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME |
|
}, true); |
|
option("rtlMoveVisually", !windows); |
|
option("wholeLineUpdateBefore", true); |
|
|
|
option("theme", "default", function(cm) { |
|
themeChanged(cm); |
|
guttersChanged(cm); |
|
}, true); |
|
option("keyMap", "default", function(cm, val, old) { |
|
var next = getKeyMap(val); |
|
var prev = old != CodeMirror.Init && getKeyMap(old); |
|
if (prev && prev.detach) prev.detach(cm, next); |
|
if (next.attach) next.attach(cm, prev || null); |
|
}); |
|
option("extraKeys", null); |
|
|
|
option("lineWrapping", false, wrappingChanged, true); |
|
option("gutters", [], function(cm) { |
|
setGuttersForLineNumbers(cm.options); |
|
guttersChanged(cm); |
|
}, true); |
|
option("fixedGutter", true, function(cm, val) { |
|
cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; |
|
cm.refresh(); |
|
}, true); |
|
option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); |
|
option("scrollbarStyle", "native", function(cm) { |
|
initScrollbars(cm); |
|
updateScrollbars(cm); |
|
cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); |
|
cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); |
|
}, true); |
|
option("lineNumbers", false, function(cm) { |
|
setGuttersForLineNumbers(cm.options); |
|
guttersChanged(cm); |
|
}, true); |
|
option("firstLineNumber", 1, guttersChanged, true); |
|
option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); |
|
option("showCursorWhenSelecting", false, updateSelection, true); |
|
|
|
option("resetSelectionOnContextMenu", true); |
|
option("lineWiseCopyCut", true); |
|
|
|
option("readOnly", false, function(cm, val) { |
|
if (val == "nocursor") { |
|
onBlur(cm); |
|
cm.display.input.blur(); |
|
cm.display.disabled = true; |
|
} else { |
|
cm.display.disabled = false; |
|
if (!val) cm.display.input.reset(); |
|
} |
|
}); |
|
option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); |
|
option("dragDrop", true, dragDropChanged); |
|
|
|
option("cursorBlinkRate", 530); |
|
option("cursorScrollMargin", 0); |
|
option("cursorHeight", 1, updateSelection, true); |
|
option("singleCursorHeightPerLine", true, updateSelection, true); |
|
option("workTime", 100); |
|
option("workDelay", 100); |
|
option("flattenSpans", true, resetModeState, true); |
|
option("addModeClass", false, resetModeState, true); |
|
option("pollInterval", 100); |
|
option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); |
|
option("historyEventDelay", 1250); |
|
option("viewportMargin", 10, function(cm){cm.refresh();}, true); |
|
option("maxHighlightLength", 10000, resetModeState, true); |
|
option("moveInputWithCursor", true, function(cm, val) { |
|
if (!val) cm.display.input.resetPosition(); |
|
}); |
|
|
|
option("tabindex", null, function(cm, val) { |
|
cm.display.input.getField().tabIndex = val || ""; |
|
}); |
|
option("autofocus", null); |
|
|
|
// MODE DEFINITION AND QUERYING |
|
|
|
// Known modes, by name and by MIME |
|
var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; |
|
|
|
// Extra arguments are stored as the mode's dependencies, which is |
|
// used by (legacy) mechanisms like loadmode.js to automatically |
|
// load a mode. (Preferred mechanism is the require/define calls.) |
|
CodeMirror.defineMode = function(name, mode) { |
|
if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; |
|
if (arguments.length > 2) |
|
mode.dependencies = Array.prototype.slice.call(arguments, 2); |
|
modes[name] = mode; |
|
}; |
|
|
|
CodeMirror.defineMIME = function(mime, spec) { |
|
mimeModes[mime] = spec; |
|
}; |
|
|
|
// Given a MIME type, a {name, ...options} config object, or a name |
|
// string, return a mode config object. |
|
CodeMirror.resolveMode = function(spec) { |
|
if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { |
|
spec = mimeModes[spec]; |
|
} else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { |
|
var found = mimeModes[spec.name]; |
|
if (typeof found == "string") found = {name: found}; |
|
spec = createObj(found, spec); |
|
spec.name = found.name; |
|
} else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { |
|
return CodeMirror.resolveMode("application/xml"); |
|
} |
|
if (typeof spec == "string") return {name: spec}; |
|
else return spec || {name: "null"}; |
|
}; |
|
|
|
// Given a mode spec (anything that resolveMode accepts), find and |
|
// initialize an actual mode object. |
|
CodeMirror.getMode = function(options, spec) { |
|
var spec = CodeMirror.resolveMode(spec); |
|
var mfactory = modes[spec.name]; |
|
if (!mfactory) return CodeMirror.getMode(options, "text/plain"); |
|
var modeObj = mfactory(options, spec); |
|
if (modeExtensions.hasOwnProperty(spec.name)) { |
|
var exts = modeExtensions[spec.name]; |
|
for (var prop in exts) { |
|
if (!exts.hasOwnProperty(prop)) continue; |
|
if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; |
|
modeObj[prop] = exts[prop]; |
|
} |
|
} |
|
modeObj.name = spec.name; |
|
if (spec.helperType) modeObj.helperType = spec.helperType; |
|
if (spec.modeProps) for (var prop in spec.modeProps) |
|
modeObj[prop] = spec.modeProps[prop]; |
|
|
|
return modeObj; |
|
}; |
|
|
|
// Minimal default mode. |
|
CodeMirror.defineMode("null", function() { |
|
return {token: function(stream) {stream.skipToEnd();}}; |
|
}); |
|
CodeMirror.defineMIME("text/plain", "null"); |
|
|
|
// This can be used to attach properties to mode objects from |
|
// outside the actual mode definition. |
|
var modeExtensions = CodeMirror.modeExtensions = {}; |
|
CodeMirror.extendMode = function(mode, properties) { |
|
var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); |
|
copyObj(properties, exts); |
|
}; |
|
|
|
// EXTENSIONS |
|
|
|
CodeMirror.defineExtension = function(name, func) { |
|
CodeMirror.prototype[name] = func; |
|
}; |
|
CodeMirror.defineDocExtension = function(name, func) { |
|
Doc.prototype[name] = func; |
|
}; |
|
CodeMirror.defineOption = option; |
|
|
|
var initHooks = []; |
|
CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; |
|
|
|
var helpers = CodeMirror.helpers = {}; |
|
CodeMirror.registerHelper = function(type, name, value) { |
|
if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; |
|
helpers[type][name] = value; |
|
}; |
|
CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { |
|
CodeMirror.registerHelper(type, name, value); |
|
helpers[type]._global.push({pred: predicate, val: value}); |
|
}; |
|
|
|
// MODE STATE HANDLING |
|
|
|
// Utility functions for working with state. Exported because nested |
|
// modes need to do this for their inner modes. |
|
|
|
var copyState = CodeMirror.copyState = function(mode, state) { |
|
if (state === true) return state; |
|
if (mode.copyState) return mode.copyState(state); |
|
var nstate = {}; |
|
for (var n in state) { |
|
var val = state[n]; |
|
if (val instanceof Array) val = val.concat([]); |
|
nstate[n] = val; |
|
} |
|
return nstate; |
|
}; |
|
|
|
var startState = CodeMirror.startState = function(mode, a1, a2) { |
|
return mode.startState ? mode.startState(a1, a2) : true; |
|
}; |
|
|
|
// Given a mode and a state (for that mode), find the inner mode and |
|
// state at the position that the state refers to. |
|
CodeMirror.innerMode = function(mode, state) { |
|
while (mode.innerMode) { |
|
var info = mode.innerMode(state); |
|
if (!info || info.mode == mode) break; |
|
state = info.state; |
|
mode = info.mode; |
|
} |
|
return info || {mode: mode, state: state}; |
|
}; |
|
|
|
// STANDARD COMMANDS |
|
|
|
// Commands are parameter-less actions that can be performed on an |
|
// editor, mostly used for keybindings. |
|
var commands = CodeMirror.commands = { |
|
selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, |
|
singleSelection: function(cm) { |
|
cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); |
|
}, |
|
killLine: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
if (range.empty()) { |
|
var len = getLine(cm.doc, range.head.line).text.length; |
|
if (range.head.ch == len && range.head.line < cm.lastLine()) |
|
return {from: range.head, to: Pos(range.head.line + 1, 0)}; |
|
else |
|
return {from: range.head, to: Pos(range.head.line, len)}; |
|
} else { |
|
return {from: range.from(), to: range.to()}; |
|
} |
|
}); |
|
}, |
|
deleteLine: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
return {from: Pos(range.from().line, 0), |
|
to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; |
|
}); |
|
}, |
|
delLineLeft: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
return {from: Pos(range.from().line, 0), to: range.from()}; |
|
}); |
|
}, |
|
delWrappedLineLeft: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
var leftPos = cm.coordsChar({left: 0, top: top}, "div"); |
|
return {from: leftPos, to: range.from()}; |
|
}); |
|
}, |
|
delWrappedLineRight: function(cm) { |
|
deleteNearSelection(cm, function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); |
|
return {from: range.from(), to: rightPos }; |
|
}); |
|
}, |
|
undo: function(cm) {cm.undo();}, |
|
redo: function(cm) {cm.redo();}, |
|
undoSelection: function(cm) {cm.undoSelection();}, |
|
redoSelection: function(cm) {cm.redoSelection();}, |
|
goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, |
|
goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, |
|
goLineStart: function(cm) { |
|
cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, |
|
{origin: "+move", bias: 1}); |
|
}, |
|
goLineStartSmart: function(cm) { |
|
cm.extendSelectionsBy(function(range) { |
|
return lineStartSmart(cm, range.head); |
|
}, {origin: "+move", bias: 1}); |
|
}, |
|
goLineEnd: function(cm) { |
|
cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, |
|
{origin: "+move", bias: -1}); |
|
}, |
|
goLineRight: function(cm) { |
|
cm.extendSelectionsBy(function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); |
|
}, sel_move); |
|
}, |
|
goLineLeft: function(cm) { |
|
cm.extendSelectionsBy(function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
return cm.coordsChar({left: 0, top: top}, "div"); |
|
}, sel_move); |
|
}, |
|
goLineLeftSmart: function(cm) { |
|
cm.extendSelectionsBy(function(range) { |
|
var top = cm.charCoords(range.head, "div").top + 5; |
|
var pos = cm.coordsChar({left: 0, top: top}, "div"); |
|
if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); |
|
return pos; |
|
}, sel_move); |
|
}, |
|
goLineUp: function(cm) {cm.moveV(-1, "line");}, |
|
goLineDown: function(cm) {cm.moveV(1, "line");}, |
|
goPageUp: function(cm) {cm.moveV(-1, "page");}, |
|
goPageDown: function(cm) {cm.moveV(1, "page");}, |
|
goCharLeft: function(cm) {cm.moveH(-1, "char");}, |
|
goCharRight: function(cm) {cm.moveH(1, "char");}, |
|
goColumnLeft: function(cm) {cm.moveH(-1, "column");}, |
|
goColumnRight: function(cm) {cm.moveH(1, "column");}, |
|
goWordLeft: function(cm) {cm.moveH(-1, "word");}, |
|
goGroupRight: function(cm) {cm.moveH(1, "group");}, |
|
goGroupLeft: function(cm) {cm.moveH(-1, "group");}, |
|
goWordRight: function(cm) {cm.moveH(1, "word");}, |
|
delCharBefore: function(cm) {cm.deleteH(-1, "char");}, |
|
delCharAfter: function(cm) {cm.deleteH(1, "char");}, |
|
delWordBefore: function(cm) {cm.deleteH(-1, "word");}, |
|
delWordAfter: function(cm) {cm.deleteH(1, "word");}, |
|
delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, |
|
delGroupAfter: function(cm) {cm.deleteH(1, "group");}, |
|
indentAuto: function(cm) {cm.indentSelection("smart");}, |
|
indentMore: function(cm) {cm.indentSelection("add");}, |
|
indentLess: function(cm) {cm.indentSelection("subtract");}, |
|
insertTab: function(cm) {cm.replaceSelection("\t");}, |
|
insertSoftTab: function(cm) { |
|
var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var pos = ranges[i].from(); |
|
var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); |
|
spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); |
|
} |
|
cm.replaceSelections(spaces); |
|
}, |
|
defaultTab: function(cm) { |
|
if (cm.somethingSelected()) cm.indentSelection("add"); |
|
else cm.execCommand("insertTab"); |
|
}, |
|
transposeChars: function(cm) { |
|
runInOp(cm, function() { |
|
var ranges = cm.listSelections(), newSel = []; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; |
|
if (line) { |
|
if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); |
|
if (cur.ch > 0) { |
|
cur = new Pos(cur.line, cur.ch + 1); |
|
cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), |
|
Pos(cur.line, cur.ch - 2), cur, "+transpose"); |
|
} else if (cur.line > cm.doc.first) { |
|
var prev = getLine(cm.doc, cur.line - 1).text; |
|
if (prev) |
|
cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), |
|
Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); |
|
} |
|
} |
|
newSel.push(new Range(cur, cur)); |
|
} |
|
cm.setSelections(newSel); |
|
}); |
|
}, |
|
newlineAndIndent: function(cm) { |
|
runInOp(cm, function() { |
|
var len = cm.listSelections().length; |
|
for (var i = 0; i < len; i++) { |
|
var range = cm.listSelections()[i]; |
|
cm.replaceRange("\n", range.anchor, range.head, "+input"); |
|
cm.indentLine(range.from().line + 1, null, true); |
|
ensureCursorVisible(cm); |
|
} |
|
}); |
|
}, |
|
toggleOverwrite: function(cm) {cm.toggleOverwrite();} |
|
}; |
|
|
|
|
|
// STANDARD KEYMAPS |
|
|
|
var keyMap = CodeMirror.keyMap = {}; |
|
|
|
keyMap.basic = { |
|
"Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", |
|
"End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", |
|
"Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", |
|
"Tab": "defaultTab", "Shift-Tab": "indentAuto", |
|
"Enter": "newlineAndIndent", "Insert": "toggleOverwrite", |
|
"Esc": "singleSelection" |
|
}; |
|
// Note that the save and find-related commands aren't defined by |
|
// default. User code or addons can define them. Unknown commands |
|
// are simply ignored. |
|
keyMap.pcDefault = { |
|
"Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", |
|
"Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", |
|
"Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", |
|
"Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", |
|
"Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", |
|
"Ctrl-[": "indentLess", "Ctrl-]": "indentMore", |
|
"Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", |
|
fallthrough: "basic" |
|
}; |
|
// Very basic readline/emacs-style bindings, which are standard on Mac. |
|
keyMap.emacsy = { |
|
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", |
|
"Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", |
|
"Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", |
|
"Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" |
|
}; |
|
keyMap.macDefault = { |
|
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", |
|
"Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", |
|
"Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", |
|
"Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", |
|
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", |
|
"Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", |
|
"Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", |
|
fallthrough: ["basic", "emacsy"] |
|
}; |
|
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; |
|
|
|
// KEYMAP DISPATCH |
|
|
|
function normalizeKeyName(name) { |
|
var parts = name.split(/-(?!$)/), name = parts[parts.length - 1]; |
|
var alt, ctrl, shift, cmd; |
|
for (var i = 0; i < parts.length - 1; i++) { |
|
var mod = parts[i]; |
|
if (/^(cmd|meta|m)$/i.test(mod)) cmd = true; |
|
else if (/^a(lt)?$/i.test(mod)) alt = true; |
|
else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; |
|
else if (/^s(hift)$/i.test(mod)) shift = true; |
|
else throw new Error("Unrecognized modifier name: " + mod); |
|
} |
|
if (alt) name = "Alt-" + name; |
|
if (ctrl) name = "Ctrl-" + name; |
|
if (cmd) name = "Cmd-" + name; |
|
if (shift) name = "Shift-" + name; |
|
return name; |
|
} |
|
|
|
// This is a kludge to keep keymaps mostly working as raw objects |
|
// (backwards compatibility) while at the same time support features |
|
// like normalization and multi-stroke key bindings. It compiles a |
|
// new normalized keymap, and then updates the old object to reflect |
|
// this. |
|
CodeMirror.normalizeKeyMap = function(keymap) { |
|
var copy = {}; |
|
for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) { |
|
var value = keymap[keyname]; |
|
if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue; |
|
if (value == "...") { delete keymap[keyname]; continue; } |
|
|
|
var keys = map(keyname.split(" "), normalizeKeyName); |
|
for (var i = 0; i < keys.length; i++) { |
|
var val, name; |
|
if (i == keys.length - 1) { |
|
name = keys.join(" "); |
|
val = value; |
|
} else { |
|
name = keys.slice(0, i + 1).join(" "); |
|
val = "..."; |
|
} |
|
var prev = copy[name]; |
|
if (!prev) copy[name] = val; |
|
else if (prev != val) throw new Error("Inconsistent bindings for " + name); |
|
} |
|
delete keymap[keyname]; |
|
} |
|
for (var prop in copy) keymap[prop] = copy[prop]; |
|
return keymap; |
|
}; |
|
|
|
var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) { |
|
map = getKeyMap(map); |
|
var found = map.call ? map.call(key, context) : map[key]; |
|
if (found === false) return "nothing"; |
|
if (found === "...") return "multi"; |
|
if (found != null && handle(found)) return "handled"; |
|
|
|
if (map.fallthrough) { |
|
if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") |
|
return lookupKey(key, map.fallthrough, handle, context); |
|
for (var i = 0; i < map.fallthrough.length; i++) { |
|
var result = lookupKey(key, map.fallthrough[i], handle, context); |
|
if (result) return result; |
|
} |
|
} |
|
}; |
|
|
|
// Modifier key presses don't count as 'real' key presses for the |
|
// purpose of keymap fallthrough. |
|
var isModifierKey = CodeMirror.isModifierKey = function(value) { |
|
var name = typeof value == "string" ? value : keyNames[value.keyCode]; |
|
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; |
|
}; |
|
|
|
// Look up the name of a key as indicated by an event object. |
|
var keyName = CodeMirror.keyName = function(event, noShift) { |
|
if (presto && event.keyCode == 34 && event["char"]) return false; |
|
var base = keyNames[event.keyCode], name = base; |
|
if (name == null || event.altGraphKey) return false; |
|
if (event.altKey && base != "Alt") name = "Alt-" + name; |
|
if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name; |
|
if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name; |
|
if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name; |
|
return name; |
|
}; |
|
|
|
function getKeyMap(val) { |
|
return typeof val == "string" ? keyMap[val] : val; |
|
} |
|
|
|
// FROMTEXTAREA |
|
|
|
CodeMirror.fromTextArea = function(textarea, options) { |
|
options = options ? copyObj(options) : {}; |
|
options.value = textarea.value; |
|
if (!options.tabindex && textarea.tabIndex) |
|
options.tabindex = textarea.tabIndex; |
|
if (!options.placeholder && textarea.placeholder) |
|
options.placeholder = textarea.placeholder; |
|
// Set autofocus to true if this textarea is focused, or if it has |
|
// autofocus and no other element is focused. |
|
if (options.autofocus == null) { |
|
var hasFocus = activeElt(); |
|
options.autofocus = hasFocus == textarea || |
|
textarea.getAttribute("autofocus") != null && hasFocus == document.body; |
|
} |
|
|
|
function save() {textarea.value = cm.getValue();} |
|
if (textarea.form) { |
|
on(textarea.form, "submit", save); |
|
// Deplorable hack to make the submit method do the right thing. |
|
if (!options.leaveSubmitMethodAlone) { |
|
var form = textarea.form, realSubmit = form.submit; |
|
try { |
|
var wrappedSubmit = form.submit = function() { |
|
save(); |
|
form.submit = realSubmit; |
|
form.submit(); |
|
form.submit = wrappedSubmit; |
|
}; |
|
} catch(e) {} |
|
} |
|
} |
|
|
|
options.finishInit = function(cm) { |
|
cm.save = save; |
|
cm.getTextArea = function() { return textarea; }; |
|
cm.toTextArea = function() { |
|
cm.toTextArea = isNaN; // Prevent this from being ran twice |
|
save(); |
|
textarea.parentNode.removeChild(cm.getWrapperElement()); |
|
textarea.style.display = ""; |
|
if (textarea.form) { |
|
off(textarea.form, "submit", save); |
|
if (typeof textarea.form.submit == "function") |
|
textarea.form.submit = realSubmit; |
|
} |
|
}; |
|
}; |
|
|
|
textarea.style.display = "none"; |
|
var cm = CodeMirror(function(node) { |
|
textarea.parentNode.insertBefore(node, textarea.nextSibling); |
|
}, options); |
|
return cm; |
|
}; |
|
|
|
// STRING STREAM |
|
|
|
// Fed to the mode parsers, provides helper functions to make |
|
// parsers more succinct. |
|
|
|
var StringStream = CodeMirror.StringStream = function(string, tabSize) { |
|
this.pos = this.start = 0; |
|
this.string = string; |
|
this.tabSize = tabSize || 8; |
|
this.lastColumnPos = this.lastColumnValue = 0; |
|
this.lineStart = 0; |
|
}; |
|
|
|
StringStream.prototype = { |
|
eol: function() {return this.pos >= this.string.length;}, |
|
sol: function() {return this.pos == this.lineStart;}, |
|
peek: function() {return this.string.charAt(this.pos) || undefined;}, |
|
next: function() { |
|
if (this.pos < this.string.length) |
|
return this.string.charAt(this.pos++); |
|
}, |
|
eat: function(match) { |
|
var ch = this.string.charAt(this.pos); |
|
if (typeof match == "string") var ok = ch == match; |
|
else var ok = ch && (match.test ? match.test(ch) : match(ch)); |
|
if (ok) {++this.pos; return ch;} |
|
}, |
|
eatWhile: function(match) { |
|
var start = this.pos; |
|
while (this.eat(match)){} |
|
return this.pos > start; |
|
}, |
|
eatSpace: function() { |
|
var start = this.pos; |
|
while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; |
|
return this.pos > start; |
|
}, |
|
skipToEnd: function() {this.pos = this.string.length;}, |
|
skipTo: function(ch) { |
|
var found = this.string.indexOf(ch, this.pos); |
|
if (found > -1) {this.pos = found; return true;} |
|
}, |
|
backUp: function(n) {this.pos -= n;}, |
|
column: function() { |
|
if (this.lastColumnPos < this.start) { |
|
this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); |
|
this.lastColumnPos = this.start; |
|
} |
|
return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); |
|
}, |
|
indentation: function() { |
|
return countColumn(this.string, null, this.tabSize) - |
|
(this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); |
|
}, |
|
match: function(pattern, consume, caseInsensitive) { |
|
if (typeof pattern == "string") { |
|
var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; |
|
var substr = this.string.substr(this.pos, pattern.length); |
|
if (cased(substr) == cased(pattern)) { |
|
if (consume !== false) this.pos += pattern.length; |
|
return true; |
|
} |
|
} else { |
|
var match = this.string.slice(this.pos).match(pattern); |
|
if (match && match.index > 0) return null; |
|
if (match && consume !== false) this.pos += match[0].length; |
|
return match; |
|
} |
|
}, |
|
current: function(){return this.string.slice(this.start, this.pos);}, |
|
hideFirstChars: function(n, inner) { |
|
this.lineStart += n; |
|
try { return inner(); } |
|
finally { this.lineStart -= n; } |
|
} |
|
}; |
|
|
|
// TEXTMARKERS |
|
|
|
// Created with markText and setBookmark methods. A TextMarker is a |
|
// handle that can be used to clear or find a marked position in the |
|
// document. Line objects hold arrays (markedSpans) containing |
|
// {from, to, marker} object pointing to such marker objects, and |
|
// indicating that such a marker is present on that line. Multiple |
|
// lines may point to the same marker when it spans across lines. |
|
// The spans will have null for their from/to properties when the |
|
// marker continues beyond the start/end of the line. Markers have |
|
// links back to the lines they currently touch. |
|
|
|
var nextMarkerId = 0; |
|
|
|
var TextMarker = CodeMirror.TextMarker = function(doc, type) { |
|
this.lines = []; |
|
this.type = type; |
|
this.doc = doc; |
|
this.id = ++nextMarkerId; |
|
}; |
|
eventMixin(TextMarker); |
|
|
|
// Clear the marker. |
|
TextMarker.prototype.clear = function() { |
|
if (this.explicitlyCleared) return; |
|
var cm = this.doc.cm, withOp = cm && !cm.curOp; |
|
if (withOp) startOperation(cm); |
|
if (hasHandler(this, "clear")) { |
|
var found = this.find(); |
|
if (found) signalLater(this, "clear", found.from, found.to); |
|
} |
|
var min = null, max = null; |
|
for (var i = 0; i < this.lines.length; ++i) { |
|
var line = this.lines[i]; |
|
var span = getMarkedSpanFor(line.markedSpans, this); |
|
if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); |
|
else if (cm) { |
|
if (span.to != null) max = lineNo(line); |
|
if (span.from != null) min = lineNo(line); |
|
} |
|
line.markedSpans = removeMarkedSpan(line.markedSpans, span); |
|
if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) |
|
updateLineHeight(line, textHeight(cm.display)); |
|
} |
|
if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { |
|
var visual = visualLine(this.lines[i]), len = lineLength(visual); |
|
if (len > cm.display.maxLineLength) { |
|
cm.display.maxLine = visual; |
|
cm.display.maxLineLength = len; |
|
cm.display.maxLineChanged = true; |
|
} |
|
} |
|
|
|
if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); |
|
this.lines.length = 0; |
|
this.explicitlyCleared = true; |
|
if (this.atomic && this.doc.cantEdit) { |
|
this.doc.cantEdit = false; |
|
if (cm) reCheckSelection(cm.doc); |
|
} |
|
if (cm) signalLater(cm, "markerCleared", cm, this); |
|
if (withOp) endOperation(cm); |
|
if (this.parent) this.parent.clear(); |
|
}; |
|
|
|
// Find the position of the marker in the document. Returns a {from, |
|
// to} object by default. Side can be passed to get a specific side |
|
// -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the |
|
// Pos objects returned contain a line object, rather than a line |
|
// number (used to prevent looking up the same line twice). |
|
TextMarker.prototype.find = function(side, lineObj) { |
|
if (side == null && this.type == "bookmark") side = 1; |
|
var from, to; |
|
for (var i = 0; i < this.lines.length; ++i) { |
|
var line = this.lines[i]; |
|
var span = getMarkedSpanFor(line.markedSpans, this); |
|
if (span.from != null) { |
|
from = Pos(lineObj ? line : lineNo(line), span.from); |
|
if (side == -1) return from; |
|
} |
|
if (span.to != null) { |
|
to = Pos(lineObj ? line : lineNo(line), span.to); |
|
if (side == 1) return to; |
|
} |
|
} |
|
return from && {from: from, to: to}; |
|
}; |
|
|
|
// Signals that the marker's widget changed, and surrounding layout |
|
// should be recomputed. |
|
TextMarker.prototype.changed = function() { |
|
var pos = this.find(-1, true), widget = this, cm = this.doc.cm; |
|
if (!pos || !cm) return; |
|
runInOp(cm, function() { |
|
var line = pos.line, lineN = lineNo(pos.line); |
|
var view = findViewForLine(cm, lineN); |
|
if (view) { |
|
clearLineMeasurementCacheFor(view); |
|
cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; |
|
} |
|
cm.curOp.updateMaxLine = true; |
|
if (!lineIsHidden(widget.doc, line) && widget.height != null) { |
|
var oldHeight = widget.height; |
|
widget.height = null; |
|
var dHeight = widgetHeight(widget) - oldHeight; |
|
if (dHeight) |
|
updateLineHeight(line, line.height + dHeight); |
|
} |
|
}); |
|
}; |
|
|
|
TextMarker.prototype.attachLine = function(line) { |
|
if (!this.lines.length && this.doc.cm) { |
|
var op = this.doc.cm.curOp; |
|
if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) |
|
(op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); |
|
} |
|
this.lines.push(line); |
|
}; |
|
TextMarker.prototype.detachLine = function(line) { |
|
this.lines.splice(indexOf(this.lines, line), 1); |
|
if (!this.lines.length && this.doc.cm) { |
|
var op = this.doc.cm.curOp; |
|
(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); |
|
} |
|
}; |
|
|
|
// Collapsed markers have unique ids, in order to be able to order |
|
// them, which is needed for uniquely determining an outer marker |
|
// when they overlap (they may nest, but not partially overlap). |
|
var nextMarkerId = 0; |
|
|
|
// Create a marker, wire it up to the right lines, and |
|
function markText(doc, from, to, options, type) { |
|
// Shared markers (across linked documents) are handled separately |
|
// (markTextShared will call out to this again, once per |
|
// document). |
|
if (options && options.shared) return markTextShared(doc, from, to, options, type); |
|
// Ensure we are in an operation. |
|
if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); |
|
|
|
var marker = new TextMarker(doc, type), diff = cmp(from, to); |
|
if (options) copyObj(options, marker, false); |
|
// Don't connect empty markers unless clearWhenEmpty is false |
|
if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) |
|
return marker; |
|
if (marker.replacedWith) { |
|
// Showing up as a widget implies collapsed (widget replaces text) |
|
marker.collapsed = true; |
|
marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); |
|
if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true"); |
|
if (options.insertLeft) marker.widgetNode.insertLeft = true; |
|
} |
|
if (marker.collapsed) { |
|
if (conflictingCollapsedRange(doc, from.line, from, to, marker) || |
|
from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) |
|
throw new Error("Inserting collapsed marker partially overlapping an existing one"); |
|
sawCollapsedSpans = true; |
|
} |
|
|
|
if (marker.addToHistory) |
|
addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); |
|
|
|
var curLine = from.line, cm = doc.cm, updateMaxLine; |
|
doc.iter(curLine, to.line + 1, function(line) { |
|
if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) |
|
updateMaxLine = true; |
|
if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); |
|
addMarkedSpan(line, new MarkedSpan(marker, |
|
curLine == from.line ? from.ch : null, |
|
curLine == to.line ? to.ch : null)); |
|
++curLine; |
|
}); |
|
// lineIsHidden depends on the presence of the spans, so needs a second pass |
|
if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { |
|
if (lineIsHidden(doc, line)) updateLineHeight(line, 0); |
|
}); |
|
|
|
if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); |
|
|
|
if (marker.readOnly) { |
|
sawReadOnlySpans = true; |
|
if (doc.history.done.length || doc.history.undone.length) |
|
doc.clearHistory(); |
|
} |
|
if (marker.collapsed) { |
|
marker.id = ++nextMarkerId; |
|
marker.atomic = true; |
|
} |
|
if (cm) { |
|
// Sync editor state |
|
if (updateMaxLine) cm.curOp.updateMaxLine = true; |
|
if (marker.collapsed) |
|
regChange(cm, from.line, to.line + 1); |
|
else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) |
|
for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); |
|
if (marker.atomic) reCheckSelection(cm.doc); |
|
signalLater(cm, "markerAdded", cm, marker); |
|
} |
|
return marker; |
|
} |
|
|
|
// SHARED TEXTMARKERS |
|
|
|
// A shared marker spans multiple linked documents. It is |
|
// implemented as a meta-marker-object controlling multiple normal |
|
// markers. |
|
var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { |
|
this.markers = markers; |
|
this.primary = primary; |
|
for (var i = 0; i < markers.length; ++i) |
|
markers[i].parent = this; |
|
}; |
|
eventMixin(SharedTextMarker); |
|
|
|
SharedTextMarker.prototype.clear = function() { |
|
if (this.explicitlyCleared) return; |
|
this.explicitlyCleared = true; |
|
for (var i = 0; i < this.markers.length; ++i) |
|
this.markers[i].clear(); |
|
signalLater(this, "clear"); |
|
}; |
|
SharedTextMarker.prototype.find = function(side, lineObj) { |
|
return this.primary.find(side, lineObj); |
|
}; |
|
|
|
function markTextShared(doc, from, to, options, type) { |
|
options = copyObj(options); |
|
options.shared = false; |
|
var markers = [markText(doc, from, to, options, type)], primary = markers[0]; |
|
var widget = options.widgetNode; |
|
linkedDocs(doc, function(doc) { |
|
if (widget) options.widgetNode = widget.cloneNode(true); |
|
markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); |
|
for (var i = 0; i < doc.linked.length; ++i) |
|
if (doc.linked[i].isParent) return; |
|
primary = lst(markers); |
|
}); |
|
return new SharedTextMarker(markers, primary); |
|
} |
|
|
|
function findSharedMarkers(doc) { |
|
return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), |
|
function(m) { return m.parent; }); |
|
} |
|
|
|
function copySharedMarkers(doc, markers) { |
|
for (var i = 0; i < markers.length; i++) { |
|
var marker = markers[i], pos = marker.find(); |
|
var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); |
|
if (cmp(mFrom, mTo)) { |
|
var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); |
|
marker.markers.push(subMark); |
|
subMark.parent = marker; |
|
} |
|
} |
|
} |
|
|
|
function detachSharedMarkers(markers) { |
|
for (var i = 0; i < markers.length; i++) { |
|
var marker = markers[i], linked = [marker.primary.doc];; |
|
linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); |
|
for (var j = 0; j < marker.markers.length; j++) { |
|
var subMarker = marker.markers[j]; |
|
if (indexOf(linked, subMarker.doc) == -1) { |
|
subMarker.parent = null; |
|
marker.markers.splice(j--, 1); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// TEXTMARKER SPANS |
|
|
|
function MarkedSpan(marker, from, to) { |
|
this.marker = marker; |
|
this.from = from; this.to = to; |
|
} |
|
|
|
// Search an array of spans for a span matching the given marker. |
|
function getMarkedSpanFor(spans, marker) { |
|
if (spans) for (var i = 0; i < spans.length; ++i) { |
|
var span = spans[i]; |
|
if (span.marker == marker) return span; |
|
} |
|
} |
|
// Remove a span from an array, returning undefined if no spans are |
|
// left (we don't store arrays for lines without spans). |
|
function removeMarkedSpan(spans, span) { |
|
for (var r, i = 0; i < spans.length; ++i) |
|
if (spans[i] != span) (r || (r = [])).push(spans[i]); |
|
return r; |
|
} |
|
// Add a span to a line. |
|
function addMarkedSpan(line, span) { |
|
line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; |
|
span.marker.attachLine(line); |
|
} |
|
|
|
// Used for the algorithm that adjusts markers for a change in the |
|
// document. These functions cut an array of spans at a given |
|
// character position, returning an array of remaining chunks (or |
|
// undefined if nothing remains). |
|
function markedSpansBefore(old, startCh, isInsert) { |
|
if (old) for (var i = 0, nw; i < old.length; ++i) { |
|
var span = old[i], marker = span.marker; |
|
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); |
|
if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { |
|
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); |
|
(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); |
|
} |
|
} |
|
return nw; |
|
} |
|
function markedSpansAfter(old, endCh, isInsert) { |
|
if (old) for (var i = 0, nw; i < old.length; ++i) { |
|
var span = old[i], marker = span.marker; |
|
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); |
|
if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { |
|
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); |
|
(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, |
|
span.to == null ? null : span.to - endCh)); |
|
} |
|
} |
|
return nw; |
|
} |
|
|
|
// Given a change object, compute the new set of marker spans that |
|
// cover the line in which the change took place. Removes spans |
|
// entirely within the change, reconnects spans belonging to the |
|
// same marker that appear on both sides of the change, and cuts off |
|
// spans partially within the change. Returns an array of span |
|
// arrays with one element for each line in (after) the change. |
|
function stretchSpansOverChange(doc, change) { |
|
if (change.full) return null; |
|
var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; |
|
var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; |
|
if (!oldFirst && !oldLast) return null; |
|
|
|
var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; |
|
// Get the spans that 'stick out' on both sides |
|
var first = markedSpansBefore(oldFirst, startCh, isInsert); |
|
var last = markedSpansAfter(oldLast, endCh, isInsert); |
|
|
|
// Next, merge those two ends |
|
var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); |
|
if (first) { |
|
// Fix up .to properties of first |
|
for (var i = 0; i < first.length; ++i) { |
|
var span = first[i]; |
|
if (span.to == null) { |
|
var found = getMarkedSpanFor(last, span.marker); |
|
if (!found) span.to = startCh; |
|
else if (sameLine) span.to = found.to == null ? null : found.to + offset; |
|
} |
|
} |
|
} |
|
if (last) { |
|
// Fix up .from in last (or move them into first in case of sameLine) |
|
for (var i = 0; i < last.length; ++i) { |
|
var span = last[i]; |
|
if (span.to != null) span.to += offset; |
|
if (span.from == null) { |
|
var found = getMarkedSpanFor(first, span.marker); |
|
if (!found) { |
|
span.from = offset; |
|
if (sameLine) (first || (first = [])).push(span); |
|
} |
|
} else { |
|
span.from += offset; |
|
if (sameLine) (first || (first = [])).push(span); |
|
} |
|
} |
|
} |
|
// Make sure we didn't create any zero-length spans |
|
if (first) first = clearEmptySpans(first); |
|
if (last && last != first) last = clearEmptySpans(last); |
|
|
|
var newMarkers = [first]; |
|
if (!sameLine) { |
|
// Fill gap with whole-line-spans |
|
var gap = change.text.length - 2, gapMarkers; |
|
if (gap > 0 && first) |
|
for (var i = 0; i < first.length; ++i) |
|
if (first[i].to == null) |
|
(gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); |
|
for (var i = 0; i < gap; ++i) |
|
newMarkers.push(gapMarkers); |
|
newMarkers.push(last); |
|
} |
|
return newMarkers; |
|
} |
|
|
|
// Remove spans that are empty and don't have a clearWhenEmpty |
|
// option of false. |
|
function clearEmptySpans(spans) { |
|
for (var i = 0; i < spans.length; ++i) { |
|
var span = spans[i]; |
|
if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) |
|
spans.splice(i--, 1); |
|
} |
|
if (!spans.length) return null; |
|
return spans; |
|
} |
|
|
|
// Used for un/re-doing changes from the history. Combines the |
|
// result of computing the existing spans with the set of spans that |
|
// existed in the history (so that deleting around a span and then |
|
// undoing brings back the span). |
|
function mergeOldSpans(doc, change) { |
|
var old = getOldSpans(doc, change); |
|
var stretched = stretchSpansOverChange(doc, change); |
|
if (!old) return stretched; |
|
if (!stretched) return old; |
|
|
|
for (var i = 0; i < old.length; ++i) { |
|
var oldCur = old[i], stretchCur = stretched[i]; |
|
if (oldCur && stretchCur) { |
|
spans: for (var j = 0; j < stretchCur.length; ++j) { |
|
var span = stretchCur[j]; |
|
for (var k = 0; k < oldCur.length; ++k) |
|
if (oldCur[k].marker == span.marker) continue spans; |
|
oldCur.push(span); |
|
} |
|
} else if (stretchCur) { |
|
old[i] = stretchCur; |
|
} |
|
} |
|
return old; |
|
} |
|
|
|
// Used to 'clip' out readOnly ranges when making a change. |
|
function removeReadOnlyRanges(doc, from, to) { |
|
var markers = null; |
|
doc.iter(from.line, to.line + 1, function(line) { |
|
if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { |
|
var mark = line.markedSpans[i].marker; |
|
if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) |
|
(markers || (markers = [])).push(mark); |
|
} |
|
}); |
|
if (!markers) return null; |
|
var parts = [{from: from, to: to}]; |
|
for (var i = 0; i < markers.length; ++i) { |
|
var mk = markers[i], m = mk.find(0); |
|
for (var j = 0; j < parts.length; ++j) { |
|
var p = parts[j]; |
|
if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; |
|
var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); |
|
if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) |
|
newParts.push({from: p.from, to: m.from}); |
|
if (dto > 0 || !mk.inclusiveRight && !dto) |
|
newParts.push({from: m.to, to: p.to}); |
|
parts.splice.apply(parts, newParts); |
|
j += newParts.length - 1; |
|
} |
|
} |
|
return parts; |
|
} |
|
|
|
// Connect or disconnect spans from a line. |
|
function detachMarkedSpans(line) { |
|
var spans = line.markedSpans; |
|
if (!spans) return; |
|
for (var i = 0; i < spans.length; ++i) |
|
spans[i].marker.detachLine(line); |
|
line.markedSpans = null; |
|
} |
|
function attachMarkedSpans(line, spans) { |
|
if (!spans) return; |
|
for (var i = 0; i < spans.length; ++i) |
|
spans[i].marker.attachLine(line); |
|
line.markedSpans = spans; |
|
} |
|
|
|
// Helpers used when computing which overlapping collapsed span |
|
// counts as the larger one. |
|
function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } |
|
function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } |
|
|
|
// Returns a number indicating which of two overlapping collapsed |
|
// spans is larger (and thus includes the other). Falls back to |
|
// comparing ids when the spans cover exactly the same range. |
|
function compareCollapsedMarkers(a, b) { |
|
var lenDiff = a.lines.length - b.lines.length; |
|
if (lenDiff != 0) return lenDiff; |
|
var aPos = a.find(), bPos = b.find(); |
|
var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); |
|
if (fromCmp) return -fromCmp; |
|
var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); |
|
if (toCmp) return toCmp; |
|
return b.id - a.id; |
|
} |
|
|
|
// Find out whether a line ends or starts in a collapsed span. If |
|
// so, return the marker for that span. |
|
function collapsedSpanAtSide(line, start) { |
|
var sps = sawCollapsedSpans && line.markedSpans, found; |
|
if (sps) for (var sp, i = 0; i < sps.length; ++i) { |
|
sp = sps[i]; |
|
if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && |
|
(!found || compareCollapsedMarkers(found, sp.marker) < 0)) |
|
found = sp.marker; |
|
} |
|
return found; |
|
} |
|
function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } |
|
function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } |
|
|
|
// Test whether there exists a collapsed span that partially |
|
// overlaps (covers the start or end, but not both) of a new span. |
|
// Such overlap is not allowed. |
|
function conflictingCollapsedRange(doc, lineNo, from, to, marker) { |
|
var line = getLine(doc, lineNo); |
|
var sps = sawCollapsedSpans && line.markedSpans; |
|
if (sps) for (var i = 0; i < sps.length; ++i) { |
|
var sp = sps[i]; |
|
if (!sp.marker.collapsed) continue; |
|
var found = sp.marker.find(0); |
|
var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); |
|
var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); |
|
if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; |
|
if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || |
|
fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) |
|
return true; |
|
} |
|
} |
|
|
|
// A visual line is a line as drawn on the screen. Folding, for |
|
// example, can cause multiple logical lines to appear on the same |
|
// visual line. This finds the start of the visual line that the |
|
// given line is part of (usually that is the line itself). |
|
function visualLine(line) { |
|
var merged; |
|
while (merged = collapsedSpanAtStart(line)) |
|
line = merged.find(-1, true).line; |
|
return line; |
|
} |
|
|
|
// Returns an array of logical lines that continue the visual line |
|
// started by the argument, or undefined if there are no such lines. |
|
function visualLineContinued(line) { |
|
var merged, lines; |
|
while (merged = collapsedSpanAtEnd(line)) { |
|
line = merged.find(1, true).line; |
|
(lines || (lines = [])).push(line); |
|
} |
|
return lines; |
|
} |
|
|
|
// Get the line number of the start of the visual line that the |
|
// given line number is part of. |
|
function visualLineNo(doc, lineN) { |
|
var line = getLine(doc, lineN), vis = visualLine(line); |
|
if (line == vis) return lineN; |
|
return lineNo(vis); |
|
} |
|
// Get the line number of the start of the next visual line after |
|
// the given line. |
|
function visualLineEndNo(doc, lineN) { |
|
if (lineN > doc.lastLine()) return lineN; |
|
var line = getLine(doc, lineN), merged; |
|
if (!lineIsHidden(doc, line)) return lineN; |
|
while (merged = collapsedSpanAtEnd(line)) |
|
line = merged.find(1, true).line; |
|
return lineNo(line) + 1; |
|
} |
|
|
|
// Compute whether a line is hidden. Lines count as hidden when they |
|
// are part of a visual line that starts with another line, or when |
|
// they are entirely covered by collapsed, non-widget span. |
|
function lineIsHidden(doc, line) { |
|
var sps = sawCollapsedSpans && line.markedSpans; |
|
if (sps) for (var sp, i = 0; i < sps.length; ++i) { |
|
sp = sps[i]; |
|
if (!sp.marker.collapsed) continue; |
|
if (sp.from == null) return true; |
|
if (sp.marker.widgetNode) continue; |
|
if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) |
|
return true; |
|
} |
|
} |
|
function lineIsHiddenInner(doc, line, span) { |
|
if (span.to == null) { |
|
var end = span.marker.find(1, true); |
|
return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); |
|
} |
|
if (span.marker.inclusiveRight && span.to == line.text.length) |
|
return true; |
|
for (var sp, i = 0; i < line.markedSpans.length; ++i) { |
|
sp = line.markedSpans[i]; |
|
if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && |
|
(sp.to == null || sp.to != span.from) && |
|
(sp.marker.inclusiveLeft || span.marker.inclusiveRight) && |
|
lineIsHiddenInner(doc, line, sp)) return true; |
|
} |
|
} |
|
|
|
// LINE WIDGETS |
|
|
|
// Line widgets are block elements displayed above or below a line. |
|
|
|
var LineWidget = CodeMirror.LineWidget = function(doc, node, options) { |
|
if (options) for (var opt in options) if (options.hasOwnProperty(opt)) |
|
this[opt] = options[opt]; |
|
this.doc = doc; |
|
this.node = node; |
|
}; |
|
eventMixin(LineWidget); |
|
|
|
function adjustScrollWhenAboveVisible(cm, line, diff) { |
|
if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) |
|
addToScrollPos(cm, null, diff); |
|
} |
|
|
|
LineWidget.prototype.clear = function() { |
|
var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); |
|
if (no == null || !ws) return; |
|
for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); |
|
if (!ws.length) line.widgets = null; |
|
var height = widgetHeight(this); |
|
updateLineHeight(line, Math.max(0, line.height - height)); |
|
if (cm) runInOp(cm, function() { |
|
adjustScrollWhenAboveVisible(cm, line, -height); |
|
regLineChange(cm, no, "widget"); |
|
}); |
|
}; |
|
LineWidget.prototype.changed = function() { |
|
var oldH = this.height, cm = this.doc.cm, line = this.line; |
|
this.height = null; |
|
var diff = widgetHeight(this) - oldH; |
|
if (!diff) return; |
|
updateLineHeight(line, line.height + diff); |
|
if (cm) runInOp(cm, function() { |
|
cm.curOp.forceUpdate = true; |
|
adjustScrollWhenAboveVisible(cm, line, diff); |
|
}); |
|
}; |
|
|
|
function widgetHeight(widget) { |
|
if (widget.height != null) return widget.height; |
|
var cm = widget.doc.cm; |
|
if (!cm) return 0; |
|
if (!contains(document.body, widget.node)) { |
|
var parentStyle = "position: relative;"; |
|
if (widget.coverGutter) |
|
parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; |
|
if (widget.noHScroll) |
|
parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; |
|
removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); |
|
} |
|
return widget.height = widget.node.offsetHeight; |
|
} |
|
|
|
function addLineWidget(doc, handle, node, options) { |
|
var widget = new LineWidget(doc, node, options); |
|
var cm = doc.cm; |
|
if (cm && widget.noHScroll) cm.display.alignWidgets = true; |
|
changeLine(doc, handle, "widget", function(line) { |
|
var widgets = line.widgets || (line.widgets = []); |
|
if (widget.insertAt == null) widgets.push(widget); |
|
else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); |
|
widget.line = line; |
|
if (cm && !lineIsHidden(doc, line)) { |
|
var aboveVisible = heightAtLine(line) < doc.scrollTop; |
|
updateLineHeight(line, line.height + widgetHeight(widget)); |
|
if (aboveVisible) addToScrollPos(cm, null, widget.height); |
|
cm.curOp.forceUpdate = true; |
|
} |
|
return true; |
|
}); |
|
return widget; |
|
} |
|
|
|
// LINE DATA STRUCTURE |
|
|
|
// Line objects. These hold state related to a line, including |
|
// highlighting info (the styles array). |
|
var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { |
|
this.text = text; |
|
attachMarkedSpans(this, markedSpans); |
|
this.height = estimateHeight ? estimateHeight(this) : 1; |
|
}; |
|
eventMixin(Line); |
|
Line.prototype.lineNo = function() { return lineNo(this); }; |
|
|
|
// Change the content (text, markers) of a line. Automatically |
|
// invalidates cached information and tries to re-estimate the |
|
// line's height. |
|
function updateLine(line, text, markedSpans, estimateHeight) { |
|
line.text = text; |
|
if (line.stateAfter) line.stateAfter = null; |
|
if (line.styles) line.styles = null; |
|
if (line.order != null) line.order = null; |
|
detachMarkedSpans(line); |
|
attachMarkedSpans(line, markedSpans); |
|
var estHeight = estimateHeight ? estimateHeight(line) : 1; |
|
if (estHeight != line.height) updateLineHeight(line, estHeight); |
|
} |
|
|
|
// Detach a line from the document tree and its markers. |
|
function cleanUpLine(line) { |
|
line.parent = null; |
|
detachMarkedSpans(line); |
|
} |
|
|
|
function extractLineClasses(type, output) { |
|
if (type) for (;;) { |
|
var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); |
|
if (!lineClass) break; |
|
type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); |
|
var prop = lineClass[1] ? "bgClass" : "textClass"; |
|
if (output[prop] == null) |
|
output[prop] = lineClass[2]; |
|
else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) |
|
output[prop] += " " + lineClass[2]; |
|
} |
|
return type; |
|
} |
|
|
|
function callBlankLine(mode, state) { |
|
if (mode.blankLine) return mode.blankLine(state); |
|
if (!mode.innerMode) return; |
|
var inner = CodeMirror.innerMode(mode, state); |
|
if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); |
|
} |
|
|
|
function readToken(mode, stream, state, inner) { |
|
for (var i = 0; i < 10; i++) { |
|
if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode; |
|
var style = mode.token(stream, state); |
|
if (stream.pos > stream.start) return style; |
|
} |
|
throw new Error("Mode " + mode.name + " failed to advance stream."); |
|
} |
|
|
|
// Utility for getTokenAt and getLineTokens |
|
function takeToken(cm, pos, precise, asArray) { |
|
function getObj(copy) { |
|
return {start: stream.start, end: stream.pos, |
|
string: stream.current(), |
|
type: style || null, |
|
state: copy ? copyState(doc.mode, state) : state}; |
|
} |
|
|
|
var doc = cm.doc, mode = doc.mode, style; |
|
pos = clipPos(doc, pos); |
|
var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise); |
|
var stream = new StringStream(line.text, cm.options.tabSize), tokens; |
|
if (asArray) tokens = []; |
|
while ((asArray || stream.pos < pos.ch) && !stream.eol()) { |
|
stream.start = stream.pos; |
|
style = readToken(mode, stream, state); |
|
if (asArray) tokens.push(getObj(true)); |
|
} |
|
return asArray ? tokens : getObj(); |
|
} |
|
|
|
// Run the given mode's parser over a line, calling f for each token. |
|
function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { |
|
var flattenSpans = mode.flattenSpans; |
|
if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; |
|
var curStart = 0, curStyle = null; |
|
var stream = new StringStream(text, cm.options.tabSize), style; |
|
var inner = cm.options.addModeClass && [null]; |
|
if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); |
|
while (!stream.eol()) { |
|
if (stream.pos > cm.options.maxHighlightLength) { |
|
flattenSpans = false; |
|
if (forceToEnd) processLine(cm, text, state, stream.pos); |
|
stream.pos = text.length; |
|
style = null; |
|
} else { |
|
style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses); |
|
} |
|
if (inner) { |
|
var mName = inner[0].name; |
|
if (mName) style = "m-" + (style ? mName + " " + style : mName); |
|
} |
|
if (!flattenSpans || curStyle != style) { |
|
while (curStart < stream.start) { |
|
curStart = Math.min(stream.start, curStart + 50000); |
|
f(curStart, curStyle); |
|
} |
|
curStyle = style; |
|
} |
|
stream.start = stream.pos; |
|
} |
|
while (curStart < stream.pos) { |
|
// Webkit seems to refuse to render text nodes longer than 57444 characters |
|
var pos = Math.min(stream.pos, curStart + 50000); |
|
f(pos, curStyle); |
|
curStart = pos; |
|
} |
|
} |
|
|
|
// Compute a style array (an array starting with a mode generation |
|
// -- for invalidation -- followed by pairs of end positions and |
|
// style strings), which is used to highlight the tokens on the |
|
// line. |
|
function highlightLine(cm, line, state, forceToEnd) { |
|
// A styles array always starts with a number identifying the |
|
// mode/overlays that it is based on (for easy invalidation). |
|
var st = [cm.state.modeGen], lineClasses = {}; |
|
// Compute the base array of styles |
|
runMode(cm, line.text, cm.doc.mode, state, function(end, style) { |
|
st.push(end, style); |
|
}, lineClasses, forceToEnd); |
|
|
|
// Run overlays, adjust style array. |
|
for (var o = 0; o < cm.state.overlays.length; ++o) { |
|
var overlay = cm.state.overlays[o], i = 1, at = 0; |
|
runMode(cm, line.text, overlay.mode, true, function(end, style) { |
|
var start = i; |
|
// Ensure there's a token end at the current position, and that i points at it |
|
while (at < end) { |
|
var i_end = st[i]; |
|
if (i_end > end) |
|
st.splice(i, 1, end, st[i+1], i_end); |
|
i += 2; |
|
at = Math.min(end, i_end); |
|
} |
|
if (!style) return; |
|
if (overlay.opaque) { |
|
st.splice(start, i - start, end, "cm-overlay " + style); |
|
i = start + 2; |
|
} else { |
|
for (; start < i; start += 2) { |
|
var cur = st[start+1]; |
|
st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; |
|
} |
|
} |
|
}, lineClasses); |
|
} |
|
|
|
return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; |
|
} |
|
|
|
function getLineStyles(cm, line, updateFrontier) { |
|
if (!line.styles || line.styles[0] != cm.state.modeGen) { |
|
var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); |
|
line.styles = result.styles; |
|
if (result.classes) line.styleClasses = result.classes; |
|
else if (line.styleClasses) line.styleClasses = null; |
|
if (updateFrontier === cm.doc.frontier) cm.doc.frontier++; |
|
} |
|
return line.styles; |
|
} |
|
|
|
// Lightweight form of highlight -- proceed over this line and |
|
// update state, but don't save a style array. Used for lines that |
|
// aren't currently visible. |
|
function processLine(cm, text, state, startAt) { |
|
var mode = cm.doc.mode; |
|
var stream = new StringStream(text, cm.options.tabSize); |
|
stream.start = stream.pos = startAt || 0; |
|
if (text == "") callBlankLine(mode, state); |
|
while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { |
|
readToken(mode, stream, state); |
|
stream.start = stream.pos; |
|
} |
|
} |
|
|
|
// Convert a style as returned by a mode (either null, or a string |
|
// containing one or more styles) to a CSS style. This is cached, |
|
// and also looks for line-wide styles. |
|
var styleToClassCache = {}, styleToClassCacheWithMode = {}; |
|
function interpretTokenStyle(style, options) { |
|
if (!style || /^\s*$/.test(style)) return null; |
|
var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; |
|
return cache[style] || |
|
(cache[style] = style.replace(/\S+/g, "cm-$&")); |
|
} |
|
|
|
// Render the DOM representation of the text of a line. Also builds |
|
// up a 'line map', which points at the DOM nodes that represent |
|
// specific stretches of text, and is used by the measuring code. |
|
// The returned object contains the DOM node, this map, and |
|
// information about line-wide styles that were set by the mode. |
|
function buildLineContent(cm, lineView) { |
|
// The padding-right forces the element to have a 'border', which |
|
// is needed on Webkit to be able to get line-level bounding |
|
// rectangles for it (in measureChar). |
|
var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); |
|
var builder = {pre: elt("pre", [content], "CodeMirror-line"), content: content, |
|
col: 0, pos: 0, cm: cm, |
|
splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; |
|
lineView.measure = {}; |
|
|
|
// Iterate over the logical lines that make up this visual line. |
|
for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { |
|
var line = i ? lineView.rest[i - 1] : lineView.line, order; |
|
builder.pos = 0; |
|
builder.addToken = buildToken; |
|
// Optionally wire in some hacks into the token-rendering |
|
// algorithm, to deal with browser quirks. |
|
if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) |
|
builder.addToken = buildTokenBadBidi(builder.addToken, order); |
|
builder.map = []; |
|
var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); |
|
insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); |
|
if (line.styleClasses) { |
|
if (line.styleClasses.bgClass) |
|
builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); |
|
if (line.styleClasses.textClass) |
|
builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); |
|
} |
|
|
|
// Ensure at least a single node is present, for measuring. |
|
if (builder.map.length == 0) |
|
builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); |
|
|
|
// Store the map and a cache object for the current logical line |
|
if (i == 0) { |
|
lineView.measure.map = builder.map; |
|
lineView.measure.cache = {}; |
|
} else { |
|
(lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); |
|
(lineView.measure.caches || (lineView.measure.caches = [])).push({}); |
|
} |
|
} |
|
|
|
// See issue #2901 |
|
if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className)) |
|
builder.content.className = "cm-tab-wrap-hack"; |
|
|
|
signal(cm, "renderLine", cm, lineView.line, builder.pre); |
|
if (builder.pre.className) |
|
builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); |
|
|
|
return builder; |
|
} |
|
|
|
function defaultSpecialCharPlaceholder(ch) { |
|
var token = elt("span", "\u2022", "cm-invalidchar"); |
|
token.title = "\\u" + ch.charCodeAt(0).toString(16); |
|
token.setAttribute("aria-label", token.title); |
|
return token; |
|
} |
|
|
|
// Build up the DOM representation for a single token, and add it to |
|
// the line map. Takes care to render special characters separately. |
|
function buildToken(builder, text, style, startStyle, endStyle, title, css) { |
|
if (!text) return; |
|
var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text; |
|
var special = builder.cm.state.specialChars, mustWrap = false; |
|
if (!special.test(text)) { |
|
builder.col += text.length; |
|
var content = document.createTextNode(displayText); |
|
builder.map.push(builder.pos, builder.pos + text.length, content); |
|
if (ie && ie_version < 9) mustWrap = true; |
|
builder.pos += text.length; |
|
} else { |
|
var content = document.createDocumentFragment(), pos = 0; |
|
while (true) { |
|
special.lastIndex = pos; |
|
var m = special.exec(text); |
|
var skipped = m ? m.index - pos : text.length - pos; |
|
if (skipped) { |
|
var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); |
|
if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); |
|
else content.appendChild(txt); |
|
builder.map.push(builder.pos, builder.pos + skipped, txt); |
|
builder.col += skipped; |
|
builder.pos += skipped; |
|
} |
|
if (!m) break; |
|
pos += skipped + 1; |
|
if (m[0] == "\t") { |
|
var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; |
|
var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); |
|
txt.setAttribute("role", "presentation"); |
|
txt.setAttribute("cm-text", "\t"); |
|
builder.col += tabWidth; |
|
} else { |
|
var txt = builder.cm.options.specialCharPlaceholder(m[0]); |
|
txt.setAttribute("cm-text", m[0]); |
|
if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); |
|
else content.appendChild(txt); |
|
builder.col += 1; |
|
} |
|
builder.map.push(builder.pos, builder.pos + 1, txt); |
|
builder.pos++; |
|
} |
|
} |
|
if (style || startStyle || endStyle || mustWrap || css) { |
|
var fullStyle = style || ""; |
|
if (startStyle) fullStyle += startStyle; |
|
if (endStyle) fullStyle += endStyle; |
|
var token = elt("span", [content], fullStyle, css); |
|
if (title) token.title = title; |
|
return builder.content.appendChild(token); |
|
} |
|
builder.content.appendChild(content); |
|
} |
|
|
|
function splitSpaces(old) { |
|
var out = " "; |
|
for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; |
|
out += " "; |
|
return out; |
|
} |
|
|
|
// Work around nonsense dimensions being reported for stretches of |
|
// right-to-left text. |
|
function buildTokenBadBidi(inner, order) { |
|
return function(builder, text, style, startStyle, endStyle, title, css) { |
|
style = style ? style + " cm-force-border" : "cm-force-border"; |
|
var start = builder.pos, end = start + text.length; |
|
for (;;) { |
|
// Find the part that overlaps with the start of this text |
|
for (var i = 0; i < order.length; i++) { |
|
var part = order[i]; |
|
if (part.to > start && part.from <= start) break; |
|
} |
|
if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css); |
|
inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); |
|
startStyle = null; |
|
text = text.slice(part.to - start); |
|
start = part.to; |
|
} |
|
}; |
|
} |
|
|
|
function buildCollapsedSpan(builder, size, marker, ignoreWidget) { |
|
var widget = !ignoreWidget && marker.widgetNode; |
|
if (widget) builder.map.push(builder.pos, builder.pos + size, widget); |
|
if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { |
|
if (!widget) |
|
widget = builder.content.appendChild(document.createElement("span")); |
|
widget.setAttribute("cm-marker", marker.id); |
|
} |
|
if (widget) { |
|
builder.cm.display.input.setUneditable(widget); |
|
builder.content.appendChild(widget); |
|
} |
|
builder.pos += size; |
|
} |
|
|
|
// Outputs a number of spans to make up a line, taking highlighting |
|
// and marked text into account. |
|
function insertLineContent(line, builder, styles) { |
|
var spans = line.markedSpans, allText = line.text, at = 0; |
|
if (!spans) { |
|
for (var i = 1; i < styles.length; i+=2) |
|
builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); |
|
return; |
|
} |
|
|
|
var len = allText.length, pos = 0, i = 1, text = "", style, css; |
|
var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; |
|
for (;;) { |
|
if (nextChange == pos) { // Update current marker set |
|
spanStyle = spanEndStyle = spanStartStyle = title = css = ""; |
|
collapsed = null; nextChange = Infinity; |
|
var foundBookmarks = []; |
|
for (var j = 0; j < spans.length; ++j) { |
|
var sp = spans[j], m = sp.marker; |
|
if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { |
|
foundBookmarks.push(m); |
|
} else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { |
|
if (sp.to != null && sp.to != pos && nextChange > sp.to) { |
|
nextChange = sp.to; |
|
spanEndStyle = ""; |
|
} |
|
if (m.className) spanStyle += " " + m.className; |
|
if (m.css) css = m.css; |
|
if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; |
|
if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; |
|
if (m.title && !title) title = m.title; |
|
if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) |
|
collapsed = sp; |
|
} else if (sp.from > pos && nextChange > sp.from) { |
|
nextChange = sp.from; |
|
} |
|
} |
|
if (collapsed && (collapsed.from || 0) == pos) { |
|
buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, |
|
collapsed.marker, collapsed.from == null); |
|
if (collapsed.to == null) return; |
|
if (collapsed.to == pos) collapsed = false; |
|
} |
|
if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) |
|
buildCollapsedSpan(builder, 0, foundBookmarks[j]); |
|
} |
|
if (pos >= len) break; |
|
|
|
var upto = Math.min(len, nextChange); |
|
while (true) { |
|
if (text) { |
|
var end = pos + text.length; |
|
if (!collapsed) { |
|
var tokenText = end > upto ? text.slice(0, upto - pos) : text; |
|
builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, |
|
spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); |
|
} |
|
if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} |
|
pos = end; |
|
spanStartStyle = ""; |
|
} |
|
text = allText.slice(at, at = styles[i++]); |
|
style = interpretTokenStyle(styles[i++], builder.cm.options); |
|
} |
|
} |
|
} |
|
|
|
// DOCUMENT DATA STRUCTURE |
|
|
|
// By default, updates that start and end at the beginning of a line |
|
// are treated specially, in order to make the association of line |
|
// widgets and marker elements with the text behave more intuitive. |
|
function isWholeLineUpdate(doc, change) { |
|
return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && |
|
(!doc.cm || doc.cm.options.wholeLineUpdateBefore); |
|
} |
|
|
|
// Perform a change on the document data structure. |
|
function updateDoc(doc, change, markedSpans, estimateHeight) { |
|
function spansFor(n) {return markedSpans ? markedSpans[n] : null;} |
|
function update(line, text, spans) { |
|
updateLine(line, text, spans, estimateHeight); |
|
signalLater(line, "change", line, change); |
|
} |
|
function linesFor(start, end) { |
|
for (var i = start, result = []; i < end; ++i) |
|
result.push(new Line(text[i], spansFor(i), estimateHeight)); |
|
return result; |
|
} |
|
|
|
var from = change.from, to = change.to, text = change.text; |
|
var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); |
|
var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; |
|
|
|
// Adjust the line structure |
|
if (change.full) { |
|
doc.insert(0, linesFor(0, text.length)); |
|
doc.remove(text.length, doc.size - text.length); |
|
} else if (isWholeLineUpdate(doc, change)) { |
|
// This is a whole-line replace. Treated specially to make |
|
// sure line objects move the way they are supposed to. |
|
var added = linesFor(0, text.length - 1); |
|
update(lastLine, lastLine.text, lastSpans); |
|
if (nlines) doc.remove(from.line, nlines); |
|
if (added.length) doc.insert(from.line, added); |
|
} else if (firstLine == lastLine) { |
|
if (text.length == 1) { |
|
update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); |
|
} else { |
|
var added = linesFor(1, text.length - 1); |
|
added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); |
|
update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); |
|
doc.insert(from.line + 1, added); |
|
} |
|
} else if (text.length == 1) { |
|
update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); |
|
doc.remove(from.line + 1, nlines); |
|
} else { |
|
update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); |
|
update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); |
|
var added = linesFor(1, text.length - 1); |
|
if (nlines > 1) doc.remove(from.line + 1, nlines - 1); |
|
doc.insert(from.line + 1, added); |
|
} |
|
|
|
signalLater(doc, "change", doc, change); |
|
} |
|
|
|
// The document is represented as a BTree consisting of leaves, with |
|
// chunk of lines in them, and branches, with up to ten leaves or |
|
// other branch nodes below them. The top node is always a branch |
|
// node, and is the document object itself (meaning it has |
|
// additional methods and properties). |
|
// |
|
// All nodes have parent links. The tree is used both to go from |
|
// line numbers to line objects, and to go from objects to numbers. |
|
// It also indexes by height, and is used to convert between height |
|
// and line object, and to find the total height of the document. |
|
// |
|
// See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html |
|
|
|
function LeafChunk(lines) { |
|
this.lines = lines; |
|
this.parent = null; |
|
for (var i = 0, height = 0; i < lines.length; ++i) { |
|
lines[i].parent = this; |
|
height += lines[i].height; |
|
} |
|
this.height = height; |
|
} |
|
|
|
LeafChunk.prototype = { |
|
chunkSize: function() { return this.lines.length; }, |
|
// Remove the n lines at offset 'at'. |
|
removeInner: function(at, n) { |
|
for (var i = at, e = at + n; i < e; ++i) { |
|
var line = this.lines[i]; |
|
this.height -= line.height; |
|
cleanUpLine(line); |
|
signalLater(line, "delete"); |
|
} |
|
this.lines.splice(at, n); |
|
}, |
|
// Helper used to collapse a small branch into a single leaf. |
|
collapse: function(lines) { |
|
lines.push.apply(lines, this.lines); |
|
}, |
|
// Insert the given array of lines at offset 'at', count them as |
|
// having the given height. |
|
insertInner: function(at, lines, height) { |
|
this.height += height; |
|
this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); |
|
for (var i = 0; i < lines.length; ++i) lines[i].parent = this; |
|
}, |
|
// Used to iterate over a part of the tree. |
|
iterN: function(at, n, op) { |
|
for (var e = at + n; at < e; ++at) |
|
if (op(this.lines[at])) return true; |
|
} |
|
}; |
|
|
|
function BranchChunk(children) { |
|
this.children = children; |
|
var size = 0, height = 0; |
|
for (var i = 0; i < children.length; ++i) { |
|
var ch = children[i]; |
|
size += ch.chunkSize(); height += ch.height; |
|
ch.parent = this; |
|
} |
|
this.size = size; |
|
this.height = height; |
|
this.parent = null; |
|
} |
|
|
|
BranchChunk.prototype = { |
|
chunkSize: function() { return this.size; }, |
|
removeInner: function(at, n) { |
|
this.size -= n; |
|
for (var i = 0; i < this.children.length; ++i) { |
|
var child = this.children[i], sz = child.chunkSize(); |
|
if (at < sz) { |
|
var rm = Math.min(n, sz - at), oldHeight = child.height; |
|
child.removeInner(at, rm); |
|
this.height -= oldHeight - child.height; |
|
if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } |
|
if ((n -= rm) == 0) break; |
|
at = 0; |
|
} else at -= sz; |
|
} |
|
// If the result is smaller than 25 lines, ensure that it is a |
|
// single leaf node. |
|
if (this.size - n < 25 && |
|
(this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { |
|
var lines = []; |
|
this.collapse(lines); |
|
this.children = [new LeafChunk(lines)]; |
|
this.children[0].parent = this; |
|
} |
|
}, |
|
collapse: function(lines) { |
|
for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); |
|
}, |
|
insertInner: function(at, lines, height) { |
|
this.size += lines.length; |
|
this.height += height; |
|
for (var i = 0; i < this.children.length; ++i) { |
|
var child = this.children[i], sz = child.chunkSize(); |
|
if (at <= sz) { |
|
child.insertInner(at, lines, height); |
|
if (child.lines && child.lines.length > 50) { |
|
while (child.lines.length > 50) { |
|
var spilled = child.lines.splice(child.lines.length - 25, 25); |
|
var newleaf = new LeafChunk(spilled); |
|
child.height -= newleaf.height; |
|
this.children.splice(i + 1, 0, newleaf); |
|
newleaf.parent = this; |
|
} |
|
this.maybeSpill(); |
|
} |
|
break; |
|
} |
|
at -= sz; |
|
} |
|
}, |
|
// When a node has grown, check whether it should be split. |
|
maybeSpill: function() { |
|
if (this.children.length <= 10) return; |
|
var me = this; |
|
do { |
|
var spilled = me.children.splice(me.children.length - 5, 5); |
|
var sibling = new BranchChunk(spilled); |
|
if (!me.parent) { // Become the parent node |
|
var copy = new BranchChunk(me.children); |
|
copy.parent = me; |
|
me.children = [copy, sibling]; |
|
me = copy; |
|
} else { |
|
me.size -= sibling.size; |
|
me.height -= sibling.height; |
|
var myIndex = indexOf(me.parent.children, me); |
|
me.parent.children.splice(myIndex + 1, 0, sibling); |
|
} |
|
sibling.parent = me.parent; |
|
} while (me.children.length > 10); |
|
me.parent.maybeSpill(); |
|
}, |
|
iterN: function(at, n, op) { |
|
for (var i = 0; i < this.children.length; ++i) { |
|
var child = this.children[i], sz = child.chunkSize(); |
|
if (at < sz) { |
|
var used = Math.min(n, sz - at); |
|
if (child.iterN(at, used, op)) return true; |
|
if ((n -= used) == 0) break; |
|
at = 0; |
|
} else at -= sz; |
|
} |
|
} |
|
}; |
|
|
|
var nextDocId = 0; |
|
var Doc = CodeMirror.Doc = function(text, mode, firstLine) { |
|
if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); |
|
if (firstLine == null) firstLine = 0; |
|
|
|
BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); |
|
this.first = firstLine; |
|
this.scrollTop = this.scrollLeft = 0; |
|
this.cantEdit = false; |
|
this.cleanGeneration = 1; |
|
this.frontier = firstLine; |
|
var start = Pos(firstLine, 0); |
|
this.sel = simpleSelection(start); |
|
this.history = new History(null); |
|
this.id = ++nextDocId; |
|
this.modeOption = mode; |
|
|
|
if (typeof text == "string") text = splitLines(text); |
|
updateDoc(this, {from: start, to: start, text: text}); |
|
setSelection(this, simpleSelection(start), sel_dontScroll); |
|
}; |
|
|
|
Doc.prototype = createObj(BranchChunk.prototype, { |
|
constructor: Doc, |
|
// Iterate over the document. Supports two forms -- with only one |
|
// argument, it calls that for each line in the document. With |
|
// three, it iterates over the range given by the first two (with |
|
// the second being non-inclusive). |
|
iter: function(from, to, op) { |
|
if (op) this.iterN(from - this.first, to - from, op); |
|
else this.iterN(this.first, this.first + this.size, from); |
|
}, |
|
|
|
// Non-public interface for adding and removing lines. |
|
insert: function(at, lines) { |
|
var height = 0; |
|
for (var i = 0; i < lines.length; ++i) height += lines[i].height; |
|
this.insertInner(at - this.first, lines, height); |
|
}, |
|
remove: function(at, n) { this.removeInner(at - this.first, n); }, |
|
|
|
// From here, the methods are part of the public interface. Most |
|
// are also available from CodeMirror (editor) instances. |
|
|
|
getValue: function(lineSep, lineFilter) { |
|
var lines = getLines(this, this.first, this.first + this.size, lineFilter); |
|
if (lineSep === false) return lines; |
|
return lines.join(lineSep || "\n"); |
|
}, |
|
setValue: docMethodOp(function(code) { |
|
var top = Pos(this.first, 0), last = this.first + this.size - 1; |
|
makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), |
|
text: splitLines(code), origin: "setValue", full: true}, true); |
|
setSelection(this, simpleSelection(top)); |
|
}), |
|
replaceRange: function(code, from, to, origin) { |
|
from = clipPos(this, from); |
|
to = to ? clipPos(this, to) : from; |
|
replaceRange(this, code, from, to, origin); |
|
}, |
|
getRange: function(from, to, lineSep) { |
|
var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); |
|
if (lineSep === false) return lines; |
|
return lines.join(lineSep || "\n"); |
|
}, |
|
|
|
getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, |
|
|
|
getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, |
|
getLineNumber: function(line) {return lineNo(line);}, |
|
|
|
getLineHandleVisualStart: function(line) { |
|
if (typeof line == "number") line = getLine(this, line); |
|
return visualLine(line); |
|
}, |
|
|
|
lineCount: function() {return this.size;}, |
|
firstLine: function() {return this.first;}, |
|
lastLine: function() {return this.first + this.size - 1;}, |
|
|
|
clipPos: function(pos) {return clipPos(this, pos);}, |
|
|
|
getCursor: function(start) { |
|
var range = this.sel.primary(), pos; |
|
if (start == null || start == "head") pos = range.head; |
|
else if (start == "anchor") pos = range.anchor; |
|
else if (start == "end" || start == "to" || start === false) pos = range.to(); |
|
else pos = range.from(); |
|
return pos; |
|
}, |
|
listSelections: function() { return this.sel.ranges; }, |
|
somethingSelected: function() {return this.sel.somethingSelected();}, |
|
|
|
setCursor: docMethodOp(function(line, ch, options) { |
|
setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); |
|
}), |
|
setSelection: docMethodOp(function(anchor, head, options) { |
|
setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); |
|
}), |
|
extendSelection: docMethodOp(function(head, other, options) { |
|
extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); |
|
}), |
|
extendSelections: docMethodOp(function(heads, options) { |
|
extendSelections(this, clipPosArray(this, heads, options)); |
|
}), |
|
extendSelectionsBy: docMethodOp(function(f, options) { |
|
extendSelections(this, map(this.sel.ranges, f), options); |
|
}), |
|
setSelections: docMethodOp(function(ranges, primary, options) { |
|
if (!ranges.length) return; |
|
for (var i = 0, out = []; i < ranges.length; i++) |
|
out[i] = new Range(clipPos(this, ranges[i].anchor), |
|
clipPos(this, ranges[i].head)); |
|
if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); |
|
setSelection(this, normalizeSelection(out, primary), options); |
|
}), |
|
addSelection: docMethodOp(function(anchor, head, options) { |
|
var ranges = this.sel.ranges.slice(0); |
|
ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); |
|
setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); |
|
}), |
|
|
|
getSelection: function(lineSep) { |
|
var ranges = this.sel.ranges, lines; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var sel = getBetween(this, ranges[i].from(), ranges[i].to()); |
|
lines = lines ? lines.concat(sel) : sel; |
|
} |
|
if (lineSep === false) return lines; |
|
else return lines.join(lineSep || "\n"); |
|
}, |
|
getSelections: function(lineSep) { |
|
var parts = [], ranges = this.sel.ranges; |
|
for (var i = 0; i < ranges.length; i++) { |
|
var sel = getBetween(this, ranges[i].from(), ranges[i].to()); |
|
if (lineSep !== false) sel = sel.join(lineSep || "\n"); |
|
parts[i] = sel; |
|
} |
|
return parts; |
|
}, |
|
replaceSelection: function(code, collapse, origin) { |
|
var dup = []; |
|
for (var i = 0; i < this.sel.ranges.length; i++) |
|
dup[i] = code; |
|
this.replaceSelections(dup, collapse, origin || "+input"); |
|
}, |
|
replaceSelections: docMethodOp(function(code, collapse, origin) { |
|
var changes = [], sel = this.sel; |
|
for (var i = 0; i < sel.ranges.length; i++) { |
|
var range = sel.ranges[i]; |
|
changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; |
|
} |
|
var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); |
|
for (var i = changes.length - 1; i >= 0; i--) |
|
makeChange(this, changes[i]); |
|
if (newSel) setSelectionReplaceHistory(this, newSel); |
|
else if (this.cm) ensureCursorVisible(this.cm); |
|
}), |
|
undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), |
|
redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), |
|
undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), |
|
redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), |
|
|
|
setExtending: function(val) {this.extend = val;}, |
|
getExtending: function() {return this.extend;}, |
|
|
|
historySize: function() { |
|
var hist = this.history, done = 0, undone = 0; |
|
for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; |
|
for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; |
|
return {undo: done, redo: undone}; |
|
}, |
|
clearHistory: function() {this.history = new History(this.history.maxGeneration);}, |
|
|
|
markClean: function() { |
|
this.cleanGeneration = this.changeGeneration(true); |
|
}, |
|
changeGeneration: function(forceSplit) { |
|
if (forceSplit) |
|
this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; |
|
return this.history.generation; |
|
}, |
|
isClean: function (gen) { |
|
return this.history.generation == (gen || this.cleanGeneration); |
|
}, |
|
|
|
getHistory: function() { |
|
return {done: copyHistoryArray(this.history.done), |
|
undone: copyHistoryArray(this.history.undone)}; |
|
}, |
|
setHistory: function(histData) { |
|
var hist = this.history = new History(this.history.maxGeneration); |
|
hist.done = copyHistoryArray(histData.done.slice(0), null, true); |
|
hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); |
|
}, |
|
|
|
addLineClass: docMethodOp(function(handle, where, cls) { |
|
return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { |
|
var prop = where == "text" ? "textClass" |
|
: where == "background" ? "bgClass" |
|
: where == "gutter" ? "gutterClass" : "wrapClass"; |
|
if (!line[prop]) line[prop] = cls; |
|
else if (classTest(cls).test(line[prop])) return false; |
|
else line[prop] += " " + cls; |
|
return true; |
|
}); |
|
}), |
|
removeLineClass: docMethodOp(function(handle, where, cls) { |
|
return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { |
|
var prop = where == "text" ? "textClass" |
|
: where == "background" ? "bgClass" |
|
: where == "gutter" ? "gutterClass" : "wrapClass"; |
|
var cur = line[prop]; |
|
if (!cur) return false; |
|
else if (cls == null) line[prop] = null; |
|
else { |
|
var found = cur.match(classTest(cls)); |
|
if (!found) return false; |
|
var end = found.index + found[0].length; |
|
line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; |
|
} |
|
return true; |
|
}); |
|
}), |
|
|
|
addLineWidget: docMethodOp(function(handle, node, options) { |
|
return addLineWidget(this, handle, node, options); |
|
}), |
|
removeLineWidget: function(widget) { widget.clear(); }, |
|
|
|
markText: function(from, to, options) { |
|
return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); |
|
}, |
|
setBookmark: function(pos, options) { |
|
var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), |
|
insertLeft: options && options.insertLeft, |
|
clearWhenEmpty: false, shared: options && options.shared, |
|
handleMouseEvents: options && options.handleMouseEvents}; |
|
pos = clipPos(this, pos); |
|
return markText(this, pos, pos, realOpts, "bookmark"); |
|
}, |
|
findMarksAt: function(pos) { |
|
pos = clipPos(this, pos); |
|
var markers = [], spans = getLine(this, pos.line).markedSpans; |
|
if (spans) for (var i = 0; i < spans.length; ++i) { |
|
var span = spans[i]; |
|
if ((span.from == null || span.from <= pos.ch) && |
|
(span.to == null || span.to >= pos.ch)) |
|
markers.push(span.marker.parent || span.marker); |
|
} |
|
return markers; |
|
}, |
|
findMarks: function(from, to, filter) { |
|
from = clipPos(this, from); to = clipPos(this, to); |
|
var found = [], lineNo = from.line; |
|
this.iter(from.line, to.line + 1, function(line) { |
|
var spans = line.markedSpans; |
|
if (spans) for (var i = 0; i < spans.length; i++) { |
|
var span = spans[i]; |
|
if (!(lineNo == from.line && from.ch > span.to || |
|
span.from == null && lineNo != from.line|| |
|
lineNo == to.line && span.from > to.ch) && |
|
(!filter || filter(span.marker))) |
|
found.push(span.marker.parent || span.marker); |
|
} |
|
++lineNo; |
|
}); |
|
return found; |
|
}, |
|
getAllMarks: function() { |
|
var markers = []; |
|
this.iter(function(line) { |
|
var sps = line.markedSpans; |
|
if (sps) for (var i = 0; i < sps.length; ++i) |
|
if (sps[i].from != null) markers.push(sps[i].marker); |
|
}); |
|
return markers; |
|
}, |
|
|
|
posFromIndex: function(off) { |
|
var ch, lineNo = this.first; |
|
this.iter(function(line) { |
|
var sz = line.text.length + 1; |
|
if (sz > off) { ch = off; return true; } |
|
off -= sz; |
|
++lineNo; |
|
}); |
|
return clipPos(this, Pos(lineNo, ch)); |
|
}, |
|
indexFromPos: function (coords) { |
|
coords = clipPos(this, coords); |
|
var index = coords.ch; |
|
if (coords.line < this.first || coords.ch < 0) return 0; |
|
this.iter(this.first, coords.line, function (line) { |
|
index += line.text.length + 1; |
|
}); |
|
return index; |
|
}, |
|
|
|
copy: function(copyHistory) { |
|
var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); |
|
doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; |
|
doc.sel = this.sel; |
|
doc.extend = false; |
|
if (copyHistory) { |
|
doc.history.undoDepth = this.history.undoDepth; |
|
doc.setHistory(this.getHistory()); |
|
} |
|
return doc; |
|
}, |
|
|
|
linkedDoc: function(options) { |
|
if (!options) options = {}; |
|
var from = this.first, to = this.first + this.size; |
|
if (options.from != null && options.from > from) from = options.from; |
|
if (options.to != null && options.to < to) to = options.to; |
|
var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); |
|
if (options.sharedHist) copy.history = this.history; |
|
(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); |
|
copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; |
|
copySharedMarkers(copy, findSharedMarkers(this)); |
|
return copy; |
|
}, |
|
unlinkDoc: function(other) { |
|
if (other instanceof CodeMirror) other = other.doc; |
|
if (this.linked) for (var i = 0; i < this.linked.length; ++i) { |
|
var link = this.linked[i]; |
|
if (link.doc != other) continue; |
|
this.linked.splice(i, 1); |
|
other.unlinkDoc(this); |
|
detachSharedMarkers(findSharedMarkers(this)); |
|
break; |
|
} |
|
// If the histories were shared, split them again |
|
if (other.history == this.history) { |
|
var splitIds = [other.id]; |
|
linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); |
|
other.history = new History(null); |
|
other.history.done = copyHistoryArray(this.history.done, splitIds); |
|
other.history.undone = copyHistoryArray(this.history.undone, splitIds); |
|
} |
|
}, |
|
iterLinkedDocs: function(f) {linkedDocs(this, f);}, |
|
|
|
getMode: function() {return this.mode;}, |
|
getEditor: function() {return this.cm;} |
|
}); |
|
|
|
// Public alias. |
|
Doc.prototype.eachLine = Doc.prototype.iter; |
|
|
|
// Set up methods on CodeMirror's prototype to redirect to the editor's document. |
|
var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); |
|
for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) |
|
CodeMirror.prototype[prop] = (function(method) { |
|
return function() {return method.apply(this.doc, arguments);}; |
|
})(Doc.prototype[prop]); |
|
|
|
eventMixin(Doc); |
|
|
|
// Call f for all linked documents. |
|
function linkedDocs(doc, f, sharedHistOnly) { |
|
function propagate(doc, skip, sharedHist) { |
|
if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { |
|
var rel = doc.linked[i]; |
|
if (rel.doc == skip) continue; |
|
var shared = sharedHist && rel.sharedHist; |
|
if (sharedHistOnly && !shared) continue; |
|
f(rel.doc, shared); |
|
propagate(rel.doc, doc, shared); |
|
} |
|
} |
|
propagate(doc, null, true); |
|
} |
|
|
|
// Attach a document to an editor. |
|
function attachDoc(cm, doc) { |
|
if (doc.cm) throw new Error("This document is already in use."); |
|
cm.doc = doc; |
|
doc.cm = cm; |
|
estimateLineHeights(cm); |
|
loadMode(cm); |
|
if (!cm.options.lineWrapping) findMaxLine(cm); |
|
cm.options.mode = doc.modeOption; |
|
regChange(cm); |
|
} |
|
|
|
// LINE UTILITIES |
|
|
|
// Find the line object corresponding to the given line number. |
|
function getLine(doc, n) { |
|
n -= doc.first; |
|
if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); |
|
for (var chunk = doc; !chunk.lines;) { |
|
for (var i = 0;; ++i) { |
|
var child = chunk.children[i], sz = child.chunkSize(); |
|
if (n < sz) { chunk = child; break; } |
|
n -= sz; |
|
} |
|
} |
|
return chunk.lines[n]; |
|
} |
|
|
|
// Get the part of a document between two positions, as an array of |
|
// strings. |
|
function getBetween(doc, start, end) { |
|
var out = [], n = start.line; |
|
doc.iter(start.line, end.line + 1, function(line) { |
|
var text = line.text; |
|
if (n == end.line) text = text.slice(0, end.ch); |
|
if (n == start.line) text = text.slice(start.ch); |
|
out.push(text); |
|
++n; |
|
}); |
|
return out; |
|
} |
|
// Get the lines between from and to, as array of strings. |
|
function getLines(doc, from, to, lineFilter) { |
|
var out = []; |
|
doc.iter(from, to, function(line) { |
|
var text = line.text; |
|
if(lineFilter){ |
|
text = lineFilter(line); |
|
} |
|
out.push(text); |
|
}); |
|
return out; |
|
} |
|
|
|
// Update the height of a line, propagating the height change |
|
// upwards to parent nodes. |
|
function updateLineHeight(line, height) { |
|
var diff = height - line.height; |
|
if (diff) for (var n = line; n; n = n.parent) n.height += diff; |
|
} |
|
|
|
// Given a line object, find its line number by walking up through |
|
// its parent links. |
|
function lineNo(line) { |
|
if (line.parent == null) return null; |
|
var cur = line.parent, no = indexOf(cur.lines, line); |
|
for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { |
|
for (var i = 0;; ++i) { |
|
if (chunk.children[i] == cur) break; |
|
no += chunk.children[i].chunkSize(); |
|
} |
|
} |
|
return no + cur.first; |
|
} |
|
|
|
// Find the line at the given vertical position, using the height |
|
// information in the document tree. |
|
function lineAtHeight(chunk, h) { |
|
var n = chunk.first; |
|
outer: do { |
|
for (var i = 0; i < chunk.children.length; ++i) { |
|
var child = chunk.children[i], ch = child.height; |
|
if (h < ch) { chunk = child; continue outer; } |
|
h -= ch; |
|
n += child.chunkSize(); |
|
} |
|
return n; |
|
} while (!chunk.lines); |
|
for (var i = 0; i < chunk.lines.length; ++i) { |
|
var line = chunk.lines[i], lh = line.height; |
|
if (h < lh) break; |
|
h -= lh; |
|
} |
|
return n + i; |
|
} |
|
|
|
|
|
// Find the height above the given line. |
|
function heightAtLine(lineObj) { |
|
lineObj = visualLine(lineObj); |
|
|
|
var h = 0, chunk = lineObj.parent; |
|
for (var i = 0; i < chunk.lines.length; ++i) { |
|
var line = chunk.lines[i]; |
|
if (line == lineObj) break; |
|
else h += line.height; |
|
} |
|
for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { |
|
for (var i = 0; i < p.children.length; ++i) { |
|
var cur = p.children[i]; |
|
if (cur == chunk) break; |
|
else h += cur.height; |
|
} |
|
} |
|
return h; |
|
} |
|
|
|
// Get the bidi ordering for the given line (and cache it). Returns |
|
// false for lines that are fully left-to-right, and an array of |
|
// BidiSpan objects otherwise. |
|
function getOrder(line) { |
|
var order = line.order; |
|
if (order == null) order = line.order = bidiOrdering(line.text); |
|
return order; |
|
} |
|
|
|
// HISTORY |
|
|
|
function History(startGen) { |
|
// Arrays of change events and selections. Doing something adds an |
|
// event to done and clears undo. Undoing moves events from done |
|
// to undone, redoing moves them in the other direction. |
|
this.done = []; this.undone = []; |
|
this.undoDepth = Infinity; |
|
// Used to track when changes can be merged into a single undo |
|
// event |
|
this.lastModTime = this.lastSelTime = 0; |
|
this.lastOp = this.lastSelOp = null; |
|
this.lastOrigin = this.lastSelOrigin = null; |
|
// Used by the isClean() method |
|
this.generation = this.maxGeneration = startGen || 1; |
|
} |
|
|
|
// Create a history change event from an updateDoc-style change |
|
// object. |
|
function historyChangeFromChange(doc, change) { |
|
var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; |
|
attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); |
|
linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); |
|
return histChange; |
|
} |
|
|
|
// Pop all selection events off the end of a history array. Stop at |
|
// a change event. |
|
function clearSelectionEvents(array) { |
|
while (array.length) { |
|
var last = lst(array); |
|
if (last.ranges) array.pop(); |
|
else break; |
|
} |
|
} |
|
|
|
// Find the top change event in the history. Pop off selection |
|
// events that are in the way. |
|
function lastChangeEvent(hist, force) { |
|
if (force) { |
|
clearSelectionEvents(hist.done); |
|
return lst(hist.done); |
|
} else if (hist.done.length && !lst(hist.done).ranges) { |
|
return lst(hist.done); |
|
} else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { |
|
hist.done.pop(); |
|
return lst(hist.done); |
|
} |
|
} |
|
|
|
// Register a change in the history. Merges changes that are within |
|
// a single operation, ore are close together with an origin that |
|
// allows merging (starting with "+") into a single event. |
|
function addChangeToHistory(doc, change, selAfter, opId) { |
|
var hist = doc.history; |
|
hist.undone.length = 0; |
|
var time = +new Date, cur; |
|
|
|
if ((hist.lastOp == opId || |
|
hist.lastOrigin == change.origin && change.origin && |
|
((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || |
|
change.origin.charAt(0) == "*")) && |
|
(cur = lastChangeEvent(hist, hist.lastOp == opId))) { |
|
// Merge this change into the last event |
|
var last = lst(cur.changes); |
|
if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { |
|
// Optimized case for simple insertion -- don't want to add |
|
// new changesets for every character typed |
|
last.to = changeEnd(change); |
|
} else { |
|
// Add new sub-event |
|
cur.changes.push(historyChangeFromChange(doc, change)); |
|
} |
|
} else { |
|
// Can not be merged, start a new event. |
|
var before = lst(hist.done); |
|
if (!before || !before.ranges) |
|
pushSelectionToHistory(doc.sel, hist.done); |
|
cur = {changes: [historyChangeFromChange(doc, change)], |
|
generation: hist.generation}; |
|
hist.done.push(cur); |
|
while (hist.done.length > hist.undoDepth) { |
|
hist.done.shift(); |
|
if (!hist.done[0].ranges) hist.done.shift(); |
|
} |
|
} |
|
hist.done.push(selAfter); |
|
hist.generation = ++hist.maxGeneration; |
|
hist.lastModTime = hist.lastSelTime = time; |
|
hist.lastOp = hist.lastSelOp = opId; |
|
hist.lastOrigin = hist.lastSelOrigin = change.origin; |
|
|
|
if (!last) signal(doc, "historyAdded"); |
|
} |
|
|
|
function selectionEventCanBeMerged(doc, origin, prev, sel) { |
|
var ch = origin.charAt(0); |
|
return ch == "*" || |
|
ch == "+" && |
|
prev.ranges.length == sel.ranges.length && |
|
prev.somethingSelected() == sel.somethingSelected() && |
|
new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); |
|
} |
|
|
|
// Called whenever the selection changes, sets the new selection as |
|
// the pending selection in the history, and pushes the old pending |
|
// selection into the 'done' array when it was significantly |
|
// different (in number of selected ranges, emptiness, or time). |
|
function addSelectionToHistory(doc, sel, opId, options) { |
|
var hist = doc.history, origin = options && options.origin; |
|
|
|
// A new event is started when the previous origin does not match |
|
// the current, or the origins don't allow matching. Origins |
|
// starting with * are always merged, those starting with + are |
|
// merged when similar and close together in time. |
|
if (opId == hist.lastSelOp || |
|
(origin && hist.lastSelOrigin == origin && |
|
(hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || |
|
selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) |
|
hist.done[hist.done.length - 1] = sel; |
|
else |
|
pushSelectionToHistory(sel, hist.done); |
|
|
|
hist.lastSelTime = +new Date; |
|
hist.lastSelOrigin = origin; |
|
hist.lastSelOp = opId; |
|
if (options && options.clearRedo !== false) |
|
clearSelectionEvents(hist.undone); |
|
} |
|
|
|
function pushSelectionToHistory(sel, dest) { |
|
var top = lst(dest); |
|
if (!(top && top.ranges && top.equals(sel))) |
|
dest.push(sel); |
|
} |
|
|
|
// Used to store marked span information in the history. |
|
function attachLocalSpans(doc, change, from, to) { |
|
var existing = change["spans_" + doc.id], n = 0; |
|
doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { |
|
if (line.markedSpans) |
|
(existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; |
|
++n; |
|
}); |
|
} |
|
|
|
// When un/re-doing restores text containing marked spans, those |
|
// that have been explicitly cleared should not be restored. |
|
function removeClearedSpans(spans) { |
|
if (!spans) return null; |
|
for (var i = 0, out; i < spans.length; ++i) { |
|
if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } |
|
else if (out) out.push(spans[i]); |
|
} |
|
return !out ? spans : out.length ? out : null; |
|
} |
|
|
|
// Retrieve and filter the old marked spans stored in a change event. |
|
function getOldSpans(doc, change) { |
|
var found = change["spans_" + doc.id]; |
|
if (!found) return null; |
|
for (var i = 0, nw = []; i < change.text.length; ++i) |
|
nw.push(removeClearedSpans(found[i])); |
|
return nw; |
|
} |
|
|
|
// Used both to provide a JSON-safe object in .getHistory, and, when |
|
// detaching a document, to split the history in two |
|
function copyHistoryArray(events, newGroup, instantiateSel) { |
|
for (var i = 0, copy = []; i < events.length; ++i) { |
|
var event = events[i]; |
|
if (event.ranges) { |
|
copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); |
|
continue; |
|
} |
|
var changes = event.changes, newChanges = []; |
|
copy.push({changes: newChanges}); |
|
for (var j = 0; j < changes.length; ++j) { |
|
var change = changes[j], m; |
|
newChanges.push({from: change.from, to: change.to, text: change.text}); |
|
if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { |
|
if (indexOf(newGroup, Number(m[1])) > -1) { |
|
lst(newChanges)[prop] = change[prop]; |
|
delete change[prop]; |
|
} |
|
} |
|
} |
|
} |
|
return copy; |
|
} |
|
|
|
// Rebasing/resetting history to deal with externally-sourced changes |
|
|
|
function rebaseHistSelSingle(pos, from, to, diff) { |
|
if (to < pos.line) { |
|
pos.line += diff; |
|
} else if (from < pos.line) { |
|
pos.line = from; |
|
pos.ch = 0; |
|
} |
|
} |
|
|
|
// Tries to rebase an array of history events given a change in the |
|
// document. If the change touches the same lines as the event, the |
|
// event, and everything 'behind' it, is discarded. If the change is |
|
// before the event, the event's positions are updated. Uses a |
|
// copy-on-write scheme for the positions, to avoid having to |
|
// reallocate them all on every rebase, but also avoid problems with |
|
// shared position objects being unsafely updated. |
|
function rebaseHistArray(array, from, to, diff) { |
|
for (var i = 0; i < array.length; ++i) { |
|
var sub = array[i], ok = true; |
|
if (sub.ranges) { |
|
if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } |
|
for (var j = 0; j < sub.ranges.length; j++) { |
|
rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); |
|
rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); |
|
} |
|
continue; |
|
} |
|
for (var j = 0; j < sub.changes.length; ++j) { |
|
var cur = sub.changes[j]; |
|
if (to < cur.from.line) { |
|
cur.from = Pos(cur.from.line + diff, cur.from.ch); |
|
cur.to = Pos(cur.to.line + diff, cur.to.ch); |
|
} else if (from <= cur.to.line) { |
|
ok = false; |
|
break; |
|
} |
|
} |
|
if (!ok) { |
|
array.splice(0, i + 1); |
|
i = 0; |
|
} |
|
} |
|
} |
|
|
|
function rebaseHist(hist, change) { |
|
var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; |
|
rebaseHistArray(hist.done, from, to, diff); |
|
rebaseHistArray(hist.undone, from, to, diff); |
|
} |
|
|
|
// EVENT UTILITIES |
|
|
|
// Due to the fact that we still support jurassic IE versions, some |
|
// compatibility wrappers are needed. |
|
|
|
var e_preventDefault = CodeMirror.e_preventDefault = function(e) { |
|
if (e.preventDefault) e.preventDefault(); |
|
else e.returnValue = false; |
|
}; |
|
var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { |
|
if (e.stopPropagation) e.stopPropagation(); |
|
else e.cancelBubble = true; |
|
}; |
|
function e_defaultPrevented(e) { |
|
return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; |
|
} |
|
var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; |
|
|
|
function e_target(e) {return e.target || e.srcElement;} |
|
function e_button(e) { |
|
var b = e.which; |
|
if (b == null) { |
|
if (e.button & 1) b = 1; |
|
else if (e.button & 2) b = 3; |
|
else if (e.button & 4) b = 2; |
|
} |
|
if (mac && e.ctrlKey && b == 1) b = 3; |
|
return b; |
|
} |
|
|
|
// EVENT HANDLING |
|
|
|
// Lightweight event framework. on/off also work on DOM nodes, |
|
// registering native DOM handlers. |
|
|
|
var on = CodeMirror.on = function(emitter, type, f) { |
|
if (emitter.addEventListener) |
|
emitter.addEventListener(type, f, false); |
|
else if (emitter.attachEvent) |
|
emitter.attachEvent("on" + type, f); |
|
else { |
|
var map = emitter._handlers || (emitter._handlers = {}); |
|
var arr = map[type] || (map[type] = []); |
|
arr.push(f); |
|
} |
|
}; |
|
|
|
var off = CodeMirror.off = function(emitter, type, f) { |
|
if (emitter.removeEventListener) |
|
emitter.removeEventListener(type, f, false); |
|
else if (emitter.detachEvent) |
|
emitter.detachEvent("on" + type, f); |
|
else { |
|
var arr = emitter._handlers && emitter._handlers[type]; |
|
if (!arr) return; |
|
for (var i = 0; i < arr.length; ++i) |
|
if (arr[i] == f) { arr.splice(i, 1); break; } |
|
} |
|
}; |
|
|
|
var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { |
|
var arr = emitter._handlers && emitter._handlers[type]; |
|
if (!arr) return; |
|
var args = Array.prototype.slice.call(arguments, 2); |
|
for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); |
|
}; |
|
|
|
var orphanDelayedCallbacks = null; |
|
|
|
// Often, we want to signal events at a point where we are in the |
|
// middle of some work, but don't want the handler to start calling |
|
// other methods on the editor, which might be in an inconsistent |
|
// state or simply not expect any other events to happen. |
|
// signalLater looks whether there are any handlers, and schedules |
|
// them to be executed when the last operation ends, or, if no |
|
// operation is active, when a timeout fires. |
|
function signalLater(emitter, type /*, values...*/) { |
|
var arr = emitter._handlers && emitter._handlers[type]; |
|
if (!arr) return; |
|
var args = Array.prototype.slice.call(arguments, 2), list; |
|
if (operationGroup) { |
|
list = operationGroup.delayedCallbacks; |
|
} else if (orphanDelayedCallbacks) { |
|
list = orphanDelayedCallbacks; |
|
} else { |
|
list = orphanDelayedCallbacks = []; |
|
setTimeout(fireOrphanDelayed, 0); |
|
} |
|
function bnd(f) {return function(){f.apply(null, args);};}; |
|
for (var i = 0; i < arr.length; ++i) |
|
list.push(bnd(arr[i])); |
|
} |
|
|
|
function fireOrphanDelayed() { |
|
var delayed = orphanDelayedCallbacks; |
|
orphanDelayedCallbacks = null; |
|
for (var i = 0; i < delayed.length; ++i) delayed[i](); |
|
} |
|
|
|
// The DOM events that CodeMirror handles can be overridden by |
|
// registering a (non-DOM) handler on the editor for the event name, |
|
// and preventDefault-ing the event in that handler. |
|
function signalDOMEvent(cm, e, override) { |
|
if (typeof e == "string") |
|
e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; |
|
signal(cm, override || e.type, cm, e); |
|
return e_defaultPrevented(e) || e.codemirrorIgnore; |
|
} |
|
|
|
function signalCursorActivity(cm) { |
|
var arr = cm._handlers && cm._handlers.cursorActivity; |
|
if (!arr) return; |
|
var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); |
|
for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) |
|
set.push(arr[i]); |
|
} |
|
|
|
function hasHandler(emitter, type) { |
|
var arr = emitter._handlers && emitter._handlers[type]; |
|
return arr && arr.length > 0; |
|
} |
|
|
|
// Add on and off methods to a constructor's prototype, to make |
|
// registering events on such objects more convenient. |
|
function eventMixin(ctor) { |
|
ctor.prototype.on = function(type, f) {on(this, type, f);}; |
|
ctor.prototype.off = function(type, f) {off(this, type, f);}; |
|
} |
|
|
|
// MISC UTILITIES |
|
|
|
// Number of pixels added to scroller and sizer to hide scrollbar |
|
var scrollerGap = 30; |
|
|
|
// Returned or thrown by various protocols to signal 'I'm not |
|
// handling this'. |
|
var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; |
|
|
|
// Reused option objects for setSelection & friends |
|
var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; |
|
|
|
function Delayed() {this.id = null;} |
|
Delayed.prototype.set = function(ms, f) { |
|
clearTimeout(this.id); |
|
this.id = setTimeout(f, ms); |
|
}; |
|
|
|
// Counts the column offset in a string, taking tabs into account. |
|
// Used mostly to find indentation. |
|
var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { |
|
if (end == null) { |
|
end = string.search(/[^\s\u00a0]/); |
|
if (end == -1) end = string.length; |
|
} |
|
for (var i = startIndex || 0, n = startValue || 0;;) { |
|
var nextTab = string.indexOf("\t", i); |
|
if (nextTab < 0 || nextTab >= end) |
|
return n + (end - i); |
|
n += nextTab - i; |
|
n += tabSize - (n % tabSize); |
|
i = nextTab + 1; |
|
} |
|
}; |
|
|
|
// The inverse of countColumn -- find the offset that corresponds to |
|
// a particular column. |
|
function findColumn(string, goal, tabSize) { |
|
for (var pos = 0, col = 0;;) { |
|
var nextTab = string.indexOf("\t", pos); |
|
if (nextTab == -1) nextTab = string.length; |
|
var skipped = nextTab - pos; |
|
if (nextTab == string.length || col + skipped >= goal) |
|
return pos + Math.min(skipped, goal - col); |
|
col += nextTab - pos; |
|
col += tabSize - (col % tabSize); |
|
pos = nextTab + 1; |
|
if (col >= goal) return pos; |
|
} |
|
} |
|
|
|
var spaceStrs = [""]; |
|
function spaceStr(n) { |
|
while (spaceStrs.length <= n) |
|
spaceStrs.push(lst(spaceStrs) + " "); |
|
return spaceStrs[n]; |
|
} |
|
|
|
function lst(arr) { return arr[arr.length-1]; } |
|
|
|
var selectInput = function(node) { node.select(); }; |
|
if (ios) // Mobile Safari apparently has a bug where select() is broken. |
|
selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; |
|
else if (ie) // Suppress mysterious IE10 errors |
|
selectInput = function(node) { try { node.select(); } catch(_e) {} }; |
|
|
|
function indexOf(array, elt) { |
|
for (var i = 0; i < array.length; ++i) |
|
if (array[i] == elt) return i; |
|
return -1; |
|
} |
|
function map(array, f) { |
|
var out = []; |
|
for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); |
|
return out; |
|
} |
|
|
|
function nothing() {} |
|
|
|
function createObj(base, props) { |
|
var inst; |
|
if (Object.create) { |
|
inst = Object.create(base); |
|
} else { |
|
nothing.prototype = base; |
|
inst = new nothing(); |
|
} |
|
if (props) copyObj(props, inst); |
|
return inst; |
|
}; |
|
|
|
function copyObj(obj, target, overwrite) { |
|
if (!target) target = {}; |
|
for (var prop in obj) |
|
if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) |
|
target[prop] = obj[prop]; |
|
return target; |
|
} |
|
|
|
function bind(f) { |
|
var args = Array.prototype.slice.call(arguments, 1); |
|
return function(){return f.apply(null, args);}; |
|
} |
|
|
|
var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; |
|
var isWordCharBasic = CodeMirror.isWordChar = function(ch) { |
|
return /\w/.test(ch) || ch > "\x80" && |
|
(ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); |
|
}; |
|
function isWordChar(ch, helper) { |
|
if (!helper) return isWordCharBasic(ch); |
|
if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; |
|
return helper.test(ch); |
|
} |
|
|
|
function isEmpty(obj) { |
|
for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; |
|
return true; |
|
} |
|
|
|
// Extending unicode characters. A series of a non-extending char + |
|
// any number of extending chars is treated as a single unit as far |
|
// as editing and measuring is concerned. This is not fully correct, |
|
// since some scripts/fonts/browsers also treat other configurations |
|
// of code points as a group. |
|
var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; |
|
function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } |
|
|
|
// DOM UTILITIES |
|
|
|
function elt(tag, content, className, style) { |
|
var e = document.createElement(tag); |
|
if (className) e.className = className; |
|
if (style) e.style.cssText = style; |
|
if (typeof content == "string") e.appendChild(document.createTextNode(content)); |
|
else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); |
|
return e; |
|
} |
|
|
|
var range; |
|
if (document.createRange) range = function(node, start, end, endNode) { |
|
var r = document.createRange(); |
|
r.setEnd(endNode || node, end); |
|
r.setStart(node, start); |
|
return r; |
|
}; |
|
else range = function(node, start, end) { |
|
var r = document.body.createTextRange(); |
|
try { r.moveToElementText(node.parentNode); } |
|
catch(e) { return r; } |
|
r.collapse(true); |
|
r.moveEnd("character", end); |
|
r.moveStart("character", start); |
|
return r; |
|
}; |
|
|
|
function removeChildren(e) { |
|
for (var count = e.childNodes.length; count > 0; --count) |
|
e.removeChild(e.firstChild); |
|
return e; |
|
} |
|
|
|
function removeChildrenAndAdd(parent, e) { |
|
return removeChildren(parent).appendChild(e); |
|
} |
|
|
|
var contains = CodeMirror.contains = function(parent, child) { |
|
if (child.nodeType == 3) // Android browser always returns false when child is a textnode |
|
child = child.parentNode; |
|
if (parent.contains) |
|
return parent.contains(child); |
|
do { |
|
if (child.nodeType == 11) child = child.host; |
|
if (child == parent) return true; |
|
} while (child = child.parentNode); |
|
}; |
|
|
|
function activeElt() { return document.activeElement; } |
|
// Older versions of IE throws unspecified error when touching |
|
// document.activeElement in some cases (during loading, in iframe) |
|
if (ie && ie_version < 11) activeElt = function() { |
|
try { return document.activeElement; } |
|
catch(e) { return document.body; } |
|
}; |
|
|
|
function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); } |
|
var rmClass = CodeMirror.rmClass = function(node, cls) { |
|
var current = node.className; |
|
var match = classTest(cls).exec(current); |
|
if (match) { |
|
var after = current.slice(match.index + match[0].length); |
|
node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); |
|
} |
|
}; |
|
var addClass = CodeMirror.addClass = function(node, cls) { |
|
var current = node.className; |
|
if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls; |
|
}; |
|
function joinClasses(a, b) { |
|
var as = a.split(" "); |
|
for (var i = 0; i < as.length; i++) |
|
if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; |
|
return b; |
|
} |
|
|
|
// WINDOW-WIDE EVENTS |
|
|
|
// These must be handled carefully, because naively registering a |
|
// handler for each editor will cause the editors to never be |
|
// garbage collected. |
|
|
|
function forEachCodeMirror(f) { |
|
if (!document.body.getElementsByClassName) return; |
|
var byClass = document.body.getElementsByClassName("CodeMirror"); |
|
for (var i = 0; i < byClass.length; i++) { |
|
var cm = byClass[i].CodeMirror; |
|
if (cm) f(cm); |
|
} |
|
} |
|
|
|
var globalsRegistered = false; |
|
function ensureGlobalHandlers() { |
|
if (globalsRegistered) return; |
|
registerGlobalHandlers(); |
|
globalsRegistered = true; |
|
} |
|
function registerGlobalHandlers() { |
|
// When the window resizes, we need to refresh active editors. |
|
var resizeTimer; |
|
on(window, "resize", function() { |
|
if (resizeTimer == null) resizeTimer = setTimeout(function() { |
|
resizeTimer = null; |
|
forEachCodeMirror(onResize); |
|
}, 100); |
|
}); |
|
// When the window loses focus, we want to show the editor as blurred |
|
on(window, "blur", function() { |
|
forEachCodeMirror(onBlur); |
|
}); |
|
} |
|
|
|
// FEATURE DETECTION |
|
|
|
// Detect drag-and-drop |
|
var dragAndDrop = function() { |
|
// There is *some* kind of drag-and-drop support in IE6-8, but I |
|
// couldn't get it to work yet. |
|
if (ie && ie_version < 9) return false; |
|
var div = elt('div'); |
|
return "draggable" in div || "dragDrop" in div; |
|
}(); |
|
|
|
var zwspSupported; |
|
function zeroWidthElement(measure) { |
|
if (zwspSupported == null) { |
|
var test = elt("span", "\u200b"); |
|
removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); |
|
if (measure.firstChild.offsetHeight != 0) |
|
zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); |
|
} |
|
var node = zwspSupported ? elt("span", "\u200b") : |
|
elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); |
|
node.setAttribute("cm-text", ""); |
|
return node; |
|
} |
|
|
|
// Feature-detect IE's crummy client rect reporting for bidi text |
|
var badBidiRects; |
|
function hasBadBidiRects(measure) { |
|
if (badBidiRects != null) return badBidiRects; |
|
var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); |
|
var r0 = range(txt, 0, 1).getBoundingClientRect(); |
|
if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) |
|
var r1 = range(txt, 1, 2).getBoundingClientRect(); |
|
return badBidiRects = (r1.right - r0.right < 3); |
|
} |
|
|
|
// See if "".split is the broken IE version, if so, provide an |
|
// alternative way to split lines. |
|
var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { |
|
var pos = 0, result = [], l = string.length; |
|
while (pos <= l) { |
|
var nl = string.indexOf("\n", pos); |
|
if (nl == -1) nl = string.length; |
|
var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); |
|
var rt = line.indexOf("\r"); |
|
if (rt != -1) { |
|
result.push(line.slice(0, rt)); |
|
pos += rt + 1; |
|
} else { |
|
result.push(line); |
|
pos = nl + 1; |
|
} |
|
} |
|
return result; |
|
} : function(string){return string.split(/\r\n?|\n/);}; |
|
|
|
var hasSelection = window.getSelection ? function(te) { |
|
try { return te.selectionStart != te.selectionEnd; } |
|
catch(e) { return false; } |
|
} : function(te) { |
|
try {var range = te.ownerDocument.selection.createRange();} |
|
catch(e) {} |
|
if (!range || range.parentElement() != te) return false; |
|
return range.compareEndPoints("StartToEnd", range) != 0; |
|
}; |
|
|
|
var hasCopyEvent = (function() { |
|
var e = elt("div"); |
|
if ("oncopy" in e) return true; |
|
e.setAttribute("oncopy", "return;"); |
|
return typeof e.oncopy == "function"; |
|
})(); |
|
|
|
var badZoomedRects = null; |
|
function hasBadZoomedRects(measure) { |
|
if (badZoomedRects != null) return badZoomedRects; |
|
var node = removeChildrenAndAdd(measure, elt("span", "x")); |
|
var normal = node.getBoundingClientRect(); |
|
var fromRange = range(node, 0, 1).getBoundingClientRect(); |
|
return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; |
|
} |
|
|
|
// KEY NAMES |
|
|
|
var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", |
|
19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", |
|
36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", |
|
46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", |
|
173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", |
|
221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", |
|
63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; |
|
CodeMirror.keyNames = keyNames; |
|
(function() { |
|
// Number keys |
|
for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); |
|
// Alphabetic keys |
|
for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); |
|
// Function keys |
|
for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; |
|
})(); |
|
|
|
// BIDI HELPERS |
|
|
|
function iterateBidiSections(order, from, to, f) { |
|
if (!order) return f(from, to, "ltr"); |
|
var found = false; |
|
for (var i = 0; i < order.length; ++i) { |
|
var part = order[i]; |
|
if (part.from < to && part.to > from || from == to && part.to == from) { |
|
f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); |
|
found = true; |
|
} |
|
} |
|
if (!found) f(from, to, "ltr"); |
|
} |
|
|
|
function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } |
|
function bidiRight(part) { return part.level % 2 ? part.from : part.to; } |
|
|
|
function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } |
|
function lineRight(line) { |
|
var order = getOrder(line); |
|
if (!order) return line.text.length; |
|
return bidiRight(lst(order)); |
|
} |
|
|
|
function lineStart(cm, lineN) { |
|
var line = getLine(cm.doc, lineN); |
|
var visual = visualLine(line); |
|
if (visual != line) lineN = lineNo(visual); |
|
var order = getOrder(visual); |
|
var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); |
|
return Pos(lineN, ch); |
|
} |
|
function lineEnd(cm, lineN) { |
|
var merged, line = getLine(cm.doc, lineN); |
|
while (merged = collapsedSpanAtEnd(line)) { |
|
line = merged.find(1, true).line; |
|
lineN = null; |
|
} |
|
var order = getOrder(line); |
|
var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); |
|
return Pos(lineN == null ? lineNo(line) : lineN, ch); |
|
} |
|
function lineStartSmart(cm, pos) { |
|
var start = lineStart(cm, pos.line); |
|
var line = getLine(cm.doc, start.line); |
|
var order = getOrder(line); |
|
if (!order || order[0].level == 0) { |
|
var firstNonWS = Math.max(0, line.text.search(/\S/)); |
|
var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; |
|
return Pos(start.line, inWS ? 0 : firstNonWS); |
|
} |
|
return start; |
|
} |
|
|
|
function compareBidiLevel(order, a, b) { |
|
var linedir = order[0].level; |
|
if (a == linedir) return true; |
|
if (b == linedir) return false; |
|
return a < b; |
|
} |
|
var bidiOther; |
|
function getBidiPartAt(order, pos) { |
|
bidiOther = null; |
|
for (var i = 0, found; i < order.length; ++i) { |
|
var cur = order[i]; |
|
if (cur.from < pos && cur.to > pos) return i; |
|
if ((cur.from == pos || cur.to == pos)) { |
|
if (found == null) { |
|
found = i; |
|
} else if (compareBidiLevel(order, cur.level, order[found].level)) { |
|
if (cur.from != cur.to) bidiOther = found; |
|
return i; |
|
} else { |
|
if (cur.from != cur.to) bidiOther = i; |
|
return found; |
|
} |
|
} |
|
} |
|
return found; |
|
} |
|
|
|
function moveInLine(line, pos, dir, byUnit) { |
|
if (!byUnit) return pos + dir; |
|
do pos += dir; |
|
while (pos > 0 && isExtendingChar(line.text.charAt(pos))); |
|
return pos; |
|
} |
|
|
|
// This is needed in order to move 'visually' through bi-directional |
|
// text -- i.e., pressing left should make the cursor go left, even |
|
// when in RTL text. The tricky part is the 'jumps', where RTL and |
|
// LTR text touch each other. This often requires the cursor offset |
|
// to move more than one unit, in order to visually move one unit. |
|
function moveVisually(line, start, dir, byUnit) { |
|
var bidi = getOrder(line); |
|
if (!bidi) return moveLogically(line, start, dir, byUnit); |
|
var pos = getBidiPartAt(bidi, start), part = bidi[pos]; |
|
var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); |
|
|
|
for (;;) { |
|
if (target > part.from && target < part.to) return target; |
|
if (target == part.from || target == part.to) { |
|
if (getBidiPartAt(bidi, target) == pos) return target; |
|
part = bidi[pos += dir]; |
|
return (dir > 0) == part.level % 2 ? part.to : part.from; |
|
} else { |
|
part = bidi[pos += dir]; |
|
if (!part) return null; |
|
if ((dir > 0) == part.level % 2) |
|
target = moveInLine(line, part.to, -1, byUnit); |
|
else |
|
target = moveInLine(line, part.from, 1, byUnit); |
|
} |
|
} |
|
} |
|
|
|
function moveLogically(line, start, dir, byUnit) { |
|
var target = start + dir; |
|
if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; |
|
return target < 0 || target > line.text.length ? null : target; |
|
} |
|
|
|
// Bidirectional ordering algorithm |
|
// See http://unicode.org/reports/tr9/tr9-13.html for the algorithm |
|
// that this (partially) implements. |
|
|
|
// One-char codes used for character types: |
|
// L (L): Left-to-Right |
|
// R (R): Right-to-Left |
|
// r (AL): Right-to-Left Arabic |
|
// 1 (EN): European Number |
|
// + (ES): European Number Separator |
|
// % (ET): European Number Terminator |
|
// n (AN): Arabic Number |
|
// , (CS): Common Number Separator |
|
// m (NSM): Non-Spacing Mark |
|
// b (BN): Boundary Neutral |
|
// s (B): Paragraph Separator |
|
// t (S): Segment Separator |
|
// w (WS): Whitespace |
|
// N (ON): Other Neutrals |
|
|
|
// Returns null if characters are ordered as they appear |
|
// (left-to-right), or an array of sections ({from, to, level} |
|
// objects) in the order in which they occur visually. |
|
var bidiOrdering = (function() { |
|
// Character types for codepoints 0 to 0xff |
|
var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; |
|
// Character types for codepoints 0x600 to 0x6ff |
|
var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; |
|
function charType(code) { |
|
if (code <= 0xf7) return lowTypes.charAt(code); |
|
else if (0x590 <= code && code <= 0x5f4) return "R"; |
|
else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); |
|
else if (0x6ee <= code && code <= 0x8ac) return "r"; |
|
else if (0x2000 <= code && code <= 0x200b) return "w"; |
|
else if (code == 0x200c) return "b"; |
|
else return "L"; |
|
} |
|
|
|
var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; |
|
var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; |
|
// Browsers seem to always treat the boundaries of block elements as being L. |
|
var outerType = "L"; |
|
|
|
function BidiSpan(level, from, to) { |
|
this.level = level; |
|
this.from = from; this.to = to; |
|
} |
|
|
|
return function(str) { |
|
if (!bidiRE.test(str)) return false; |
|
var len = str.length, types = []; |
|
for (var i = 0, type; i < len; ++i) |
|
types.push(type = charType(str.charCodeAt(i))); |
|
|
|
// W1. Examine each non-spacing mark (NSM) in the level run, and |
|
// change the type of the NSM to the type of the previous |
|
// character. If the NSM is at the start of the level run, it will |
|
// get the type of sor. |
|
for (var i = 0, prev = outerType; i < len; ++i) { |
|
var type = types[i]; |
|
if (type == "m") types[i] = prev; |
|
else prev = type; |
|
} |
|
|
|
// W2. Search backwards from each instance of a European number |
|
// until the first strong type (R, L, AL, or sor) is found. If an |
|
// AL is found, change the type of the European number to Arabic |
|
// number. |
|
// W3. Change all ALs to R. |
|
for (var i = 0, cur = outerType; i < len; ++i) { |
|
var type = types[i]; |
|
if (type == "1" && cur == "r") types[i] = "n"; |
|
else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } |
|
} |
|
|
|
// W4. A single European separator between two European numbers |
|
// changes to a European number. A single common separator between |
|
// two numbers of the same type changes to that type. |
|
for (var i = 1, prev = types[0]; i < len - 1; ++i) { |
|
var type = types[i]; |
|
if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; |
|
else if (type == "," && prev == types[i+1] && |
|
(prev == "1" || prev == "n")) types[i] = prev; |
|
prev = type; |
|
} |
|
|
|
// W5. A sequence of European terminators adjacent to European |
|
// numbers changes to all European numbers. |
|
// W6. Otherwise, separators and terminators change to Other |
|
// Neutral. |
|
for (var i = 0; i < len; ++i) { |
|
var type = types[i]; |
|
if (type == ",") types[i] = "N"; |
|
else if (type == "%") { |
|
for (var end = i + 1; end < len && types[end] == "%"; ++end) {} |
|
var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; |
|
for (var j = i; j < end; ++j) types[j] = replace; |
|
i = end - 1; |
|
} |
|
} |
|
|
|
// W7. Search backwards from each instance of a European number |
|
// until the first strong type (R, L, or sor) is found. If an L is |
|
// found, then change the type of the European number to L. |
|
for (var i = 0, cur = outerType; i < len; ++i) { |
|
var type = types[i]; |
|
if (cur == "L" && type == "1") types[i] = "L"; |
|
else if (isStrong.test(type)) cur = type; |
|
} |
|
|
|
// N1. A sequence of neutrals takes the direction of the |
|
// surrounding strong text if the text on both sides has the same |
|
// direction. European and Arabic numbers act as if they were R in |
|
// terms of their influence on neutrals. Start-of-level-run (sor) |
|
// and end-of-level-run (eor) are used at level run boundaries. |
|
// N2. Any remaining neutrals take the embedding direction. |
|
for (var i = 0; i < len; ++i) { |
|
if (isNeutral.test(types[i])) { |
|
for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} |
|
var before = (i ? types[i-1] : outerType) == "L"; |
|
var after = (end < len ? types[end] : outerType) == "L"; |
|
var replace = before || after ? "L" : "R"; |
|
for (var j = i; j < end; ++j) types[j] = replace; |
|
i = end - 1; |
|
} |
|
} |
|
|
|
// Here we depart from the documented algorithm, in order to avoid |
|
// building up an actual levels array. Since there are only three |
|
// levels (0, 1, 2) in an implementation that doesn't take |
|
// explicit embedding into account, we can build up the order on |
|
// the fly, without following the level-based algorithm. |
|
var order = [], m; |
|
for (var i = 0; i < len;) { |
|
if (countsAsLeft.test(types[i])) { |
|
var start = i; |
|
for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} |
|
order.push(new BidiSpan(0, start, i)); |
|
} else { |
|
var pos = i, at = order.length; |
|
for (++i; i < len && types[i] != "L"; ++i) {} |
|
for (var j = pos; j < i;) { |
|
if (countsAsNum.test(types[j])) { |
|
if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); |
|
var nstart = j; |
|
for (++j; j < i && countsAsNum.test(types[j]); ++j) {} |
|
order.splice(at, 0, new BidiSpan(2, nstart, j)); |
|
pos = j; |
|
} else ++j; |
|
} |
|
if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); |
|
} |
|
} |
|
if (order[0].level == 1 && (m = str.match(/^\s+/))) { |
|
order[0].from = m[0].length; |
|
order.unshift(new BidiSpan(0, 0, m[0].length)); |
|
} |
|
if (lst(order).level == 1 && (m = str.match(/\s+$/))) { |
|
lst(order).to -= m[0].length; |
|
order.push(new BidiSpan(0, len - m[0].length, len)); |
|
} |
|
if (order[0].level == 2) |
|
order.unshift(new BidiSpan(1, order[0].to, order[0].to)); |
|
if (order[0].level != lst(order).level) |
|
order.push(new BidiSpan(order[0].level, len, len)); |
|
|
|
return order; |
|
}; |
|
})(); |
|
|
|
// THE END |
|
|
|
CodeMirror.version = "5.4.1"; |
|
|
|
return CodeMirror; |
|
});; |
|
(function ($) { |
|
/** |
|
* 公式编辑控件 |
|
* @class BI.FormulaEditor |
|
* @extends BI.Widget |
|
*/ |
|
BI.FormulaEditor = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
return $.extend(BI.FormulaEditor.superclass._defaultConfig.apply(), { |
|
baseCls: 'bi-formula-editor', |
|
watermark: '', |
|
value: '', |
|
fieldTextValueMap: {}, |
|
showHint: true |
|
}); |
|
}, |
|
_init: function () { |
|
BI.FormulaEditor.superclass._init.apply(this, arguments); |
|
var o = this.options, self = this; |
|
this.editor = CodeMirror(this.element[0], { |
|
textWrapping: true, |
|
lineWrapping: true, |
|
lineNumbers: false, |
|
mode: 'formula' |
|
}); |
|
this.editor.on("change", function (cm, change) { |
|
self._checkWaterMark(); |
|
if(o.showHint){ |
|
CodeMirror.showHint(cm, CodeMirror.formulaHint, {completeSingle: false}); |
|
} |
|
BI.nextTick(function () { |
|
self.fireEvent(BI.FormulaEditor.EVENT_CHANGE) |
|
}); |
|
}); |
|
|
|
this.editor.on("focus", function () { |
|
self._checkWaterMark(); |
|
self.fireEvent(BI.FormulaEditor.EVENT_FOCUS); |
|
}); |
|
|
|
this.editor.on("blur", function () { |
|
self.fireEvent(BI.FormulaEditor.EVENT_BLUR); |
|
}); |
|
|
|
|
|
if (BI.isKey(o.value)) { |
|
self.setValue(o.value); |
|
} |
|
|
|
if (BI.isKey(this.options.watermark)) { |
|
var self = this; |
|
this.watermark = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "bi-water-mark", |
|
text: this.options.watermark, |
|
whiteSpace: "nowrap", |
|
textAlign: "left" |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: self, |
|
items: [{ |
|
el: self.watermark, |
|
left: 0, |
|
top: 0 |
|
}] |
|
}); |
|
|
|
this.watermark.element.bind( |
|
"mousedown", function (e) { |
|
self.insertString(""); |
|
self.editor.focus(); |
|
e.stopEvent(); |
|
} |
|
); |
|
this.watermark.element.bind("click", function (e) { |
|
self.editor.focus(); |
|
e.stopEvent(); |
|
}); |
|
this.watermark.element.css({ |
|
position: "absolute", |
|
left: 3, |
|
right: 3, |
|
top: 6, |
|
bottom: 0 |
|
}); |
|
} |
|
}, |
|
|
|
_checkWaterMark: function () { |
|
var o = this.options; |
|
if (!this.disabledWarterMark && BI.isEmptyString(this.editor.getValue()) && BI.isKey(o.watermark)) { |
|
this.watermark && this.watermark.visible(); |
|
} else { |
|
this.watermark && this.watermark.invisible(); |
|
} |
|
}, |
|
|
|
disableWarterMark: function () { |
|
this.disabledWarterMark = true; |
|
this._checkWaterMark(); |
|
}, |
|
|
|
focus: function() { |
|
this.editor.focus(); |
|
}, |
|
|
|
/** |
|
* 添加字段 |
|
* @param field |
|
*/ |
|
insertField: function (field) { |
|
var from = this.editor.getCursor(); |
|
this.editor.replaceSelection(field); |
|
var to = this.editor.getCursor(); |
|
this.editor.markText(from, to, {className: 'fieldName', atomic: true, startStyle : "start", endStyle:"end"}); |
|
this.editor.replaceSelection(" "); |
|
this.editor.focus(); |
|
}, |
|
|
|
insertFunction: function (fn) { |
|
var from = this.editor.getCursor(); |
|
this.editor.replaceSelection(fn); |
|
var to = this.editor.getCursor(); |
|
this.editor.markText(from, to, {className: "#function", atomic: true}); |
|
this.editor.replaceSelection("() "); |
|
to = this.editor.getCursor(); |
|
to.ch = to.ch - 2; |
|
this.editor.setCursor(to); |
|
this.editor.focus(); |
|
}, |
|
insertOperator: function (op) { |
|
var from = this.editor.getCursor(); |
|
this.editor.replaceSelection(op); |
|
var to = this.editor.getCursor(); |
|
this.editor.markText(from, to, {className: "%operator", atomic: true}); |
|
this.editor.replaceSelection(" "); |
|
this.editor.focus(); |
|
}, |
|
|
|
setFunction: function (v) { |
|
var from = this.editor.getCursor(); |
|
this.editor.replaceSelection(v); |
|
var to = this.editor.getCursor(); |
|
this.editor.markText(from, to, {className: "#function", atomic: true}); |
|
}, |
|
|
|
insertString: function (str) { |
|
this.editor.replaceSelection(str); |
|
this.editor.focus(); |
|
}, |
|
|
|
getFormulaString: function () { |
|
return this.editor.getValue(); |
|
}, |
|
|
|
getUsedFields: function () { |
|
var fieldMap = this.options.fieldTextValueMap; |
|
var fields = []; |
|
this.editor.getValue(true, function (line) { |
|
var value = line.text; |
|
_.forEach(line.markedSpans, function (i, ms) { |
|
switch (i.marker.className) { |
|
case "fieldName": |
|
var dId = fieldMap[value.substr(i.from, i.to - i.from)]; |
|
if (!fields.contains(dId)) { |
|
fields.push(dId); |
|
} |
|
|
|
} |
|
}); |
|
}); |
|
return fields; |
|
}, |
|
|
|
getCheckString: function () { |
|
return this.editor.getValue(true, function (line) { |
|
var rawText = line.text, value = line.text, num = 0; |
|
value.text = rawText; |
|
_.forEach(line.markedSpans, function (i, ms) { |
|
|
|
switch (i.marker.className) { |
|
case "fieldName": |
|
var fieldNameLength = i.to - i.from; |
|
value = value.substr(0, i.from + num) + "$a" + value.substr(i.to + num, value.length); |
|
num = num + 2 - fieldNameLength; |
|
break; |
|
} |
|
|
|
}); |
|
return value; |
|
}); |
|
}, |
|
|
|
getValue: function () { |
|
var fieldMap = this.options.fieldTextValueMap; |
|
return this.editor.getValue("\n", function (line) { |
|
var rawText = line.text, value = line.text, num = 0; |
|
value.text = rawText; |
|
_.forEach(line.markedSpans, function (i, ms) { |
|
switch (i.marker.className) { |
|
case "fieldName": |
|
var fieldNameLength = i.to - i.from; |
|
var fieldId = fieldMap[value.substr(i.from + num, fieldNameLength)]; |
|
value = value.substr(0, i.from + num) + "$\{" + fieldMap[value.substr(i.from + num, fieldNameLength)] + "\}" + value.substr(i.to + num, value.length); |
|
num += fieldId.length - fieldNameLength + 3; |
|
break; |
|
} |
|
}); |
|
return value; |
|
}); |
|
}, |
|
|
|
setValue: function (value) { |
|
this.editor.setValue(value); |
|
}, |
|
|
|
setFieldTextValueMap: function (fieldTextValueMap) { |
|
this.options.fieldTextValueMap = fieldTextValueMap; |
|
}, |
|
|
|
refresh: function () { |
|
var self = this; |
|
BI.nextTick(function () { |
|
self.editor.refresh(); |
|
}); |
|
} |
|
|
|
}); |
|
BI.FormulaEditor.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.FormulaEditor.EVENT_BLUR = "EVENT_BLUR"; |
|
BI.FormulaEditor.EVENT_FOCUS = "EVENT_FOCUS"; |
|
$.shortcut("bi.formula", BI.FormulaEditor); |
|
})(jQuery); |
|
/** |
|
* z-index在1亿层级 |
|
* 弹出提示消息框,用于模拟阻塞操作(通过回调函数实现) |
|
* @class BI.Msg |
|
*/ |
|
$.extend(BI, { |
|
Msg: function () { |
|
|
|
var messageShow, $mask, $pop; |
|
|
|
return { |
|
alert: function (title, message, callback) { |
|
this._show(false, title, message, callback); |
|
}, |
|
confirm: function (title, message, callback, min_width) { |
|
this._show(true, title, message, callback); |
|
}, |
|
prompt: function (title, message, value, callback, min_width) { |
|
BI.Msg.prompt(title, message, value, callback, min_width); |
|
}, |
|
toast: function (message, level, context) { |
|
context = context || $("body"); |
|
var toast = BI.createWidget({ |
|
type: "bi.toast", |
|
level: level, |
|
text: message |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: context, |
|
items: [{ |
|
el: toast, |
|
left: "50%", |
|
top: 0 |
|
}] |
|
}); |
|
if (toast.element.outerWidth() > context.outerWidth()) { |
|
toast.setWidth(context.width()); |
|
} |
|
toast.element.css({"margin-left": -1 * toast.element.outerWidth() / 2}); |
|
toast.invisible(); |
|
toast.element.slideDown(500, function () { |
|
BI.delay(function () { |
|
toast.element.slideUp(500, function () { |
|
toast.destroy(); |
|
}) |
|
}, 5000) |
|
}) |
|
}, |
|
_show: function (hasCancel, title, message, callback) { |
|
$mask = $('<div class="bi-message-mask">').css({ |
|
position: 'absolute', |
|
'zIndex': BI.zIndex_tip - 2, |
|
top: 0, |
|
left: 0, |
|
right: 0, |
|
bottom: 0, |
|
opacity: 0.5 |
|
}).appendTo('body'); |
|
$pop = $('<div class="bi-message-depend">').css({ |
|
position: 'absolute', |
|
'zIndex': BI.zIndex_tip - 1, |
|
top: 0, |
|
left: 0, |
|
right: 0, |
|
bottom: 0 |
|
}).appendTo('body'); |
|
var close = function () { |
|
messageShow.destroy(); |
|
$mask.remove(); |
|
}; |
|
var controlItems = []; |
|
if (hasCancel === true) { |
|
controlItems.push({ |
|
el: { |
|
type: 'bi.button', |
|
text: BI.i18nText("BI-Basic_Cancel"), |
|
height: 30, |
|
level: 'ignore', |
|
handler: function () { |
|
close(); |
|
if (BI.isFunction(callback)) { |
|
callback.apply(null, [false]); |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
controlItems.push({ |
|
el: { |
|
type: 'bi.button', |
|
text: BI.i18nText("BI-Basic_OK"), |
|
height: 30, |
|
handler: function () { |
|
close(); |
|
if (BI.isFunction(callback)) { |
|
callback.apply(null, [true]); |
|
} |
|
} |
|
} |
|
}); |
|
var conf = { |
|
element: $pop, |
|
type: 'bi.center_adapt', |
|
items: [ |
|
{ |
|
type: 'bi.border', |
|
cls: 'bi-message-content', |
|
items: { |
|
'north': { |
|
el: { |
|
type: 'bi.border', |
|
cls: 'bi-message-title', |
|
items: { |
|
center: { |
|
el: { |
|
type: 'bi.label', |
|
text: title || BI.i18nText("BI-Basic_Prompt"), |
|
textAlign: 'left', |
|
hgap: 20, |
|
height: 50 |
|
} |
|
}, |
|
east: { |
|
el: { |
|
type: 'bi.icon_button', |
|
cls: 'bi-message-close close-font', |
|
// height: 50, |
|
handler: function () { |
|
close(); |
|
} |
|
}, |
|
width: 60 |
|
} |
|
} |
|
}, |
|
height: 50 |
|
}, |
|
'center': { |
|
el: { |
|
type: "bi.text", |
|
cls: "bi-message-text", |
|
tgap: 60, |
|
hgap: 20, |
|
lineHeight: 30, |
|
whiteSpace: "normal", |
|
text: message |
|
} |
|
}, |
|
'south': { |
|
el: { |
|
type: "bi.absolute", |
|
items: [{ |
|
el: { |
|
type: 'bi.right_vertical_adapt', |
|
hgap: 5, |
|
items: controlItems |
|
}, |
|
top: 0, |
|
left: 20, |
|
right: 20, |
|
bottom: 0 |
|
}] |
|
|
|
}, |
|
height: 60 |
|
} |
|
}, |
|
width: 400, |
|
height: 300 |
|
} |
|
] |
|
}; |
|
|
|
messageShow = BI.createWidget(conf); |
|
} |
|
}; |
|
}() |
|
});/** |
|
* Grid |
|
* |
|
* Created by GUY on 2016/1/11. |
|
* @class BI.Grid |
|
* @extends BI.Widget |
|
*/ |
|
BI.Grid = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.Grid.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-grid", |
|
width: 400, |
|
height: 300, |
|
overflowX: true, |
|
overflowY: true, |
|
overscanColumnCount: 0, |
|
overscanRowCount: 0, |
|
rowHeightGetter: BI.emptyFn, |
|
columnWidthGetter: BI.emptyFn, |
|
estimatedColumnSize: 100, |
|
estimatedRowSize: 30, |
|
scrollLeft: 0, |
|
scrollTop: 0, |
|
items: [] |
|
}); |
|
}, |
|
|
|
_init: function () { |
|
BI.Grid.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.renderedCells = []; |
|
this.renderedKeys = []; |
|
this._scrollLock = false; |
|
this._debounceRelease = BI.debounce(function () { |
|
self._scrollLock = false; |
|
}, 150); |
|
this.container = BI.createWidget({ |
|
type: "bi.absolute" |
|
}); |
|
this.element.scroll(function () { |
|
if (self._scrollLock === true) { |
|
return; |
|
} |
|
o.scrollLeft = self.element.scrollLeft(); |
|
o.scrollTop = self.element.scrollTop(); |
|
self._calculateChildrenToRender(); |
|
self.fireEvent(BI.Grid.EVENT_SCROLL, { |
|
scrollLeft: o.scrollLeft, |
|
scrollTop: o.scrollTop |
|
}); |
|
}); |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
element: this, |
|
scrollable: o.overflowX === true && o.overflowY === true, |
|
scrolly: o.overflowX === false && o.overflowY === true, |
|
scrollx: o.overflowX === true && o.overflowY === false, |
|
items: [this.container] |
|
}); |
|
if (o.items.length > 0) { |
|
this._populate(); |
|
} |
|
if (o.scrollLeft !== 0 || o.scrollTop !== 0) { |
|
BI.nextTick(function () { |
|
self.element.scrollTop(o.scrollTop); |
|
self.element.scrollLeft(o.scrollLeft); |
|
}); |
|
} |
|
}, |
|
|
|
_getOverscanIndices: function (cellCount, overscanCellsCount, startIndex, stopIndex) { |
|
return { |
|
overscanStartIndex: Math.max(0, startIndex - overscanCellsCount), |
|
overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount) |
|
} |
|
}, |
|
|
|
_calculateChildrenToRender: function () { |
|
var self = this, o = this.options; |
|
|
|
var width = o.width, height = o.height, scrollLeft = BI.clamp(o.scrollLeft, 0, this._getMaxScrollLeft()), scrollTop = BI.clamp(o.scrollTop, 0, this._getMaxScrollTop()), |
|
overscanColumnCount = o.overscanColumnCount, overscanRowCount = o.overscanRowCount; |
|
|
|
if (height > 0 && width > 0) { |
|
var visibleColumnIndices = this._columnSizeAndPositionManager.getVisibleCellRange(width, scrollLeft); |
|
var visibleRowIndices = this._rowSizeAndPositionManager.getVisibleCellRange(height, scrollTop); |
|
|
|
var horizontalOffsetAdjustment = this._columnSizeAndPositionManager.getOffsetAdjustment(width, scrollLeft); |
|
var verticalOffsetAdjustment = this._rowSizeAndPositionManager.getOffsetAdjustment(height, scrollTop); |
|
|
|
this._renderedColumnStartIndex = visibleColumnIndices.start; |
|
this._renderedColumnStopIndex = visibleColumnIndices.stop; |
|
this._renderedRowStartIndex = visibleRowIndices.start; |
|
this._renderedRowStopIndex = visibleRowIndices.stop; |
|
|
|
var overscanColumnIndices = this._getOverscanIndices(this.columnCount, overscanColumnCount, this._renderedColumnStartIndex, this._renderedColumnStopIndex) |
|
|
|
var overscanRowIndices = this._getOverscanIndices(this.rowCount, overscanRowCount, this._renderedRowStartIndex, this._renderedRowStopIndex); |
|
|
|
var columnStartIndex = overscanColumnIndices.overscanStartIndex; |
|
var columnStopIndex = overscanColumnIndices.overscanStopIndex; |
|
var rowStartIndex = overscanRowIndices.overscanStartIndex; |
|
var rowStopIndex = overscanRowIndices.overscanStopIndex; |
|
|
|
var renderedCells = [], renderedKeys = []; |
|
|
|
for (var rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) { |
|
var rowDatum = this._rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex); |
|
|
|
for (var columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) { |
|
var key = [rowIndex, columnIndex]; |
|
var columnDatum = this._columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex); |
|
|
|
var index = BI.deepIndexOf(this.renderedKeys, key); |
|
if (index > -1) { |
|
if (columnDatum.size !== this.renderedCells[index]._width) { |
|
this.renderedCells[index]._width = columnDatum.size; |
|
this.renderedCells[index].el.setWidth(columnDatum.size); |
|
} |
|
if (rowDatum.size !== this.renderedCells[index]._height) { |
|
this.renderedCells[index]._height = rowDatum.size; |
|
this.renderedCells[index].el.setHeight(rowDatum.size); |
|
} |
|
if (this.renderedCells[index].left !== columnDatum.offset + horizontalOffsetAdjustment) { |
|
this.renderedCells[index].el.element.css("left", (columnDatum.offset + horizontalOffsetAdjustment) + "px"); |
|
} |
|
if (this.renderedCells[index].top !== rowDatum.offset + verticalOffsetAdjustment) { |
|
this.renderedCells[index].el.element.css("top", (rowDatum.offset + verticalOffsetAdjustment) + "px"); |
|
} |
|
renderedCells.push(this.renderedCells[index]); |
|
} else { |
|
var child = BI.createWidget(BI.extend({ |
|
type: "bi.label", |
|
width: columnDatum.size, |
|
height: rowDatum.size |
|
}, o.items[rowIndex][columnIndex], { |
|
cls: (o.items[rowIndex][columnIndex].cls || "") + " grid-cell" + (rowIndex === 0 ? " first-row" : "") + (columnIndex === 0 ? " first-col" : ""), |
|
_rowIndex: rowIndex, |
|
_columnIndex: columnIndex, |
|
_left: columnDatum.offset + horizontalOffsetAdjustment, |
|
_top: rowDatum.offset + verticalOffsetAdjustment |
|
})); |
|
renderedCells.push({ |
|
el: child, |
|
left: columnDatum.offset + horizontalOffsetAdjustment, |
|
top: rowDatum.offset + verticalOffsetAdjustment, |
|
_width: columnDatum.size, |
|
_height: rowDatum.size |
|
}); |
|
} |
|
renderedKeys.push(key); |
|
} |
|
} |
|
//已存在的, 需要添加的和需要删除的 |
|
var existSet = {}, addSet = {}, deleteArray = []; |
|
BI.each(renderedKeys, function (i, key) { |
|
if (BI.deepContains(self.renderedKeys, key)) { |
|
existSet[i] = key; |
|
} else { |
|
addSet[i] = key; |
|
} |
|
}); |
|
BI.each(this.renderedKeys, function (i, key) { |
|
if (BI.deepContains(existSet, key)) { |
|
return; |
|
} |
|
if (BI.deepContains(addSet, key)) { |
|
return; |
|
} |
|
deleteArray.push(i); |
|
}); |
|
BI.each(deleteArray, function (i, index) { |
|
self.renderedCells[index].el.destroy(); |
|
}); |
|
var addedItems = []; |
|
BI.each(addSet, function (index) { |
|
addedItems.push(renderedCells[index]) |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this.container, |
|
items: addedItems |
|
}); |
|
this.renderedCells = renderedCells; |
|
this.renderedKeys = renderedKeys; |
|
} |
|
}, |
|
|
|
_getMaxScrollLeft: function () { |
|
return Math.max(0, this._columnSizeAndPositionManager.getTotalSize() - this.options.width + (this.options.overflowX ? BI.DOM.getScrollWidth() : 0)); |
|
}, |
|
|
|
_getMaxScrollTop: function () { |
|
return Math.max(0, this._rowSizeAndPositionManager.getTotalSize() - this.options.height + (this.options.overflowY ? BI.DOM.getScrollWidth() : 0)); |
|
}, |
|
|
|
_populate: function () { |
|
var self = this, o = this.options; |
|
if (o.items.length > 0) { |
|
this.columnCount = o.items[0].length; |
|
this.rowCount = o.items.length; |
|
this.container.setWidth(this.columnCount * o.estimatedColumnSize); |
|
this.container.setHeight(this.rowCount * o.estimatedRowSize); |
|
|
|
this._columnSizeAndPositionManager = new BI.ScalingCellSizeAndPositionManager(this.columnCount, o.columnWidthGetter, o.estimatedColumnSize); |
|
this._rowSizeAndPositionManager = new BI.ScalingCellSizeAndPositionManager(this.rowCount, o.rowHeightGetter, o.estimatedRowSize); |
|
|
|
this._calculateChildrenToRender(); |
|
this.element.scrollTop(o.scrollTop); |
|
this.element.scrollLeft(o.scrollLeft); |
|
} |
|
}, |
|
|
|
setScrollLeft: function (scrollLeft) { |
|
if (this.options.scrollLeft === scrollLeft) { |
|
return; |
|
} |
|
this._scrollLock = true; |
|
this.options.scrollLeft = BI.clamp(scrollLeft || 0, 0, this._getMaxScrollLeft()); |
|
this._debounceRelease(); |
|
this._calculateChildrenToRender(); |
|
this.element.scrollLeft(this.options.scrollLeft); |
|
}, |
|
|
|
setScrollTop: function (scrollTop) { |
|
if (this.options.scrollTop === scrollTop) { |
|
return; |
|
} |
|
this._scrollLock = true; |
|
this.options.scrollTop = BI.clamp(scrollTop || 0, 0, this._getMaxScrollTop()); |
|
this._debounceRelease(); |
|
this._calculateChildrenToRender(); |
|
this.element.scrollTop(this.options.scrollTop); |
|
}, |
|
|
|
setOverflowX: function (b) { |
|
var self = this; |
|
if (this.options.overflowX !== !!b) { |
|
this.options.overflowX = !!b; |
|
BI.nextTick(function () { |
|
self.element.css({overflowX: !!b ? "auto" : "hidden"}); |
|
}); |
|
} |
|
}, |
|
|
|
setOverflowY: function (b) { |
|
var self = this; |
|
if (this.options.overflowY !== !!b) { |
|
this.options.overflowY = !!b; |
|
BI.nextTick(function () { |
|
self.element.css({overflowY: !!b ? "auto" : "hidden"}); |
|
}); |
|
} |
|
}, |
|
|
|
getScrollLeft: function () { |
|
return this.options.scrollLeft; |
|
}, |
|
|
|
getScrollTop: function () { |
|
return this.options.scrollTop; |
|
}, |
|
|
|
getMaxScrollLeft: function () { |
|
return this._getMaxScrollLeft(); |
|
}, |
|
|
|
getMaxScrollTop: function () { |
|
return this._getMaxScrollTop(); |
|
}, |
|
|
|
setEstimatedColumnSize: function (width) { |
|
this.options.estimatedColumnSize = width; |
|
}, |
|
|
|
setEstimatedRowSize: function (height) { |
|
this.options.estimatedRowSize = height; |
|
}, |
|
|
|
restore: function () { |
|
BI.each(this.renderedCells, function (i, cell) { |
|
cell.el.destroy(); |
|
}); |
|
this.renderedCells = []; |
|
this.renderedKeys = []; |
|
this._scrollLock = false; |
|
}, |
|
|
|
populate: function (items) { |
|
if (items && items !== this.options.items) { |
|
this.options.items = items; |
|
this.restore(); |
|
} |
|
this._populate(); |
|
} |
|
}); |
|
BI.Grid.EVENT_SCROLL = "EVENT_SCROLL"; |
|
$.shortcut('bi.grid_view', BI.Grid);/** |
|
* floatBox弹出层, |
|
* @class BI.FloatBox |
|
* @extends BI.Widget |
|
*/ |
|
BI.FloatBox = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.FloatBox.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-float-box", |
|
width: 600, |
|
height: 500 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.FloatBox.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.showAction = new BI.ShowAction({ |
|
tar: this |
|
}); |
|
this._center = BI.createWidget(); |
|
this._north = BI.createWidget(); |
|
this.element.draggable({ |
|
cursor: BICst.cursorUrl, |
|
handle: ".bi-message-title", |
|
drag: function (e, ui) { |
|
var W = $("body").width(), H = $("body").height(); |
|
if (ui.position.left + o.width > W) { |
|
ui.position.left = W - o.width; |
|
} |
|
if (ui.position.top + o.height > H) { |
|
ui.position.top = H - o.height; |
|
} |
|
if (ui.position.left < 0) { |
|
ui.position.left = 0; |
|
} |
|
if (ui.position.top < 0) { |
|
ui.position.top = 0; |
|
} |
|
} |
|
}); |
|
this._south = BI.createWidget(); |
|
BI.createWidget({ |
|
type: 'bi.border', |
|
element: this, |
|
items: { |
|
'north': { |
|
el: { |
|
type: 'bi.border', |
|
cls: 'bi-message-title', |
|
items: { |
|
center: { |
|
el: { |
|
type: "bi.absolute", |
|
items: [{ |
|
el: this._north, |
|
left: 10, |
|
top: 0, |
|
right: 0, |
|
bottom: 0 |
|
}] |
|
} |
|
}, |
|
east: { |
|
el: { |
|
type: 'bi.icon_button', |
|
cls: 'bi-message-close close-font', |
|
height: 50, |
|
handler: function () { |
|
self.currentSectionProvider.close(); |
|
} |
|
}, |
|
width: 60 |
|
} |
|
} |
|
}, |
|
height: 50 |
|
}, |
|
'center': { |
|
el: { |
|
type: "bi.absolute", |
|
items: [{ |
|
el: this._center, |
|
left: 10, |
|
top: 10, |
|
right: 10, |
|
bottom: 10 |
|
}] |
|
} |
|
}, |
|
'south': { |
|
el: { |
|
type: "bi.absolute", |
|
items: [{ |
|
el: this._south, |
|
left: 10, |
|
top: 0, |
|
right: 10, |
|
bottom: 0 |
|
}] |
|
}, |
|
height: 60 |
|
} |
|
} |
|
}) |
|
}, |
|
|
|
populate: function (sectionProvider) { |
|
var self = this; |
|
this.currentSectionProvider = sectionProvider; |
|
sectionProvider.rebuildNorth(this._north); |
|
sectionProvider.rebuildCenter(this._center); |
|
sectionProvider.rebuildSouth(this._south); |
|
if (sectionProvider instanceof BI.Widget) { |
|
sectionProvider.on(BI.PopoverSection.EVENT_CLOSE, function () { |
|
self.close(); |
|
}) |
|
} |
|
}, |
|
|
|
show: function () { |
|
this.showAction.actionPerformed(); |
|
}, |
|
|
|
hide: function () { |
|
this.showAction.actionBack(); |
|
}, |
|
|
|
open: function () { |
|
this.show(); |
|
this.fireEvent(BI.FloatBox.EVENT_FLOAT_BOX_OPEN); |
|
}, |
|
|
|
close: function () { |
|
this.hide(); |
|
this.fireEvent(BI.FloatBox.EVENT_FLOAT_BOX_CLOSED); |
|
}, |
|
|
|
setZindex: function (zindex) { |
|
this.element.css({"z-index": zindex}); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.float_box", BI.FloatBox); |
|
|
|
BI.FloatBox.EVENT_FLOAT_BOX_CLOSED = "EVENT_FLOAT_BOX_CLOSED"; |
|
BI.FloatBox.EVENT_FLOAT_BOX_OPEN = "EVENT_FLOAT_BOX_CLOSED"; |
|
/** |
|
* 下拉框弹出层, zIndex在1000w |
|
* @class BI.PopupView |
|
* @extends BI.Widget |
|
*/ |
|
BI.PopupView = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.PopupView.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-list-view", |
|
maxWidth: 'auto', |
|
minWidth: 100, |
|
//maxHeight: 200, |
|
minHeight: 25, |
|
lgap: 0, |
|
rgap: 0, |
|
tgap: 0, |
|
bgap: 0, |
|
vgap: 0, |
|
hgap: 0, |
|
direction: BI.Direction.Top, //工具栏的方向 |
|
stopEvent: false,//是否停止mousedown、mouseup事件 |
|
stopPropagation: false, //是否停止mousedown、mouseup向上冒泡 |
|
logic: { |
|
dynamic: true |
|
}, |
|
|
|
tool: false, //自定义工具栏 |
|
tabs: [], //导航栏 |
|
buttons: [], //toolbar栏 |
|
|
|
el: { |
|
type: "bi.button_group", |
|
items: [], |
|
chooseType: 0, |
|
behaviors: {}, |
|
layouts: [{ |
|
type: "bi.vertical" |
|
}] |
|
} |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.PopupView.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
var fn = function (e) { |
|
e.stopPropagation(); |
|
}, stop = function (e) { |
|
e.stopEvent(); |
|
return false; |
|
}; |
|
this.element.css({ |
|
"z-index": BI.zIndex_popup, |
|
"min-width": o.minWidth + "px", |
|
"max-width": o.maxWidth + "px" |
|
}).bind({"click": fn}); |
|
|
|
//FIXME IE8下 jquery.mousewheeel.js 第一次执行65行$elem["offsetParent"]()的时候报错:未指明的错误 但是第二次或者调试的时候展开一下$elem内容均能避免上述问题 |
|
try { |
|
this.element.bind("mousewheel", fn); |
|
} catch (e) { |
|
this.element.bind("mousewheel", fn); |
|
} |
|
|
|
o.stopPropagation && this.element.bind({"mousedown": fn, "mouseup": fn, "mouseover": fn}); |
|
o.stopEvent && this.element.bind({"mousedown": stop, "mouseup": stop, "mouseover": stop}); |
|
this.tool = this._createTool(); |
|
this.tab = this._createTab(); |
|
this.view = this._createView(); |
|
this.toolbar = this._createToolBar(); |
|
|
|
this.button_group.on(BI.Controller.EVENT_CHANGE, function (type) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.PopupView.EVENT_CHANGE); |
|
} |
|
}); |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic(BI.LogicFactory.createLogicTypeByDirection(o.direction), BI.extend({}, o.logic, { |
|
scrolly: false, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
vgap: o.vgap, |
|
hgap: o.hgap, |
|
items: BI.LogicFactory.createLogicItemsByDirection(o.direction, |
|
BI.extend({ |
|
cls: "list-view-outer" |
|
}, BI.LogicFactory.createLogic(BI.LogicFactory.createLogicTypeByDirection(o.direction), BI.extend({}, o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection(o.direction, this.tool, this.tab, this.view, this.toolbar) |
|
}))) |
|
) |
|
})))); |
|
}, |
|
|
|
_createView: function () { |
|
var o = this.options; |
|
this.button_group = BI.createWidget(o.el, {type: "bi.button_group"}); |
|
this.button_group.element.css({"min-height": o.minHeight + "px"}); |
|
return this.button_group; |
|
}, |
|
|
|
_createTool: function () { |
|
var o = this.options; |
|
if (false === o.tool) { |
|
return; |
|
} |
|
return BI.createWidget(o.tool) |
|
}, |
|
|
|
_createTab: function () { |
|
var o = this.options; |
|
if (o.tabs.length === 0) { |
|
return; |
|
} |
|
return BI.createWidget({ |
|
type: "bi.center", |
|
cls: "list-view-tab", |
|
height: 25, |
|
items: o.tabs |
|
}) |
|
}, |
|
|
|
_createToolBar: function () { |
|
var o = this.options; |
|
if (o.buttons.length === 0) { |
|
return; |
|
} |
|
|
|
return BI.createWidget({ |
|
type: "bi.center", |
|
cls: "list-view-toolbar", |
|
height: 30, |
|
items: BI.createItems(o.buttons, { |
|
once: false, |
|
shadow: true, |
|
isShadowShowingOnSelected: true |
|
}) |
|
}) |
|
}, |
|
|
|
getView: function () { |
|
return this.button_group; |
|
}, |
|
|
|
populate: function (items) { |
|
this.button_group.populate.apply(this.button_group, arguments); |
|
}, |
|
|
|
resetWidth: function (w) { |
|
this.options.width = w; |
|
this.element.width(w); |
|
}, |
|
|
|
resetHeight: function (h) { |
|
var tbHeight = this.toolbar ? (this.toolbar.attr("height") || 30) : 0, |
|
tabHeight = this.tab ? (this.tab.attr("height") || 25) : 0, |
|
toolHeight = ((this.tool && this.tool.attr("height")) || 25) * ((this.tool && this.tool.isVisible()) ? 1 : 0); |
|
this.view.resetHeight ? this.view.resetHeight(h - tbHeight - tabHeight - toolHeight - 2) : |
|
this.view.element.css({"max-height": (h - tbHeight - tabHeight - toolHeight - 2) + "px"}) |
|
}, |
|
|
|
setEnable: function (arg) { |
|
BI.PopupView.superclass.setEnable.apply(this, arguments); |
|
this.view && this.view.setEnable(arg); |
|
}, |
|
|
|
setValue: function (selectedValues) { |
|
this.tab && this.tab.setValue(selectedValues); |
|
this.button_group.setValue(selectedValues); |
|
}, |
|
|
|
getValue: function () { |
|
return this.button_group.getValue(); |
|
} |
|
}); |
|
BI.PopupView.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.popup_view", BI.PopupView);/** |
|
* guy |
|
* @class BI.ScrollView |
|
* @extends BI.Widget |
|
*/ |
|
BI.ScrollView = BI.inherit(BI.Widget, { |
|
|
|
_const: { |
|
dropDownHeight: 15, |
|
expandIcon: "column-next-page-h-font", |
|
collapseIcon: "column-pre-page-h-font" |
|
}, |
|
|
|
_defaultConfig: function() { |
|
return BI.extend(BI.ScrollView.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls:"bi-scroll-view", |
|
scrollHeight: 50, |
|
maxHeight: 300 |
|
}) |
|
}, |
|
|
|
_init : function() { |
|
BI.ScrollView.superclass._init.apply(this, arguments); |
|
|
|
this.scrollUp = false; |
|
this.scroll = BI.createWidget({ |
|
type: "bi.vertical", |
|
cls: "scroll-container", |
|
scrolly: true |
|
}) |
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
element: this, |
|
scrolly: false, |
|
items: [this.scroll] |
|
}) |
|
|
|
this.dropdown = BI.createWidget({ |
|
type: "bi.icon_button", |
|
height: this._const.dropDownHeight, |
|
cls: "scroll-drop-down-icon " + this._const.expandIcon, |
|
handler: BI.bind(this._dropDownOrUp, this) |
|
}) |
|
|
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.dropdown, |
|
left: 0, |
|
right: 0, |
|
bottom: -1 * this._const.dropDownHeight |
|
}] |
|
}) |
|
this.populate(this.options.items); |
|
}, |
|
|
|
_dropDownOrUp: function(){ |
|
if(!this.scrollUp){ |
|
var height = this.element.height(); |
|
height += this.options.scrollHeight; |
|
height = Math.min(height, this.scroll.element[0].scrollHeight, this.options.maxHeight); |
|
this.element.height(height); |
|
this._checkDropDownState(); |
|
} else { |
|
var height = this.element.height(); |
|
height -= this.options.scrollHeight; |
|
height = Math.max(height, this.options.height); |
|
this.element.height(height); |
|
this._checkDropDownState(); |
|
} |
|
}, |
|
|
|
_checkDropDownState: function(){ |
|
var height = this.element.height(); |
|
if(!this._checkScroll() || height >= this.options.maxHeight){ |
|
this.scrollUp = true; |
|
this.dropdown.element.removeClass(this._const.expandIcon).addClass(this._const.collapseIcon); |
|
} else if(height <= this.options.height){ |
|
this.scrollUp = false; |
|
this.dropdown.element.addClass(this._const.expandIcon); |
|
} else { |
|
this.dropdown.element.addClass(this.scrollUp ? this._const.collapseIcon : this._const.expandIcon); |
|
} |
|
}, |
|
|
|
_checkScroll: function(){ |
|
this.scroll.element.height(this.element.height()); |
|
return this.scroll.element[0].scrollHeight > this.scroll.element[0].clientHeight; |
|
}, |
|
|
|
_checkDropDown: function(){ |
|
if(this._checkScroll()){ |
|
this.dropdown.visible(); |
|
//this.scrollUp = false; |
|
this._checkDropDownState(); |
|
} else { |
|
this.dropdown.invisible(); |
|
} |
|
}, |
|
|
|
populate: function(){ |
|
this.scroll.populate.apply(this.scroll, arguments); |
|
this.resize(); |
|
}, |
|
|
|
resize: function(){ |
|
this.element.height(this.options.height); |
|
BI.nextTick(BI.bind(this._checkDropDown, this)); |
|
}, |
|
|
|
addItem: function(){ |
|
this.scroll.addItem.apply(this.scroll, arguments); |
|
BI.nextTick(BI.bind(this._checkDropDown, this)); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.scroll_view", BI.ScrollView);/** |
|
* 搜索面板 |
|
* |
|
* Created by GUY on 2015/9/28. |
|
* @class BI.SearcherView |
|
* @extends BI.Pane |
|
*/ |
|
|
|
BI.SearcherView = BI.inherit(BI.Pane, { |
|
_defaultConfig: function () { |
|
var conf = BI.SearcherView.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-searcher-view", |
|
tipText: BI.i18nText("BI-No_Select"), |
|
chooseType: BI.Selection.Single, |
|
|
|
matcher: {//完全匹配的构造器 |
|
type: "bi.button_group", |
|
behaviors: { |
|
redmark: function () { |
|
return true; |
|
} |
|
}, |
|
items: [], |
|
layouts: [{ |
|
type: "bi.vertical" |
|
}] |
|
}, |
|
searcher: { |
|
type: "bi.button_group", |
|
behaviors: { |
|
redmark: function () { |
|
return true; |
|
} |
|
}, |
|
items: [], |
|
layouts: [{ |
|
type: "bi.vertical" |
|
}] |
|
} |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.SearcherView.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
|
|
this.matcher = BI.createWidget(o.matcher, { |
|
type: "bi.button_group", |
|
chooseType: o.chooseType, |
|
behaviors: { |
|
redmark: function () { |
|
return true; |
|
} |
|
}, |
|
layouts: [{ |
|
type: "bi.vertical" |
|
}] |
|
}); |
|
this.matcher.on(BI.Controller.EVENT_CHANGE, function (type, val, ob) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.SearcherView.EVENT_CHANGE, val, ob); |
|
} |
|
}); |
|
this.spliter = BI.createWidget({ |
|
type: "bi.vertical", |
|
height: 1, |
|
hgap: 10, |
|
items: [{ |
|
type: "bi.layout", |
|
height: 1, |
|
cls: "searcher-view-spliter" |
|
}] |
|
}); |
|
this.searcher = BI.createWidget(o.searcher, { |
|
type: "bi.button_group", |
|
chooseType: o.chooseType, |
|
behaviors: { |
|
redmark: function () { |
|
return true; |
|
} |
|
}, |
|
layouts: [{ |
|
type: "bi.vertical" |
|
}] |
|
}); |
|
this.searcher.on(BI.Controller.EVENT_CHANGE, function (type, val, ob) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.SearcherView.EVENT_CHANGE, val, ob); |
|
} |
|
}); |
|
|
|
BI.createWidget({ |
|
type: "bi.vertical", |
|
element: this, |
|
items: [this.matcher, this.spliter, this.searcher] |
|
}); |
|
}, |
|
|
|
startSearch: function () { |
|
|
|
}, |
|
|
|
stopSearch: function () { |
|
|
|
}, |
|
|
|
setValue: function (v) { |
|
this.matcher.setValue(v); |
|
this.searcher.setValue(v); |
|
}, |
|
|
|
getValue: function () { |
|
return this.matcher.getValue().concat(this.searcher.getValue()); |
|
}, |
|
|
|
populate: function (searchResult, matchResult, keyword) { |
|
searchResult || (searchResult = []); |
|
matchResult || (matchResult = []); |
|
this.setTipVisible(searchResult.length + matchResult.length === 0); |
|
this.spliter.setVisible(BI.isNotEmptyArray(matchResult) && BI.isNotEmptyArray(searchResult)); |
|
this.matcher.populate(matchResult, keyword); |
|
this.searcher.populate(searchResult, keyword); |
|
}, |
|
|
|
empty: function () { |
|
this.searcher.empty(); |
|
this.matcher.empty(); |
|
}, |
|
|
|
hasMatched: function () { |
|
return this.matcher.getAllButtons().length > 0; |
|
} |
|
}); |
|
BI.SearcherView.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.searcher_view", BI.SearcherView);/** |
|
* 分页控件 |
|
* |
|
* Created by GUY on 2015/8/31. |
|
* @class BI.Pager |
|
* @extends BI.Widget |
|
*/ |
|
BI.Pager = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.Pager.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-pager", |
|
behaviors: {}, |
|
layouts: [{ |
|
type: "bi.horizontal", |
|
hgap: 10, |
|
vgap: 0 |
|
}], |
|
|
|
dynamicShow: true, //是否动态显示上一页、下一页、首页、尾页, 若为false,则指对其设置使能状态 |
|
//dynamicShow为false时以下两个有用 |
|
dynamicShowFirstLast: false,//是否动态显示首页、尾页 |
|
dynamicShowPrevNext: false,//是否动态显示上一页、下一页 |
|
pages: false, //总页数 |
|
curr: function () { |
|
return 1; |
|
}, //初始化当前页 |
|
groups: 0, //连续显示分页数 |
|
jump: BI.emptyFn, //分页的回调函数 |
|
|
|
first: false, //是否显示首页 |
|
last: false, //是否显示尾页 |
|
prev: "上一页", |
|
next: "下一页", |
|
|
|
firstPage: 1, |
|
lastPage: function () { //在万不得已时才会调用这个函数获取最后一页的页码, 主要作用于setValue方法 |
|
return 1; |
|
}, |
|
hasPrev: BI.emptyFn, //pages不可用时有效 |
|
hasNext: BI.emptyFn //pages不可用时有效 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.Pager.superclass._init.apply(this, arguments); |
|
var self = this; |
|
this.currPage = BI.result(this.options, "curr"); |
|
//翻页太灵敏 |
|
this._lock = false; |
|
this._debouce = BI.debounce(function () { |
|
self._lock = false; |
|
}, 300); |
|
this._populate(); |
|
}, |
|
|
|
_populate: function () { |
|
var self = this, o = this.options, view = [], dict = {}; |
|
this.empty(); |
|
var pages = BI.result(o, "pages"); |
|
var curr = BI.result(this, "currPage"); |
|
var groups = BI.result(o, "groups"); |
|
var first = BI.result(o, "first"); |
|
var last = BI.result(o, "last"); |
|
var prev = BI.result(o, "prev"); |
|
var next = BI.result(o, "next"); |
|
|
|
if (pages === false) { |
|
groups = 0; |
|
first = false; |
|
last = false; |
|
} else { |
|
groups > pages && (groups = pages); |
|
} |
|
|
|
//计算当前组 |
|
dict.index = Math.ceil((curr + ((groups > 1 && groups !== pages) ? 1 : 0)) / (groups === 0 ? 1 : groups)); |
|
|
|
//当前页非首页,则输出上一页 |
|
if (((!o.dynamicShow && !o.dynamicShowPrevNext) || curr > 1) && prev !== false) { |
|
if (BI.isKey(prev)) { |
|
view.push({ |
|
text: prev, |
|
value: "prev", |
|
disabled: pages === false ? o.hasPrev(curr) === false : !(curr > 1 && prev !== false) |
|
}) |
|
} else { |
|
view.push(BI.extend({ |
|
disabled: pages === false ? o.hasPrev(curr) === false : !(curr > 1 && prev !== false) |
|
}, prev)); |
|
} |
|
} |
|
|
|
//当前组非首组,则输出首页 |
|
if (((!o.dynamicShow && !o.dynamicShowFirstLast) || (dict.index > 1 && groups !== 0)) && first) { |
|
view.push({ |
|
text: first, |
|
value: "first", |
|
disabled: !(dict.index > 1 && groups !== 0) |
|
}); |
|
if (dict.index > 1 && groups !== 0) { |
|
view.push({ |
|
type: "bi.label", |
|
cls: "page-ellipsis", |
|
text: "\u2026" |
|
}); |
|
} |
|
} |
|
|
|
//输出当前页组 |
|
dict.poor = Math.floor((groups - 1) / 2); |
|
dict.start = dict.index > 1 ? curr - dict.poor : 1; |
|
dict.end = dict.index > 1 ? (function () { |
|
var max = curr + (groups - dict.poor - 1); |
|
return max > pages ? pages : max; |
|
}()) : groups; |
|
if (dict.end - dict.start < groups - 1) { //最后一组状态 |
|
dict.start = dict.end - groups + 1; |
|
} |
|
var s = dict.start, e = dict.end; |
|
if (first && last && (dict.index > 1 && groups !== 0) && (pages > groups && dict.end < pages && groups !== 0)) { |
|
s++; |
|
e--; |
|
} |
|
for (; s <= e; s++) { |
|
if (s === curr) { |
|
view.push({ |
|
text: s, |
|
value: s, |
|
selected: true |
|
}) |
|
} else { |
|
view.push({ |
|
text: s, |
|
value: s |
|
}) |
|
} |
|
} |
|
|
|
//总页数大于连续分页数,且当前组最大页小于总页,输出尾页 |
|
if (((!o.dynamicShow && !o.dynamicShowFirstLast) || (pages > groups && dict.end < pages && groups !== 0)) && last) { |
|
if (pages > groups && dict.end < pages && groups !== 0) { |
|
view.push({ |
|
type: "bi.label", |
|
cls: "page-ellipsis", |
|
text: "\u2026" |
|
}); |
|
} |
|
view.push({ |
|
text: last, |
|
value: "last", |
|
disabled: !(pages > groups && dict.end < pages && groups !== 0) |
|
}) |
|
} |
|
|
|
//当前页不为尾页时,输出下一页 |
|
dict.flow = !prev && groups === 0; |
|
if (((!o.dynamicShow && !o.dynamicShowPrevNext) && next) || (curr !== pages && next || dict.flow)) { |
|
view.push((function () { |
|
if (BI.isKey(next)) { |
|
if (pages === false) { |
|
return {text: next, value: "next", disabled: o.hasNext(curr) === false} |
|
} |
|
return (dict.flow && curr === pages) |
|
? |
|
{text: next, value: "next", disabled: true} |
|
: |
|
{text: next, value: "next", disabled: !(curr !== pages && next || dict.flow)}; |
|
} else { |
|
return BI.extend({ |
|
disabled: pages === false ? o.hasNext(curr) === false : !(curr !== pages && next || dict.flow) |
|
}, next); |
|
} |
|
}())); |
|
} |
|
|
|
this.button_group = BI.createWidget({ |
|
type: "bi.button_group", |
|
element: this, |
|
items: BI.createItems(view, { |
|
cls: "page-item", |
|
height: 23, |
|
hgap: 10 |
|
}), |
|
behaviors: o.behaviors, |
|
layouts: o.layouts |
|
}); |
|
this.button_group.on(BI.Controller.EVENT_CHANGE, function (type, value, obj) { |
|
if (self._lock === true) { |
|
return; |
|
} |
|
self._lock = true; |
|
self._debouce(); |
|
if (type === BI.Events.CLICK) { |
|
var v = self.button_group.getValue()[0]; |
|
switch (v) { |
|
case "first": |
|
self.currPage = 1; |
|
break; |
|
case "last": |
|
self.currPage = pages; |
|
break; |
|
case "prev": |
|
self.currPage--; |
|
break; |
|
case "next": |
|
self.currPage++; |
|
break; |
|
default: |
|
self.currPage = v; |
|
break; |
|
} |
|
o.jump.apply(self, [{ |
|
pages: pages, |
|
curr: self.currPage |
|
}]); |
|
self._populate(); |
|
self.fireEvent(BI.Pager.EVENT_CHANGE, obj); |
|
} |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
}); |
|
this.fireEvent(BI.Pager.EVENT_AFTER_POPULATE); |
|
}, |
|
|
|
getCurrentPage: function () { |
|
return this.currPage; |
|
}, |
|
|
|
setAllPages: function (pages) { |
|
this.options.pages = pages; |
|
}, |
|
|
|
hasPrev: function (v) { |
|
v || (v = 1); |
|
var o = this.options; |
|
var pages = this.options.pages; |
|
return pages === false ? o.hasPrev(v) : v > 1; |
|
}, |
|
|
|
hasNext: function (v) { |
|
v || (v = 1); |
|
var o = this.options; |
|
var pages = this.options.pages; |
|
return pages === false ? o.hasNext(v) : v < pages; |
|
}, |
|
|
|
setValue: function (v) { |
|
var o = this.options; |
|
v = v | 0; |
|
v = v < 1 ? 1 : v; |
|
if (o.pages === false) { |
|
var lastPage = BI.result(o, "lastPage"), firstPage = 1; |
|
this.currPage = v > lastPage ? lastPage : ((firstPage = BI.result(o, "firstPage")), (v < firstPage ? firstPage : v)); |
|
} else { |
|
v = v > o.pages ? o.pages : v; |
|
this.currPage = v; |
|
} |
|
this._populate(); |
|
}, |
|
|
|
getValue: function () { |
|
var val = this.button_group.getValue()[0]; |
|
switch (val) { |
|
case "prev": |
|
return -1; |
|
case "next": |
|
return 1; |
|
case "first": |
|
return BI.MIN; |
|
case "last": |
|
return BI.MAX; |
|
default : |
|
return val; |
|
} |
|
}, |
|
|
|
attr: function (key, value) { |
|
BI.Pager.superclass.attr.apply(this, arguments); |
|
if (key === "curr") { |
|
this.currPage = BI.result(this.options, "curr"); |
|
} |
|
}, |
|
|
|
populate: function () { |
|
this._populate(); |
|
} |
|
}); |
|
BI.Pager.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.Pager.EVENT_AFTER_POPULATE = "EVENT_AFTER_POPULATE"; |
|
$.shortcut("bi.pager", BI.Pager);/** |
|
* Created by Young's on 2016/8/17. |
|
* 加载面板,一般只用于 requestAsync 中 |
|
*/ |
|
BI.RequstLoading = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.RequstLoading.superclass._defaultConfig.apply(this, arguments), {}); |
|
}, |
|
|
|
_init: function () { |
|
BI.RequstLoading.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
var mask = BI.Maskers.create(BI.RequstLoading.MASK_ID); |
|
this.callback = o.callback; |
|
this.paneTab = BI.createWidget({ |
|
type: "bi.tab", |
|
cardCreator: BI.bind(this._cardCreator, this), |
|
defaultShowIndex: BI.RequstLoading.ERROR, |
|
width: 400, |
|
height: 300 |
|
}); |
|
var tempIcon = BI.createWidget({ |
|
type: "bi.icon_button", |
|
cls: "data-link-test-fail-icon", |
|
width: 0, |
|
height: 0 |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: $('body'), |
|
items: [{ |
|
el: tempIcon, |
|
bottom: 0 |
|
}] |
|
}); |
|
|
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: mask, |
|
cls: "bi-request-loading", |
|
items: [{ |
|
el: { |
|
type: "bi.layout", |
|
cls: "mask-pane" |
|
}, |
|
top: 0, |
|
left: 0, |
|
bottom: 0, |
|
right: 0 |
|
}, { |
|
el: { |
|
type: "bi.center_adapt", |
|
items: [this.paneTab] |
|
}, |
|
top: 0, |
|
left: 0, |
|
right: 0, |
|
bottom: 0 |
|
}] |
|
}); |
|
}, |
|
|
|
_cardCreator: function (v) { |
|
var self = this; |
|
var cancel = BI.createWidget({ |
|
type: "bi.button", |
|
text: BI.i18nText("BI-Basic_Cancel"), |
|
title: BI.i18nText("BI-Basic_Cancel"), |
|
level: "ignore", |
|
height: 28, |
|
width: 90, |
|
handler: function () { |
|
BI.Maskers.hide(BI.RequstLoading.MASK_ID); |
|
} |
|
}); |
|
var retry = BI.createWidget({ |
|
type: "bi.button", |
|
text: BI.i18nText("BI-Basic_Retry"), |
|
title: BI.i18nText("BI-Basic_Retry"), |
|
level: "common", |
|
height: 28, |
|
width: 90, |
|
handler: function () { |
|
self.paneTab.setSelect(BI.RequstLoading.LOADING); |
|
self.callback(); |
|
} |
|
}); |
|
switch (v) { |
|
case BI.RequstLoading.LOADING: |
|
return BI.createWidget({ |
|
type: "bi.vertical", |
|
items: [{ |
|
type: "bi.center_adapt", |
|
cls: "loading-bar-icon", |
|
items: [{ |
|
type: "bi.icon", |
|
width: 208, |
|
height: 15 |
|
}] |
|
}, { |
|
type: "bi.label", |
|
cls: "loading-comment", |
|
text: BI.i18nText("BI-Basic_Loading"), |
|
height: 30 |
|
}], |
|
width: 208, |
|
height: 200, |
|
vgap: 10 |
|
}); |
|
case BI.RequstLoading.LOADING_TIMEOUT: |
|
return BI.createWidget({ |
|
type: "bi.vertical", |
|
items: [{ |
|
type: "bi.center_adapt", |
|
cls: "loading-bar-icon", |
|
items: [{ |
|
type: "bi.icon", |
|
width: 208, |
|
height: 15 |
|
}] |
|
}, { |
|
type: "bi.label", |
|
cls: "loading-comment", |
|
text: BI.i18nText("BI-Basic_Loading"), |
|
height: 30 |
|
}, { |
|
type: "bi.label", |
|
text: BI.i18nText("BI-Request_Time_Out_Toast_Tip"), |
|
cls: "load-timeout-warning" |
|
}, { |
|
type: "bi.left_right_vertical_adapt", |
|
items: { |
|
left: [cancel], |
|
right: [retry] |
|
}, |
|
height: 30, |
|
llgap: 100, |
|
rrgap: 100 |
|
}], |
|
vgap: 10 |
|
}); |
|
case BI.RequstLoading.ERROR: |
|
return BI.createWidget({ |
|
type: "bi.vertical", |
|
items: [{ |
|
type: "bi.center_adapt", |
|
cls: "data-link-test-fail-icon", |
|
items: [{ |
|
type: "bi.icon", |
|
width: 126, |
|
height: 126 |
|
}] |
|
}, { |
|
type: "bi.label", |
|
text: BI.i18nText("BI-Connection_Lost"), |
|
cls: "load-fail-comment" |
|
}, { |
|
type: "bi.left_right_vertical_adapt", |
|
items: { |
|
left: [cancel], |
|
right: [retry] |
|
}, |
|
height: 30, |
|
llgap: 100, |
|
rrgap: 100 |
|
}], |
|
vgap: 20 |
|
}); |
|
} |
|
}, |
|
|
|
showLoading: function () { |
|
BI.Maskers.show(BI.RequstLoading.MASK_ID); |
|
this.paneTab.setSelect(BI.RequstLoading.LOADING); |
|
}, |
|
|
|
showError: function () { |
|
BI.Maskers.show(BI.RequstLoading.MASK_ID); |
|
this.paneTab.setSelect(BI.RequstLoading.ERROR); |
|
}, |
|
|
|
setCallback: function (callback) { |
|
this.callback = callback; |
|
} |
|
}); |
|
BI.extend(BI.RequstLoading, { |
|
MASK_ID: "___request__loading___", |
|
LOADING: 1, |
|
LOADING_TIMEOUT: 2, |
|
ERROR: 3 |
|
}); |
|
$.shortcut("bi.request_loading", BI.RequstLoading);/** |
|
* 超链接 |
|
* |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.A |
|
* @extends BI.Text |
|
* @abstract |
|
*/ |
|
BI.A = BI.inherit(BI.Text, { |
|
_defaultConfig: function () { |
|
var conf = BI.A.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-a display-block", |
|
href: "", |
|
target: "_blank", |
|
el: null, |
|
element: "<a/>" |
|
}) |
|
}, |
|
_init: function () { |
|
var o = this.options; |
|
BI.A.superclass._init.apply(this, arguments); |
|
this.element.attr({href: o.href, target: o.target}); |
|
if (o.el) { |
|
BI.createWidget(o.el, { |
|
element: this |
|
}); |
|
} |
|
} |
|
}); |
|
|
|
$.shortcut("bi.a", BI.A);/** |
|
* guy |
|
* 加载条 |
|
* @type {*|void|Object} |
|
*/ |
|
BI.LoadingBar = BI.inherit(BI.Single, { |
|
consts: { |
|
loadedText: BI.i18nText("BI-Load_More"), |
|
endText: BI.i18nText("BI-No_More_Data") |
|
}, |
|
_defaultConfig: function() { |
|
var conf = BI.LoadingBar.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend( conf, { |
|
baseCls : (conf.baseCls ||"")+' bi-loading-bar', |
|
height: 30, |
|
handler: BI.emptyFn |
|
}) |
|
}, |
|
_init : function() { |
|
BI.LoadingBar.superclass._init.apply(this, arguments); |
|
var self = this; |
|
this.loaded = BI.createWidget({ |
|
type: "bi.text_button", |
|
cls: "loading-text", |
|
text: this.consts.loadedText, |
|
width: 120, |
|
handler: this.options.handler |
|
}) |
|
this.loaded.on(BI.Controller.EVENT_CHANGE, function(type){ |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
}) |
|
|
|
this.loading = BI.createWidget({ |
|
type: "bi.layout", |
|
width: this.options.height, |
|
height:this.options.height, |
|
cls: "loading-background cursor-default" |
|
}) |
|
var loaded = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
items: [this.loaded] |
|
}) |
|
var loading = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
items: [this.loading] |
|
}) |
|
this.cardLayout = BI.createWidget({ |
|
type: "bi.card", |
|
element: this, |
|
items: [{ |
|
el: loaded, |
|
cardName: "loaded" |
|
}, { |
|
el: loading, |
|
cardName: "loading" |
|
}] |
|
}) |
|
this.invisible(); |
|
}, |
|
|
|
_reset: function(){ |
|
this.visible(); |
|
this.loaded.setText(this.consts.loadedText); |
|
this.loaded.enable(); |
|
}, |
|
|
|
setLoaded: function(){ |
|
this._reset(); |
|
this.cardLayout.showCardByName("loaded"); |
|
}, |
|
|
|
setEnd: function(){ |
|
this.setLoaded(); |
|
this.loaded.setText(this.consts.endText); |
|
this.loaded.disable(); |
|
}, |
|
|
|
setLoading: function(){ |
|
this._reset(); |
|
this.cardLayout.showCardByName("loading"); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.loading_bar", BI.LoadingBar);/** |
|
* @class BI.IconButton |
|
* @extends BI.BasicButton |
|
* 图标的button |
|
*/ |
|
BI.IconButton = BI.inherit(BI.BasicButton, { |
|
_defaultConfig: function () { |
|
var conf = BI.IconButton.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-icon-button horizon-center", |
|
iconWidth: null, |
|
iconHeight: null |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.IconButton.superclass._init.apply(this, arguments); |
|
var o = this.options; |
|
this.element.css({ |
|
textAlign: 'center' |
|
}); |
|
this.icon = BI.createWidget({ |
|
type: 'bi.icon', |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
}); |
|
if (BI.isNumber(o.height) && o.height > 0 && BI.isNull(o.iconWidth) && BI.isNull(o.iconHeight)) { |
|
this.element.css("lineHeight", o.height + "px"); |
|
BI.createWidget({ |
|
type: "bi.default", |
|
element: this, |
|
items: [this.icon] |
|
}) |
|
} else { |
|
BI.createWidget({ |
|
element: this, |
|
type: 'bi.center_adapt', |
|
items: [this.icon] |
|
}); |
|
} |
|
}, |
|
|
|
doClick: function () { |
|
BI.IconButton.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.IconButton.EVENT_CHANGE, this); |
|
} |
|
} |
|
}); |
|
BI.IconButton.EVENT_CHANGE = "IconButton.EVENT_CHANGE"; |
|
$.shortcut("bi.icon_button", BI.IconButton);/** |
|
* 图片的button |
|
* |
|
* Created by GUY on 2016/1/27. |
|
* @class BI.ImageButton |
|
* @extends BI.BasicButton |
|
*/ |
|
BI.ImageButton = BI.inherit(BI.BasicButton, { |
|
_defaultConfig: function () { |
|
var conf = BI.ImageButton.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-image-button", |
|
src: "", |
|
iconWidth: "100%", |
|
iconHeight: "100%" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.ImageButton.superclass._init.apply(this, arguments); |
|
var o = this.options; |
|
this.image = BI.createWidget({ |
|
type: "bi.img", |
|
width: o.iconWidth, |
|
height: o.iconHeight, |
|
src: o.src |
|
}); |
|
if (BI.isNumber(o.iconWidth) || BI.isNumber(o.iconHeight)) { |
|
BI.createWidget({ |
|
type: "bi.center_adapt", |
|
element: this, |
|
items: [this.image] |
|
}) |
|
} else { |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
element: this, |
|
items: [this.image], |
|
scrollable: false |
|
}) |
|
} |
|
}, |
|
|
|
setWidth: function (w) { |
|
BI.ImageButton.superclass.setWidth.apply(this, arguments); |
|
this.options.width = w; |
|
}, |
|
|
|
setHeight: function (h) { |
|
BI.ImageButton.superclass.setHeight.apply(this, arguments); |
|
this.options.height = h; |
|
}, |
|
|
|
setImageWidth: function (w) { |
|
this.image.setWidth(w); |
|
}, |
|
|
|
setImageHeight: function (h) { |
|
this.image.setHeight(h); |
|
}, |
|
|
|
getImageWidth: function () { |
|
return this.image.element.width(); |
|
}, |
|
|
|
getImageHeight: function () { |
|
return this.image.element.height(); |
|
}, |
|
|
|
setSrc: function (src) { |
|
this.options.src = src; |
|
this.image.setSrc(src); |
|
}, |
|
|
|
getSrc: function () { |
|
return this.image.getSrc(); |
|
}, |
|
|
|
doClick: function () { |
|
BI.ImageButton.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.ImageButton.EVENT_CHANGE, this); |
|
} |
|
} |
|
}); |
|
BI.ImageButton.EVENT_CHANGE = "ImageButton.EVENT_CHANGE"; |
|
$.shortcut("bi.image_button", BI.ImageButton);(function ($) { |
|
|
|
/** |
|
* 文字类型的按钮 |
|
* @class BI.Button |
|
* @extends BI.BasicButton |
|
* |
|
* @cfg {JSON} options 配置属性 |
|
* @cfg {'common'/'success'/'warning'/'ignore'} [options.level='common'] 按钮类型,用不同颜色强调不同的场景 |
|
*/ |
|
BI.Button = BI.inherit(BI.BasicButton, { |
|
_const: { |
|
minWidth: 90 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.Button.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + ' bi-button', |
|
shadow: true, |
|
isShadowShowingOnSelected: true, |
|
readonly: true, |
|
iconClass: "", |
|
level: 'common', |
|
textAlign: "center", |
|
whiteSpace: "nowrap", |
|
forceCenter: false, |
|
textWidth: null, |
|
textHeight: null, |
|
hgap: 10, |
|
vgap: 0, |
|
tgap: 0, |
|
bgap: 0, |
|
lgap: 0, |
|
rgap: 0 |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Button.superclass._init.apply(this, arguments); |
|
var o = this.options, self = this; |
|
if (BI.isNumber(o.height)) { |
|
this.element.css({height: o.height - 2, lineHeight: (o.height - 2) + 'px'}); |
|
} |
|
if (BI.isKey(o.iconClass)) { |
|
this.icon = BI.createWidget({ |
|
type: "bi.icon", |
|
width: 18 |
|
}); |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
text: o.text, |
|
value: o.value |
|
}); |
|
BI.createWidget({ |
|
type: "bi.horizontal_auto", |
|
cls: "button-" + o.level + " " + o.iconClass, |
|
element: this, |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
items: [{ |
|
type: "bi.horizontal", |
|
items: [this.icon, this.text] |
|
}] |
|
}) |
|
} else { |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "button-" + o.level, |
|
textAlign: o.textAlign, |
|
whiteSpace: o.whiteSpace, |
|
forceCenter: o.forceCenter, |
|
textWidth: o.textWidth, |
|
textHeight: o.textHeight, |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
element: this, |
|
text: o.text, |
|
value: o.value |
|
}); |
|
} |
|
this.element.css({"min-width": this._const.minWidth - 2 + "px"}); |
|
}, |
|
|
|
doClick: function () { |
|
BI.Button.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.Button.EVENT_CHANGE, this); |
|
} |
|
}, |
|
|
|
setText: function (text) { |
|
BI.Button.superclass.setText.apply(this, arguments); |
|
this.text.setText(text); |
|
}, |
|
|
|
setValue: function (text) { |
|
BI.Button.superclass.setValue.apply(this, arguments); |
|
if (!this.isReadOnly()) { |
|
this.text.setValue(text); |
|
} |
|
}, |
|
|
|
setEnable: function (b) { |
|
BI.Button.superclass.setEnable.apply(this, arguments); |
|
this.text.setEnable(b); |
|
this.icon && this.icon.setEnable(b); |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
destroy: function () { |
|
BI.Button.superclass.destroy.apply(this, arguments); |
|
} |
|
}); |
|
$.shortcut('bi.button', BI.Button); |
|
BI.Button.EVENT_CHANGE = "EVENT_CHANGE"; |
|
})(jQuery);/** |
|
* guy |
|
* 可以点击的一行文字 |
|
* @class BI.TextButton |
|
* @extends BI.BasicButton |
|
* 文字button |
|
*/ |
|
BI.TextButton = BI.inherit(BI.BasicButton, { |
|
_defaultConfig: function () { |
|
var conf = BI.TextButton.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-text-button", |
|
textAlign: "center", |
|
whiteSpace: "nowrap", |
|
forceCenter: false, |
|
textWidth: null, |
|
textHeight: null, |
|
hgap: 0, |
|
lgap: 0, |
|
rgap: 0, |
|
text: "", |
|
py: "" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.TextButton.superclass._init.apply(this, arguments); |
|
var o = this.options; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
element: this, |
|
textAlign: o.textAlign, |
|
whiteSpace: o.whiteSpace, |
|
textWidth: o.textWidth, |
|
textHeight: o.textHeight, |
|
forceCenter: o.forceCenter, |
|
width: o.width, |
|
height: o.height, |
|
hgap: o.hgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
text: o.text, |
|
value: o.value, |
|
py: o.py |
|
}); |
|
}, |
|
|
|
doClick: function () { |
|
BI.TextButton.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.TextButton.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
setText: function (text) { |
|
BI.TextButton.superclass.setText.apply(this, arguments); |
|
text = BI.isArray(text) ? text.join(",") : text; |
|
this.text.setText(text); |
|
}, |
|
|
|
setStyle: function (style) { |
|
this.text.setStyle(style); |
|
}, |
|
|
|
setValue: function (text) { |
|
BI.TextButton.superclass.setValue.apply(this, arguments); |
|
if (!this.isReadOnly()) { |
|
text = BI.isArray(text) ? text.join(",") : text; |
|
this.text.setValue(text); |
|
} |
|
} |
|
}); |
|
BI.TextButton.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.text_button", BI.TextButton);/** |
|
* 带有一个占位 |
|
* |
|
* Created by GUY on 2015/9/11. |
|
* @class BI.BlankIconIconTextItem |
|
* @extends BI.BasicButton |
|
*/ |
|
BI.BlankIconIconTextItem = BI.inherit(BI.BasicButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.BlankIconIconTextItem.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-blank-icon-text-item", |
|
logic: { |
|
dynamic: false |
|
}, |
|
iconCls1: "close-ha-font", |
|
iconCls2: "close-ha-font", |
|
blankWidth: 0, |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.BlankIconIconTextItem.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
var blank = BI.createWidget({ |
|
type: "bi.layout", |
|
width: o.blankWidth |
|
}) |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "list-item-text", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}); |
|
this.icon1 = BI.createWidget({ |
|
type: "bi.icon_button", |
|
cls: o.iconCls1, |
|
forceNotSelected: true, |
|
width: c.commonWidth |
|
}); |
|
this.icon2 = BI.createWidget({ |
|
type: "bi.icon_button", |
|
cls: o.iconCls2, |
|
forceNotSelected: true, |
|
width: c.commonWidth |
|
}); |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("horizontal", BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection("left", blank, this.icon1, this.icon2, this.text) |
|
})))); |
|
}, |
|
|
|
doClick: function () { |
|
BI.BlankIconIconTextItem.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.BlankIconIconTextItem.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
setSelected: function (b) { |
|
BI.BlankIconIconTextItem.superclass.setSelected.apply(this, arguments); |
|
this.icon1.setSelected(b); |
|
this.icon2.setSelected(b); |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
} |
|
}); |
|
BI.BlankIconIconTextItem.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.blank_icon_icon_text_item", BI.BlankIconIconTextItem);/** |
|
* guy |
|
* 一个占位符和两个icon和一行数 组成的一行listitem |
|
* |
|
* Created by GUY on 2015/9/15. |
|
* @class BI.BlankIconTextIconItem |
|
* @extends BI.BasicButton |
|
*/ |
|
BI.BlankIconTextIconItem = BI.inherit(BI.BasicButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.BlankIconTextIconItem.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-blank-icon-text-icon-item", |
|
logic: { |
|
dynamic: false |
|
}, |
|
iconCls1: "close-ha-font", |
|
iconCls2: "close-ha-font", |
|
blankWidth: 0, |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.BlankIconTextIconItem.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}) |
|
|
|
var icon1 = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
cls: o.iconCls1, |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}) |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: { |
|
type: "bi.center_adapt", |
|
cls: o.iconCls2, |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}, |
|
top: 0, |
|
bottom: 0, |
|
right: 0 |
|
}] |
|
}) |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("horizontal", BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection("left", { |
|
type: "bi.layout", |
|
width: o.blankWidth |
|
}, icon1, this.text, { |
|
type: "bi.layout", |
|
width: c.commonWidth |
|
}) |
|
})))); |
|
}, |
|
|
|
doClick: function () { |
|
BI.BlankIconTextIconItem.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.BlankIconTextIconItem.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
} |
|
}); |
|
BI.BlankIconTextIconItem.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.blank_icon_text_icon_item", BI.BlankIconTextIconItem);/** |
|
* 带有一个占位 |
|
* |
|
* Created by GUY on 2015/9/11. |
|
* @class BI.BlankIconTextItem |
|
* @extends BI.BasicButton |
|
*/ |
|
BI.BlankIconTextItem = BI.inherit(BI.BasicButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.BlankIconTextItem.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-blank-icon-text-item", |
|
logic: { |
|
dynamic: false |
|
}, |
|
cls: "close-ha-font", |
|
blankWidth: 0, |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.BlankIconTextItem.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
var blank = BI.createWidget({ |
|
type: "bi.layout", |
|
width: o.blankWidth |
|
}) |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "list-item-text", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}) |
|
this.icon = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}); |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("horizontal", BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection("left", blank, this.icon, this.text) |
|
})))); |
|
}, |
|
|
|
doClick: function () { |
|
BI.BlankIconTextItem.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.BlankIconTextItem.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
} |
|
}); |
|
BI.BlankIconTextItem.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.blank_icon_text_item", BI.BlankIconTextItem);/** |
|
* guy |
|
* 两个icon和一行数 组成的一行listitem |
|
* |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.IconTextIconItem |
|
* @extends BI.BasicButton |
|
*/ |
|
BI.IconTextIconItem = BI.inherit(BI.BasicButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.IconTextIconItem.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-icon-text-icon-item", |
|
logic: { |
|
dynamic: false |
|
}, |
|
iconCls1: "close-ha-font", |
|
iconCls2: "close-ha-font", |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.IconTextIconItem.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}) |
|
|
|
var icon1 = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
cls: o.iconCls1, |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}) |
|
var blank = BI.createWidget({ |
|
type: "bi.layout", |
|
width: c.commonWidth |
|
}) |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: { |
|
type: "bi.center_adapt", |
|
cls: o.iconCls2, |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}, |
|
top: 0, |
|
bottom: 0, |
|
right: 0 |
|
}] |
|
}) |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("horizontal", BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection("left", icon1, this.text, blank) |
|
})))); |
|
}, |
|
|
|
doClick: function () { |
|
BI.IconTextIconItem.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.IconTextIconItem.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
} |
|
}); |
|
BI.IconTextIconItem.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.icon_text_icon_item", BI.IconTextIconItem);/** |
|
* guy |
|
* |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.IconTextItem |
|
* @extends BI.BasicButton |
|
*/ |
|
BI.IconTextItem = BI.inherit(BI.BasicButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.IconTextItem.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-icon-text-item", |
|
direction: BI.Direction.Left, |
|
logic: { |
|
dynamic: false |
|
}, |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.IconTextItem.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "list-item-text", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}); |
|
this.icon = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}); |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic(BI.LogicFactory.createLogicTypeByDirection(o.direction), BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection(o.direction, this.icon, this.text) |
|
})))); |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
}, |
|
|
|
doClick: function () { |
|
BI.IconTextItem.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.IconTextItem.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
} |
|
}); |
|
BI.IconTextItem.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.icon_text_item", BI.IconTextItem);/** |
|
* |
|
* 图标的button |
|
* |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.TextIconItem |
|
* @extends BI.BasicButton |
|
*/ |
|
BI.TextIconItem = BI.inherit(BI.BasicButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.TextIconItem.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-text-icon-item", |
|
logic: { |
|
dynamic: false |
|
}, |
|
cls: "close-ha-font", |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.TextIconItem.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "list-item-text", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}); |
|
this.icon = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}); |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("horizontal", BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection("left", this.text, this.icon) |
|
})))); |
|
}, |
|
|
|
doClick: function () { |
|
BI.TextIconItem.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.TextIconItem.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
} |
|
}); |
|
BI.TextIconItem.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.text_icon_item", BI.TextIconItem);/** |
|
* guy |
|
* 一个button和一行数 组成的一行listitem |
|
* |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.TextItem |
|
* @extends BI.BasicButton |
|
*/ |
|
BI.TextItem = BI.inherit(BI.BasicButton, { |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.TextItem.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-text-item", |
|
textAlign: "left", |
|
whiteSpace: "nowrap", |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.TextItem.superclass._init.apply(this, arguments); |
|
var o = this.options; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
element: this, |
|
textAlign: o.textAlign, |
|
whiteSpace: o.whiteSpace, |
|
textHeight: o.whiteSpace == "nowrap" ? o.height : o.textHeight, |
|
height: o.height, |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
py: o.py |
|
}); |
|
}, |
|
|
|
doClick: function () { |
|
BI.TextItem.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.TextItem.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
} |
|
}); |
|
BI.TextItem.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.text_item", BI.TextItem);/** |
|
* guy |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.IconTextIconNode |
|
* @extends BI.NodeButton |
|
*/ |
|
BI.IconTextIconNode = BI.inherit(BI.NodeButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.IconTextIconNode.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-icon-text-icon-node", |
|
logic: { |
|
dynamic: false |
|
}, |
|
iconCls1: "close-ha-font", |
|
iconCls2: "close-ha-font", |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.IconTextIconNode.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}) |
|
|
|
var icon1 = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
cls: o.iconCls1, |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}) |
|
var blank = BI.createWidget({ |
|
type: "bi.layout", |
|
width: c.commonWidth |
|
}) |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: { |
|
type: "bi.center_adapt", |
|
cls: o.iconCls2, |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}, |
|
top: 0, |
|
bottom: 0, |
|
right: 0 |
|
}] |
|
}) |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("horizontal", BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection("left", icon1, this.text, blank) |
|
})))); |
|
}, |
|
|
|
doClick: function () { |
|
BI.IconTextIconNode.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.IconTextIconNode.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
} |
|
}); |
|
BI.IconTextIconNode.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.icon_text_icon_node", BI.IconTextIconNode);/** |
|
* guy |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.IconTextNode |
|
* @extends BI.NodeButton |
|
*/ |
|
BI.IconTextNode = BI.inherit(BI.NodeButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.IconTextNode.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-icon-text-node", |
|
logic: { |
|
dynamic: false |
|
}, |
|
cls: "close-ha-font", |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.IconTextNode.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "list-item-text", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}) |
|
this.icon = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}) |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("horizontal", BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection("left", this.icon, this.text) |
|
})))); |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
}, |
|
|
|
doClick: function () { |
|
BI.IconTextNode.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.IconTextNode.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
} |
|
}); |
|
BI.IconTextNode.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.icon_text_node", BI.IconTextNode);/** |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.TextIconNode |
|
* @extends BI.NodeButton |
|
*/ |
|
BI.TextIconNode = BI.inherit(BI.NodeButton, { |
|
_const: { |
|
commonWidth: 25 |
|
}, |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.TextIconNode.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-text-icon-node", |
|
logic: { |
|
dynamic: false |
|
}, |
|
cls: "close-ha-font", |
|
iconHeight: null, |
|
iconWidth: null, |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.TextIconNode.superclass._init.apply(this, arguments); |
|
var o = this.options, c = this._const; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "list-item-text", |
|
textAlign: "left", |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
height: o.height |
|
}) |
|
this.icon = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
width: c.commonWidth, |
|
items: [{ |
|
el: { |
|
type: "bi.icon", |
|
width: o.iconWidth, |
|
height: o.iconHeight |
|
} |
|
}] |
|
}); |
|
|
|
BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("horizontal", BI.extend(o.logic, { |
|
items: BI.LogicFactory.createLogicItemsByDirection("left", this.text, this.icon) |
|
})))); |
|
}, |
|
|
|
doClick: function () { |
|
BI.TextIconNode.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.TextIconNode.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
} |
|
}); |
|
BI.TextIconNode.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.text_icon_node", BI.TextIconNode);/** |
|
* guy |
|
* |
|
* Created by GUY on 2015/9/9. |
|
* @class BI.TextNode |
|
* @extends BI.NodeButton |
|
*/ |
|
BI.TextNode = BI.inherit(BI.NodeButton, { |
|
|
|
_defaultConfig: function () { |
|
var conf = BI.TextNode.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-text-node", |
|
textAlign: "left", |
|
whiteSpace: "nowrap", |
|
textHgap: 0, |
|
textVgap: 0, |
|
textLgap: 0, |
|
textRgap: 0 |
|
}) |
|
}, |
|
_init: function () { |
|
BI.TextNode.superclass._init.apply(this, arguments); |
|
var o = this.options; |
|
this.text = BI.createWidget({ |
|
type: "bi.label", |
|
element: this, |
|
textAlign: o.textAlign, |
|
whiteSpace: o.whiteSpace, |
|
textHeight: o.whiteSpace == "nowrap" ? o.height : o.textHeight, |
|
height: o.height, |
|
hgap: o.textHgap, |
|
vgap: o.textVgap, |
|
lgap: o.textLgap, |
|
rgap: o.textRgap, |
|
text: o.text, |
|
value: o.value, |
|
keyword: o.keyword, |
|
py: o.py |
|
}); |
|
}, |
|
|
|
doClick: function () { |
|
BI.TextNode.superclass.doClick.apply(this, arguments); |
|
if (this.isValid()) { |
|
this.fireEvent(BI.TextNode.EVENT_CHANGE, this.getValue(), this); |
|
} |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
setValue: function () { |
|
if (!this.isReadOnly()) { |
|
this.text.setValue.apply(this.text, arguments); |
|
} |
|
}, |
|
|
|
getValue: function () { |
|
return this.text.getValue(); |
|
}, |
|
|
|
setText: function () { |
|
this.text.setText.apply(this.text, arguments); |
|
}, |
|
|
|
getText: function () { |
|
return this.text.getText(); |
|
} |
|
}); |
|
BI.TextNode.EVENT_CHANGE = "EVENT_CHANGE"; |
|
$.shortcut("bi.text_node", BI.TextNode);/** |
|
* |
|
* Created by GUY on 2016/1/15. |
|
* @class BI.CodeEditor |
|
* @extends BI.Single |
|
*/ |
|
BI.CodeEditor = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
return $.extend(BI.CodeEditor.superclass._defaultConfig.apply(), { |
|
baseCls: 'bi-code-editor', |
|
value: '', |
|
watermark: "" |
|
}); |
|
}, |
|
_init: function () { |
|
BI.CodeEditor.superclass._init.apply(this, arguments); |
|
var o = this.options, self = this; |
|
this.editor = CodeMirror(this.element[0], { |
|
textWrapping: true, |
|
lineWrapping: true, |
|
lineNumbers: false |
|
}); |
|
this.editor.on("change", function (cm, change) { |
|
BI.nextTick(function () { |
|
self.fireEvent(BI.CodeEditor.EVENT_CHANGE) |
|
}); |
|
}); |
|
|
|
this.editor.on("focus", function () { |
|
watermark.setVisible(false); |
|
self.fireEvent(BI.CodeEditor.EVENT_FOCUS); |
|
}); |
|
|
|
this.editor.on("blur", function () { |
|
watermark.setVisible(BI.isEmptyString(self.getValue())); |
|
self.fireEvent(BI.CodeEditor.EVENT_BLUR); |
|
}); |
|
|
|
// this.editor.on("blur", function () { |
|
// self.editor.execCommand("goLineEnd"); |
|
// }); |
|
|
|
//水印 |
|
var watermark = BI.createWidget({ |
|
type: "bi.label", |
|
text: o.watermark, |
|
cls: "bi-water-mark", |
|
whiteSpace: "nowrap", |
|
textAlign: "left" |
|
}); |
|
watermark.element.bind( |
|
"mousedown", function (e) { |
|
self.insertString(""); |
|
self.editor.focus(); |
|
e.stopEvent(); |
|
} |
|
); |
|
watermark.element.bind("click", function (e) { |
|
self.editor.focus(); |
|
e.stopEvent(); |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: watermark, |
|
top: 0, |
|
left: 5 |
|
}] |
|
}); |
|
|
|
if (BI.isKey(o.value)) { |
|
BI.nextTick(function () { |
|
self.setValue(o.value); |
|
}); |
|
} |
|
}, |
|
|
|
setEnable: function (b) { |
|
BI.CodeEditor.superclass.setEnable.apply(this, arguments); |
|
this.editor.setOption("readOnly", b === true ? false : "nocursor") |
|
}, |
|
|
|
insertParam: function(param){ |
|
var from = this.editor.getCursor(); |
|
this.editor.replaceSelection(param); |
|
var to = this.editor.getCursor(); |
|
this.editor.markText(from, to, {className: 'param', atomic: true}); |
|
this.editor.replaceSelection(" "); |
|
this.editor.focus(); |
|
}, |
|
|
|
insertString: function(str){ |
|
this.editor.replaceSelection(str); |
|
this.editor.focus(); |
|
}, |
|
|
|
getValue: function () { |
|
return this.editor.getValue("\n", function (line) { |
|
var rawText = line.text, value = line.text, num = 0; |
|
value.text = rawText; |
|
_.forEach(line.markedSpans, function (i, ms) { |
|
switch (i.marker.className) { |
|
case "param": |
|
var fieldNameLength = i.to - i.from; |
|
value = value.substr(0, i.from + num) + "$\{" + value.substr(i.from + num, fieldNameLength) + "\}" + value.substr(i.to + num, value.length); |
|
num += 3; |
|
break; |
|
} |
|
}); |
|
return value; |
|
}); |
|
}, |
|
|
|
_analyzeContent: function (v) { |
|
var regx = /\$[\{][^\}]*[\}]|(\s+)|\w*\w|\$\{[^\$\(\)\+\-\*\/)\$,]*\w\}|\$\{[^\$\(\)\+\-\*\/]*\w\}|\$\{[^\$\(\)\+\-\*\/]*[\u4e00-\u9fa5]\}|\w|(.)|\n/g; |
|
return v.match(regx); |
|
}, |
|
|
|
setValue: function (v) { |
|
var self = this, result; |
|
this.refresh(); |
|
self.editor.setValue(""); |
|
result = this._analyzeContent(v || ""); |
|
BI.each(result, function (i, item) { |
|
var fieldRegx = /\$[\{][^\}]*[\}]/; |
|
var str = item.match(fieldRegx); |
|
if (BI.isNotEmptyArray(str)) { |
|
self.insertParam(str[0].substring(2, item.length - 1)); |
|
} else { |
|
self.insertString(item); |
|
} |
|
}) |
|
}, |
|
|
|
refresh: function(){ |
|
var self = this; |
|
BI.nextTick(function () { |
|
self.editor.refresh(); |
|
}); |
|
} |
|
}); |
|
BI.CodeEditor.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.CodeEditor.EVENT_BLUR = "EVENT_BLUR"; |
|
BI.CodeEditor.EVENT_FOCUS = "EVENT_FOCUS"; |
|
$.shortcut("bi.code_editor", BI.CodeEditor);/** |
|
* Created by GUY on 2015/4/15. |
|
* @class BI.Editor |
|
* @extends BI.Single |
|
*/ |
|
BI.Editor = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.Editor.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: "bi-editor", |
|
hgap: 4, |
|
vgap: 2, |
|
lgap: 0, |
|
rgap: 0, |
|
tgap: 0, |
|
bgap: 0, |
|
tipType: "warning", |
|
inputType: "text", |
|
validationChecker: BI.emptyFn, |
|
quitChecker: BI.emptyFn, |
|
mouseOut: false, |
|
allowBlank: false, |
|
watermark: "", |
|
errorText: "" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Editor.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.editor = this.addWidget(BI.createWidget({ |
|
type: "bi.input", |
|
element: "<input type='" + o.inputType + "'/>", |
|
watermark: o.watermark, |
|
validationChecker: o.validationChecker, |
|
quitChecker: o.quitChecker, |
|
mouseOut: o.mouseOut, |
|
allowBlank: o.allowBlank |
|
})); |
|
this.editor.element.css({ |
|
"width": "100%", |
|
"height": "100%", |
|
"border": "none", |
|
"outline": "none", |
|
"padding": "0", |
|
"margin": "0" |
|
}); |
|
if (BI.isKey(this.options.watermark)) { |
|
this.watermark = BI.createWidget({ |
|
type: "bi.label", |
|
cls: "bi-water-mark", |
|
text: this.options.watermark, |
|
height: this.options.height, |
|
forceCenter: true, |
|
height: o.height - 2 * (o.vgap + o.tgap), |
|
whiteSpace: "nowrap", |
|
textAlign: "left" |
|
}); |
|
this.watermark.element.bind({ |
|
mousedown: function (e) { |
|
if (self.isEnabled()) { |
|
self.editor.isEditing() || self.editor.focus(); |
|
} else { |
|
self.editor.isEditing() && self.editor.blur(); |
|
} |
|
e.stopEvent(); |
|
} |
|
}); |
|
this.watermark.element.bind("click", function (e) { |
|
if (self.isEnabled()) { |
|
self.editor.isEditing() || self.editor.focus(); |
|
} else { |
|
self.editor.isEditing() && self.editor.blur(); |
|
} |
|
e.stopEvent(); |
|
}); |
|
this.watermark.element.css({ |
|
position: "absolute", |
|
left: "3px", |
|
right: "3px", |
|
top: "0px", |
|
bottom: "0px" |
|
}); |
|
} |
|
var items = [{ |
|
el: { |
|
type: "bi.default", |
|
items: this.watermark ? [this.editor, this.watermark] : [this.editor] |
|
}, |
|
left: o.hgap + o.lgap, |
|
right: o.hgap + o.rgap, |
|
top: o.vgap + o.tgap, |
|
bottom: o.vgap + o.bgap |
|
}]; |
|
|
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: items |
|
}); |
|
this.editor.on(BI.Controller.EVENT_CHANGE, function () { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_FOCUS, function () { |
|
self._checkError(); |
|
self.element.addClass("bi-editor-focus"); |
|
self.fireEvent(BI.Editor.EVENT_FOCUS, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_BLUR, function () { |
|
self.setErrorVisible(false); |
|
self.element.removeClass("bi-editor-focus"); |
|
self.fireEvent(BI.Editor.EVENT_BLUR, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_CLICK, function () { |
|
self.fireEvent(BI.Editor.EVENT_CLICK, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_CHANGE, function () { |
|
self.fireEvent(BI.Editor.EVENT_CHANGE, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_KEY_DOWN, function (v) { |
|
self.fireEvent(BI.Editor.EVENT_KEY_DOWN, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_QUICK_DOWN, function (v) { |
|
self.watermark && self.watermark.invisible(); |
|
}); |
|
|
|
this.editor.on(BI.Input.EVENT_VALID, function () { |
|
self._checkWaterMark(); |
|
self.setErrorVisible(false); |
|
self.fireEvent(BI.Editor.EVENT_VALID, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_ERROR, function () { |
|
self._checkWaterMark(); |
|
self.fireEvent(BI.Editor.EVENT_ERROR, arguments); |
|
self.setErrorVisible(self.isEditing()); |
|
}); |
|
this.editor.on(BI.Input.EVENT_RESTRICT, function () { |
|
self._checkWaterMark(); |
|
var tip = self.setErrorVisible(true); |
|
tip && tip.element.fadeOut(100, function () { |
|
tip.element.fadeIn(100); |
|
}); |
|
self.fireEvent(BI.Editor.EVENT_RESTRICT, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_EMPTY, function () { |
|
self._checkWaterMark(); |
|
self.fireEvent(BI.Editor.EVENT_EMPTY, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_ENTER, function () { |
|
self.fireEvent(BI.Editor.EVENT_ENTER, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_SPACE, function () { |
|
self.fireEvent(BI.Editor.EVENT_SPACE, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_BACKSPACE, function () { |
|
self.fireEvent(BI.Editor.EVENT_BACKSPACE, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_REMOVE, function () { |
|
self.fireEvent(BI.Editor.EVENT_REMOVE, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_START, function () { |
|
self.fireEvent(BI.Editor.EVENT_START, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_PAUSE, function () { |
|
self.fireEvent(BI.Editor.EVENT_PAUSE, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_STOP, function () { |
|
self.fireEvent(BI.Editor.EVENT_STOP, arguments); |
|
}); |
|
this.editor.on(BI.Input.EVENT_CONFIRM, function () { |
|
self.fireEvent(BI.Editor.EVENT_CONFIRM, arguments); |
|
}); |
|
this.element.click(function (e) { |
|
e.stopPropagation(); |
|
return false; |
|
}); |
|
if (BI.isKey(this.options.value) || BI.isEmptyString(this.options.value)) { |
|
this.setValue(this.options.value); |
|
} else { |
|
this._checkWaterMark(); |
|
} |
|
}, |
|
|
|
_checkToolTip: function () { |
|
var o = this.options; |
|
var errorText = o.errorText; |
|
if (BI.isFunction(errorText)) { |
|
errorText = errorText(this.editor.getValue()); |
|
} |
|
if (BI.isKey(errorText)) { |
|
if (!this.isEnabled() || this.isValid() || (BI.Bubbles.has(this.getName()) && BI.Bubbles.get(this.getName()).isVisible())) { |
|
this.setTitle(""); |
|
} else { |
|
this.setTitle(errorText); |
|
} |
|
} |
|
}, |
|
|
|
_checkError: function () { |
|
this.setErrorVisible(this.isEnabled() && !this.isValid()); |
|
this._checkToolTip(); |
|
}, |
|
|
|
_checkWaterMark: function () { |
|
var o = this.options; |
|
if (!this.disabledWarterMark && this.editor.getValue() === "" && BI.isKey(o.watermark)) { |
|
this.watermark && this.watermark.visible(); |
|
} else { |
|
this.watermark && this.watermark.invisible(); |
|
} |
|
}, |
|
|
|
setErrorText: function (text) { |
|
this.options.errorText = text; |
|
}, |
|
|
|
getErrorText: function () { |
|
return this.options.errorText; |
|
}, |
|
|
|
setErrorVisible: function (b) { |
|
var o = this.options; |
|
var errorText = o.errorText; |
|
if (BI.isFunction(errorText)) { |
|
errorText = errorText(this.editor.getValue()); |
|
} |
|
if (!this.disabledError && BI.isKey(errorText)) { |
|
BI.Bubbles[b ? "show" : "hide"](this.getName(), errorText, this); |
|
this._checkToolTip(); |
|
return BI.Bubbles.get(this.getName()); |
|
} |
|
}, |
|
|
|
disableError: function () { |
|
this.disabledError = true; |
|
this._checkError(); |
|
}, |
|
|
|
enableError: function () { |
|
this.disabledError = false; |
|
this._checkError(); |
|
}, |
|
|
|
disableWarterMark: function () { |
|
this.disabledWarterMark = true; |
|
this._checkWaterMark(); |
|
}, |
|
|
|
enableWarterMark: function () { |
|
this.disabledWarterMark = false; |
|
this._checkWaterMark(); |
|
}, |
|
|
|
focus: function () { |
|
this.element.addClass("text-editor-focus"); |
|
this.editor.focus(); |
|
}, |
|
|
|
blur: function () { |
|
this.element.removeClass("text-editor-focus"); |
|
this.editor.blur(); |
|
}, |
|
|
|
selectAll: function () { |
|
this.editor.selectAll(); |
|
}, |
|
|
|
onKeyDown: function (k) { |
|
this.editor.onKeyDown(k); |
|
}, |
|
|
|
setValue: function (v) { |
|
BI.Editor.superclass.setValue.apply(this, arguments); |
|
this.editor.setValue(v); |
|
this._checkError(); |
|
this._checkWaterMark(); |
|
}, |
|
|
|
getLastValidValue: function () { |
|
return this.editor.getLastValidValue(); |
|
}, |
|
|
|
resetLastValidValue: function () { |
|
this.editor.resetLastValidValue(); |
|
}, |
|
|
|
getValue: function () { |
|
if (!this.isValid()) { |
|
return BI.trim(this.editor.getLastValidValue()); |
|
} |
|
return BI.trim(this.editor.getValue()); |
|
}, |
|
|
|
setValid: function (b) { |
|
BI.Editor.superclass.setValid.apply(this, arguments); |
|
this.editor.setValid(b); |
|
}, |
|
|
|
isEditing: function () { |
|
return this.editor.isEditing(); |
|
}, |
|
|
|
isValid: function () { |
|
return this.editor.isValid(); |
|
}, |
|
|
|
setEnable: function (b) { |
|
BI.Editor.superclass.setEnable.apply(this, arguments); |
|
this.editor && this.editor.setEnable(b); |
|
this.watermark && this.watermark.setEnable(b); |
|
} |
|
}); |
|
BI.Editor.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.Editor.EVENT_FOCUS = "EVENT_FOCUS"; |
|
BI.Editor.EVENT_BLUR = "EVENT_BLUR"; |
|
BI.Editor.EVENT_CLICK = "EVENT_CLICK"; |
|
BI.Editor.EVENT_KEY_DOWN = "EVENT_KEY_DOWN"; |
|
BI.Editor.EVENT_SPACE = "EVENT_SPACE"; |
|
BI.Editor.EVENT_BACKSPACE = "EVENT_BACKSPACE"; |
|
|
|
BI.Editor.EVENT_START = "EVENT_START"; |
|
BI.Editor.EVENT_PAUSE = "EVENT_PAUSE"; |
|
BI.Editor.EVENT_STOP = "EVENT_STOP"; |
|
BI.Editor.EVENT_CONFIRM = "EVENT_CONFIRM"; |
|
BI.Editor.EVENT_VALID = "EVENT_VALID"; |
|
BI.Editor.EVENT_ERROR = "EVENT_ERROR"; |
|
BI.Editor.EVENT_ENTER = "EVENT_ENTER"; |
|
BI.Editor.EVENT_RESTRICT = "EVENT_RESTRICT"; |
|
BI.Editor.EVENT_REMOVE = "EVENT_REMOVE"; |
|
BI.Editor.EVENT_EMPTY = "EVENT_EMPTY"; |
|
|
|
$.shortcut("bi.editor", BI.Editor);/** |
|
* 多文件 |
|
* |
|
* Created by GUY on 2016/4/13. |
|
* @class BI.MultifileEditor |
|
* @extends BI.Single |
|
* @abstract |
|
*/ |
|
BI.MultifileEditor = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.MultifileEditor.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-multifile-editor", |
|
multiple: false, |
|
maxSize: 1024 * 1024, |
|
accept: "", |
|
url: "" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
var self = this, o = this.options; |
|
BI.MultifileEditor.superclass._init.apply(this, arguments); |
|
this.file = BI.createWidget({ |
|
type: "bi.file", |
|
cls: "multifile-editor", |
|
width: "100%", |
|
height: "100%", |
|
name: o.name, |
|
url: o.url, |
|
multiple: o.multiple, |
|
accept: o.accept, |
|
maxSize: o.maxSize |
|
}); |
|
this.file.on(BI.File.EVENT_CHANGE, function () { |
|
self.fireEvent(BI.MultifileEditor.EVENT_CHANGE, arguments); |
|
}); |
|
this.file.on(BI.File.EVENT_UPLOADSTART, function () { |
|
self.fireEvent(BI.MultifileEditor.EVENT_UPLOADSTART, arguments); |
|
}); |
|
this.file.on(BI.File.EVENT_ERROR, function () { |
|
self.fireEvent(BI.MultifileEditor.EVENT_ERROR, arguments); |
|
}); |
|
this.file.on(BI.File.EVENT_PROGRESS, function () { |
|
self.fireEvent(BI.MultifileEditor.EVENT_PROGRESS, arguments); |
|
}); |
|
this.file.on(BI.File.EVENT_UPLOADED, function () { |
|
self.fireEvent(BI.MultifileEditor.EVENT_UPLOADED, arguments); |
|
}); |
|
|
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: { |
|
type: "bi.adaptive", |
|
scrollable: false, |
|
items: [this.file] |
|
}, |
|
top: 0, |
|
right: 0, |
|
left: 0, |
|
bottom: 0 |
|
}] |
|
}); |
|
}, |
|
|
|
select: function () { |
|
this.file.select(); |
|
}, |
|
|
|
getValue: function () { |
|
return this.file.getValue(); |
|
}, |
|
|
|
upload: function () { |
|
this.file.upload(); |
|
}, |
|
|
|
reset: function () { |
|
this.file.reset(); |
|
}, |
|
|
|
setEnable: function (enable) { |
|
BI.MultiFile.superclass.setEnable.apply(this, arguments); |
|
this.file.setEnable(enable); |
|
} |
|
}); |
|
BI.MultifileEditor.EVENT_CHANGE = "MultifileEditor.EVENT_CHANGE"; |
|
BI.MultifileEditor.EVENT_UPLOADSTART = "MultifileEditor.EVENT_UPLOADSTART"; |
|
BI.MultifileEditor.EVENT_ERROR = "MultifileEditor.EVENT_ERROR"; |
|
BI.MultifileEditor.EVENT_PROGRESS = "MultifileEditor.EVENT_PROGRESS"; |
|
BI.MultifileEditor.EVENT_UPLOADED = "MultifileEditor.EVENT_UPLOADED"; |
|
$.shortcut("bi.multifile_editor", BI.MultifileEditor);/** |
|
* |
|
* Created by GUY on 2016/1/18. |
|
* @class BI.TextAreaEditor |
|
* @extends BI.Single |
|
*/ |
|
BI.TextAreaEditor = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
return $.extend(BI.TextAreaEditor.superclass._defaultConfig.apply(), { |
|
baseCls: 'bi-textarea-editor', |
|
value: '' |
|
}); |
|
}, |
|
_init: function () { |
|
BI.TextAreaEditor.superclass._init.apply(this, arguments); |
|
var o = this.options, self = this; |
|
this.content = BI.createWidget({ |
|
type: "bi.layout", |
|
tagName: "textarea", |
|
width: "100%", |
|
height: "100%", |
|
cls: "textarea-editor-content display-block" |
|
}); |
|
this.content.element.css({"resize": "none"}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: { |
|
type: "bi.adaptive", |
|
items: [this.content] |
|
}, |
|
left: 0, |
|
right: 3, |
|
top: 0, |
|
bottom: 5 |
|
}] |
|
}); |
|
|
|
this.content.element.on("input propertychange", function (e) { |
|
self._checkWaterMark(); |
|
self.fireEvent(BI.TextAreaEditor.EVENT_CHANGE); |
|
}); |
|
|
|
this.content.element.focus(function () { |
|
if (self.isValid()) { |
|
self._focus(); |
|
self.fireEvent(BI.TextAreaEditor.EVENT_FOCUS); |
|
} |
|
$(document).bind("mousedown." + self.getName(), function (e) { |
|
if (BI.DOM.isExist(self) && !self.element.__isMouseInBounds__(e)) { |
|
$(document).unbind("mousedown." + self.getName()); |
|
self.content.element.blur(); |
|
} |
|
}); |
|
}); |
|
this.content.element.blur(function () { |
|
if (self.isValid()) { |
|
self._blur(); |
|
self.fireEvent(BI.TextAreaEditor.EVENT_BLUR); |
|
} |
|
$(document).unbind("mousedown." + self.getName()); |
|
}); |
|
if (BI.isKey(o.value)) { |
|
self.setValue(o.value); |
|
} |
|
if (BI.isNotNull(o.style)) { |
|
self.setValue(o.style); |
|
} |
|
this._checkWaterMark(); |
|
}, |
|
|
|
_checkWaterMark: function () { |
|
var self = this, o = this.options; |
|
var val = this.getValue(); |
|
if (BI.isNotEmptyString(val)) { |
|
this.watermark && this.watermark.destroy(); |
|
this.watermark = null; |
|
} else { |
|
if (BI.isNotEmptyString(o.watermark)) { |
|
if (!this.watermark) { |
|
this.watermark = BI.createWidget({ |
|
type: "bi.text_button", |
|
cls: "bi-water-mark", |
|
textAlign: "left", |
|
height: 30, |
|
text: o.watermark, |
|
invalid: o.invalid, |
|
disabled: o.disabled |
|
}); |
|
this.watermark.on(BI.TextButton.EVENT_CHANGE, function () { |
|
self.focus(); |
|
}); |
|
BI.createWidget({ |
|
type: 'bi.absolute', |
|
element: this, |
|
items: [{ |
|
el: this.watermark, |
|
left: 0, |
|
top: 0, |
|
right: 0 |
|
}] |
|
}) |
|
} else { |
|
this.watermark.setText(o.watermark); |
|
this.watermark.setValid(!o.invalid); |
|
this.watermark.setEnable(!o.disabled); |
|
} |
|
} |
|
} |
|
}, |
|
|
|
_focus: function () { |
|
this.content.element.addClass("textarea-editor-focus"); |
|
this._checkWaterMark(); |
|
}, |
|
|
|
_blur: function () { |
|
this.content.element.removeClass("textarea-editor-focus"); |
|
this._checkWaterMark(); |
|
}, |
|
|
|
focus: function () { |
|
this._focus(); |
|
this.content.element.focus(); |
|
}, |
|
|
|
blur: function () { |
|
this._blur(); |
|
this.content.element.blur(); |
|
}, |
|
|
|
getValue: function () { |
|
return this.content.element.val(); |
|
}, |
|
|
|
setValue: function (value) { |
|
this.content.element.val(value); |
|
}, |
|
|
|
setStyle: function (style) { |
|
this.style = style; |
|
this.element.css(style); |
|
this.content.element.css(style) |
|
}, |
|
|
|
getStyle: function () { |
|
return this.style; |
|
}, |
|
|
|
setValid: function (b) { |
|
BI.TextAreaEditor.superclass.setValid.apply(this, arguments); |
|
this.content.setValid(b); |
|
this.watermark && this.watermark.setValid(b); |
|
}, |
|
|
|
setEnable: function (b) { |
|
BI.TextAreaEditor.superclass.setEnable.apply(this, arguments); |
|
this.content.setEnable(b); |
|
this.watermark && this.watermark.setEnable(b); |
|
} |
|
}); |
|
BI.TextAreaEditor.EVENT_CHANGE = "EVENT_CHANGE"; |
|
BI.TextAreaEditor.EVENT_BLUR = "EVENT_BLUR"; |
|
BI.TextAreaEditor.EVENT_FOCUS = "EVENT_FOCUS"; |
|
$.shortcut("bi.textarea_editor", BI.TextAreaEditor);/** |
|
* guy 图标 |
|
* @class BI.Icon |
|
* @extends BI.Single |
|
*/ |
|
BI.Icon = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.Icon.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
tagName: "i", |
|
baseCls: (conf.baseCls || "") + " x-icon b-font horizon-center display-block" |
|
}) |
|
}, |
|
_init: function () { |
|
BI.Icon.superclass._init.apply(this, arguments); |
|
} |
|
}); |
|
$.shortcut("bi.icon", BI.Icon);/** |
|
* @class BI.Iframe |
|
* @extends BI.Single |
|
* @abstract |
|
* Created by GameJian on 2016/3/2. |
|
*/ |
|
BI.Iframe = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.Iframe.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-iframe", |
|
src: "", |
|
width: "100%", |
|
height: "100%" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
var o = this.options; |
|
this.options.element = $("<iframe frameborder='0' src='" + o.src + "'>"); |
|
BI.Iframe.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
setSrc: function (src) { |
|
this.options.src = src; |
|
this.element.attr("src", src); |
|
}, |
|
|
|
getSrc: function () { |
|
return this.options.src; |
|
}, |
|
|
|
getWidth: function () { |
|
return this.options.width |
|
}, |
|
|
|
getHeight: function () { |
|
return this.options.height |
|
} |
|
}); |
|
|
|
$.shortcut("bi.iframe", BI.Iframe);/** |
|
* ͼƬ |
|
* |
|
* Created by GUY on 2016/1/26. |
|
* @class BI.Img |
|
* @extends BI.Single |
|
* @abstract |
|
*/ |
|
BI.Img = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.Img.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-img display-block", |
|
src: "", |
|
width: "100%", |
|
height: "100%" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
var o = this.options; |
|
this.options.element = $("<img src='" + o.src + "'>"); |
|
BI.Img.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
setSrc: function (src) { |
|
this.options.src = src; |
|
this.element.attr("src", src); |
|
}, |
|
|
|
getSrc: function () { |
|
return this.options.src; |
|
} |
|
}); |
|
|
|
$.shortcut("bi.img", BI.Img); |
|
/** |
|
* guy |
|
* @extends BI.Single |
|
* @type {*|void|Object} |
|
*/ |
|
BI.Checkbox = BI.inherit(BI.IconButton, { |
|
_defaultConfig: function() { |
|
var conf = BI.Checkbox.superclass._defaultConfig.apply(this,arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-checkbox check-box-icon", |
|
selected: false, |
|
handler: BI.emptyFn, |
|
width: 16, |
|
height: 16, |
|
iconWidth: 16, |
|
iconHeight: 16 |
|
}) |
|
}, |
|
|
|
_init : function() { |
|
BI.Checkbox.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
doClick: function(){ |
|
BI.Checkbox.superclass.doClick.apply(this, arguments); |
|
if(this.isValid()){ |
|
this.fireEvent(BI.Checkbox.EVENT_CHANGE); |
|
} |
|
} |
|
}); |
|
BI.Checkbox.EVENT_CHANGE = "Checkbox.EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.checkbox", BI.Checkbox);/** |
|
* 文件 |
|
* |
|
* Created by GUY on 2016/1/27. |
|
* @class BI.File |
|
* @extends BI.Single |
|
* @abstract |
|
*/ |
|
(function () { |
|
|
|
/** |
|
* @description normalize input.files. create if not present, add item method if not present |
|
* @param Object generated wrap object |
|
* @return Object the wrap object itself |
|
*/ |
|
var F = (function (item) { |
|
return function (input) { |
|
var files = input.files || [input]; |
|
if (!files.item) { |
|
files.item = item; |
|
} |
|
return files; |
|
}; |
|
})(function (i) { |
|
return this[i]; |
|
}); |
|
|
|
var event = { |
|
|
|
/** |
|
* @description add an event via addEventListener or attachEvent |
|
* @param DOMElement the element to add event |
|
* @param String event name without "on" (e.g. "mouseover") |
|
* @param Function the callback to associate as event |
|
* @return Object noswfupload.event |
|
*/ |
|
add: document.addEventListener ? |
|
function (node, name, callback) { |
|
node.addEventListener(name, callback, false); |
|
return this; |
|
} : |
|
function (node, name, callback) { |
|
node.attachEvent("on" + name, callback); |
|
return this; |
|
}, |
|
|
|
/** |
|
* @description remove an event via removeEventListener or detachEvent |
|
* @param DOMElement the element to remove event |
|
* @param String event name without "on" (e.g. "mouseover") |
|
* @param Function the callback associated as event |
|
* @return Object noswfupload.event |
|
*/ |
|
del: document.removeEventListener ? |
|
function (node, name, callback) { |
|
node.removeEventListener(name, callback, false); |
|
return this; |
|
} : |
|
function (node, name, callback) { |
|
node.detachEvent("on" + name, callback); |
|
return this; |
|
}, |
|
|
|
/** |
|
* @description to block event propagation and prevent event default |
|
* @param void generated event or undefined |
|
* @return Boolean false |
|
*/ |
|
stop: function (e) { |
|
if (!e) { |
|
if (self.event) { |
|
event.returnValue = !(event.cancelBubble = true); |
|
} |
|
} else { |
|
e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true; |
|
e.preventDefault ? e.preventDefault() : e.returnValue = false; |
|
} |
|
; |
|
return false; |
|
} |
|
}; |
|
|
|
var sendFile = (function (toString) { |
|
var multipart = function (boundary, name, file) { |
|
return "--".concat( |
|
boundary, CRLF, |
|
'Content-Disposition: form-data; name="', name, '"; filename="', BI.cjkEncode(file.fileName), '"', CRLF, |
|
"Content-Type: application/octet-stream", CRLF, |
|
CRLF, |
|
file.getAsBinary(), CRLF, |
|
"--", boundary, "--", CRLF |
|
); |
|
}, |
|
isFunction = function (Function) { |
|
return toString.call(Function) === "[object Function]"; |
|
}, |
|
split = "onabort.onerror.onloadstart.onprogress".split("."), |
|
length = split.length, |
|
CRLF = "\r\n", |
|
xhr = this.XMLHttpRequest ? new XMLHttpRequest : new ActiveXObject("Microsoft.XMLHTTP"), |
|
sendFile; |
|
|
|
// FireFox 3+, Safari 4 beta (Chrome 2 beta file is buggy and will not work) |
|
if (xhr.upload || xhr.sendAsBinary) { |
|
sendFile = function (handler, maxSize, width, height) { |
|
if (-1 < maxSize && maxSize < handler.file.fileSize) { |
|
if (isFunction(handler.onerror)) { |
|
handler.onerror(); |
|
} |
|
return; |
|
} |
|
for (var |
|
xhr = new XMLHttpRequest, |
|
upload = xhr.upload || { |
|
addEventListener: function (event, callback) { |
|
this["on" + event] = callback |
|
} |
|
}, |
|
i = 0; |
|
i < length; |
|
i++ |
|
) { |
|
upload.addEventListener( |
|
split[i].substring(2), |
|
(function (event) { |
|
return function (rpe) { |
|
if (isFunction(handler[event])) { |
|
handler[event](rpe, xhr); |
|
} |
|
}; |
|
})(split[i]), |
|
false |
|
); |
|
} |
|
upload.addEventListener( |
|
"load", |
|
function (rpe) { |
|
if (handler.onreadystatechange === false) { |
|
if (isFunction(handler.onload)) { |
|
handler.onload(rpe, xhr); |
|
} |
|
} else { |
|
setTimeout(function () { |
|
if (xhr.readyState === 4) { |
|
if (isFunction(handler.onload)) { |
|
handler.onload(rpe, xhr); |
|
} |
|
} else { |
|
setTimeout(arguments.callee, 15); |
|
} |
|
}, 15); |
|
} |
|
}, |
|
false |
|
); |
|
xhr.open("post", handler.url + '&filename=' + BI.cjkEncode(handler.file.fileName), true); |
|
if (!xhr.upload) { |
|
var rpe = {loaded: 0, total: handler.file.fileSize || handler.file.size, simulation: true}; |
|
rpe.interval = setInterval(function () { |
|
rpe.loaded += 1024 / 4; |
|
if (rpe.total <= rpe.loaded) { |
|
rpe.loaded = rpe.total; |
|
} |
|
upload.onprogress(rpe); |
|
}, 100); |
|
xhr.onabort = function () { |
|
upload.onabort({}); |
|
}; |
|
xhr.onerror = function () { |
|
upload.onerror({}); |
|
}; |
|
xhr.onreadystatechange = function () { |
|
switch (xhr.readyState) { |
|
case 2: |
|
case 3: |
|
if (rpe.total <= rpe.loaded) |
|
rpe.loaded = rpe.total; |
|
upload.onprogress(rpe); |
|
break; |
|
case 4: |
|
clearInterval(rpe.interval); |
|
rpe.interval = 0; |
|
rpe.loaded = rpe.total; |
|
upload.onprogress(rpe); |
|
if (199 < xhr.status && xhr.status < 400) { |
|
upload["onload"]({}); |
|
var attachO = BI.jsonDecode(xhr.responseText); |
|
attachO.filename = BI.cjkDecode(handler.file.fileName); |
|
if (handler.file.type.indexOf('image') != -1) { |
|
attachO.attach_type = "image"; |
|
} |
|
handler.attach_array.push(attachO); |
|
} else { |
|
upload["onerror"]({}); |
|
} |
|
break; |
|
} |
|
}; |
|
upload.onloadstart(rpe); |
|
} else { |
|
xhr.onreadystatechange = function () { |
|
switch (xhr.readyState) { |
|
case 4: |
|
var attachO = BI.jsonDecode(xhr.responseText); |
|
if (handler.file.type.indexOf('image') != -1) { |
|
attachO.attach_type = "image"; |
|
} |
|
attachO.filename = handler.file.fileName; |
|
if (handler.maxlength == 1) { |
|
handler.attach_array[0] = attachO; |
|
// handler.attach_array.push(attachO); |
|
} else { |
|
handler.attach_array.push(attachO); |
|
} |
|
break; |
|
} |
|
} |
|
} |
|
var boundary = "AjaxUploadBoundary" + (new Date).getTime(); |
|
xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary); |
|
if (handler.file.getAsBinary) { |
|
xhr[xhr.sendAsBinary ? "sendAsBinary" : "send"](multipart(boundary, handler.name, handler.file)); |
|
} else { |
|
xhr.setRequestHeader("Content-Type", "multipart/form-data"); |
|
// xhr.setRequestHeader("X-Name", handler.name); |
|
// xhr.setRequestHeader("X-File-Name", handler.file.fileName); |
|
var form = new FormData(); |
|
form.append("FileData", handler.file); |
|
xhr.send(form); |
|
} |
|
return handler; |
|
}; |
|
} |
|
// Internet Explorer, Opera, others |
|
else { |
|
sendFile = function (handler, maxSize, width, height) { |
|
var url = handler.url.concat(-1 === handler.url.indexOf("?") ? "?" : "&", "AjaxUploadFrame=true"), |
|
rpe = { |
|
loaded: 1, total: 100, simulation: true, interval: setInterval(function () { |
|
if (rpe.loaded < rpe.total) |
|
++rpe.loaded; |
|
if (isFunction(handler.onprogress)) |
|
handler.onprogress(rpe, {}); |
|
}, 100) |
|
}, |
|
onload = function () { |
|
iframe.onreadystatechange = iframe.onload = iframe.onerror = null; |
|
form.parentNode.removeChild(form); |
|
form = null; |
|
clearInterval(rpe.interval); |
|
//rpe.loaded = rpe.total; |
|
try { |
|
var responseText = (iframe.contentWindow.document || iframe.contentWindow.contentDocument).body.innerHTML; |
|
var attachO = BI.jsonDecode(responseText); |
|
if (handler.file.type.indexOf('image') != -1) { |
|
attachO.attach_type = "image"; |
|
} |
|
|
|
//attachO.fileSize = responseText.length; |
|
attachO.filename = BI.cjkDecode(handler.file.fileName); |
|
if (handler.maxlength == 1) { |
|
handler.attach_array[0] = attachO; |
|
} else { |
|
handler.attach_array.push(attachO); |
|
} |
|
} catch (e) { |
|
if (isFunction(handler.onerror)) |
|
handler.onerror(rpe, event || window.event); |
|
} |
|
if (isFunction(handler.onload)) |
|
handler.onload(rpe, {responseText: responseText}); |
|
}, |
|
target = ["AjaxUpload", (new Date).getTime(), String(Math.random()).substring(2)].join("_"); |
|
try { // IE < 8 does not accept enctype attribute ... |
|
var form = document.createElement('<form enctype="multipart/form-data"></form>'), |
|
iframe = handler.iframe || (handler.iframe = document.createElement('<iframe id="' + target + '" name="' + target + '" src="' + url + '"></iframe>')); |
|
} catch (e) { |
|
var form = document.createElement('form'), |
|
iframe = handler.iframe || (handler.iframe = document.createElement("iframe")); |
|
form.setAttribute("enctype", "multipart/form-data"); |
|
iframe.setAttribute("name", iframe.id = target); |
|
iframe.setAttribute("src", url); |
|
} |
|
iframe.style.position = "absolute"; |
|
iframe.style.left = iframe.style.top = "-10000px"; |
|
iframe.onload = onload; |
|
iframe.onerror = function (event) { |
|
if (isFunction(handler.onerror)) { |
|
handler.onerror(rpe, event || window.event); |
|
} |
|
}; |
|
iframe.onreadystatechange = function () { |
|
if (/loaded|complete/i.test(iframe.readyState)) { |
|
onload(); |
|
|
|
//wei : todo,将附件信息放到handler.attach |
|
} |
|
else if (isFunction(handler.onloadprogress)) { |
|
if (rpe.loaded < rpe.total) { |
|
++rpe.loaded; |
|
} |
|
handler.onloadprogress(rpe, { |
|
readyState: { |
|
loading: 2, |
|
interactive: 3, |
|
loaded: 4, |
|
complete: 4 |
|
}[iframe.readyState] || 1 |
|
}); |
|
} |
|
}; |
|
form.setAttribute("action", handler.url); |
|
form.setAttribute("target", iframe.id); |
|
form.setAttribute("method", "post"); |
|
form.appendChild(handler.file); |
|
form.style.display = "none"; |
|
if (isFunction(handler.onloadstart)) { |
|
handler.onloadstart(rpe, {}); |
|
} |
|
with (document.body || document.documentElement) { |
|
appendChild(iframe); |
|
appendChild(form); |
|
form.submit(); |
|
} |
|
; |
|
return handler; |
|
}; |
|
} |
|
xhr = null; |
|
return sendFile; |
|
})(Object.prototype.toString); |
|
|
|
var sendFiles = function (handler, maxSize, width, height) { |
|
|
|
var length = handler.files.length, |
|
i = 0, |
|
onload = handler.onload, |
|
onloadstart = handler.onloadstart; |
|
handler.current = 0; |
|
handler.total = 0; |
|
handler.sent = 0; |
|
while (handler.current < length) { |
|
handler.total += (handler.files[handler.current].fileSize || handler.files[handler.current].size); |
|
handler.current++; |
|
} |
|
handler.current = 0; |
|
if (length && handler.files[0].fileSize !== -1) { |
|
handler.file = handler.files[handler.current]; |
|
|
|
sendFile(handler, maxSize, width, height).onload = function (rpe, xhr) { |
|
handler.onloadstart = null; |
|
handler.sent += (handler.files[handler.current].fileSize || handler.files[handler.current].size); |
|
if (++handler.current < length) { |
|
handler.file = handler.files[handler.current]; |
|
sendFile(handler, maxSize, width, height).onload = arguments.callee; |
|
} else if (onload) { |
|
handler.onloadstart = onloadstart; |
|
handler.onload = onload; |
|
handler.onload(rpe, xhr); |
|
} |
|
}; |
|
} else if (length) { |
|
handler.total = length * 100; |
|
handler.file = handler.files[handler.current]; |
|
sendFile(handler, maxSize, width, height).onload = function (rpe, xhr) { |
|
var callee = arguments.callee; |
|
handler.onloadstart = null; |
|
handler.sent += 100; |
|
if (++handler.current < length) { |
|
if (/\b(chrome|safari)\b/i.test(navigator.userAgent)) { |
|
handler.iframe.parentNode.removeChild(handler.iframe); |
|
handler.iframe = null; |
|
} |
|
setTimeout(function () { |
|
handler.file = handler.files[handler.current]; |
|
sendFile(handler, maxSize, width, height).onload = callee; |
|
}, 15); |
|
} else if (onload) { |
|
setTimeout(function () { |
|
handler.iframe.parentNode.removeChild(handler.iframe); |
|
handler.iframe = null; |
|
handler.onloadstart = onloadstart; |
|
handler.onload = onload; |
|
handler.onload(rpe, xhr); |
|
}, 15); |
|
} |
|
}; |
|
} |
|
return handler; |
|
}; |
|
|
|
BI.File = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.File.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-file display-block", |
|
element: "<input type='file'>", |
|
name: "", |
|
url: "", |
|
multiple: true, |
|
accept: "", /**'*.jpg; *.zip'**/ |
|
maxSize: 1024 * 1024 |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
var self = this, o = this.options; |
|
BI.File.superclass._init.apply(this, arguments); |
|
if (o.multiple === true) { |
|
this.element.attr("multiple", "multiple"); |
|
} |
|
this.element.attr("name", o.name || this.getName()); |
|
|
|
BI.nextTick(function () { |
|
// create the noswfupload.wrap Object |
|
// wrap.maxSize 文件大小限制 |
|
// wrap.maxlength 文件个数限制 |
|
var _wrap = self.wrap = self._wrap(self.element[0], o.maxSize); |
|
// fileType could contain whatever text but filter checks *.{extension} |
|
// if present |
|
|
|
// handlers |
|
|
|
_wrap.onloadstart = function (rpe, xhr) { |
|
//BI.Msg.toast("loadstart"); |
|
self.fireEvent(BI.File.EVENT_UPLOADSTART); |
|
}; |
|
|
|
_wrap.onprogress = function (rpe, xhr) { |
|
//BI.Msg.toast("onprogress"); |
|
// percent for each bar |
|
|
|
// fileSize is -1 only if browser does not support file info access |
|
// this if splits recent browsers from others |
|
if (this.file.fileSize !== -1) { |
|
// simulation property indicates when the progress event is fake |
|
if (rpe.simulation) { |
|
|
|
} else { |
|
|
|
} |
|
} else { |
|
// if fileSIze is -1 browser is using an iframe because it does |
|
// not support |
|
// files sent via Ajax (XMLHttpRequest) |
|
// We can still show some information |
|
} |
|
self.fireEvent(BI.File.EVENT_PROGRESS, { |
|
file: this.file, |
|
total: rpe.total, |
|
loaded: rpe.loaded, |
|
simulation: rpe.simulation |
|
}); |
|
}; |
|
|
|
// generated if there is something wrong during upload |
|
_wrap.onerror = function () { |
|
// just inform the user something was wrong |
|
self.fireEvent(BI.File.EVENT_ERROR); |
|
}; |
|
|
|
// generated when every file has been sent (one or more, it does not |
|
// matter) |
|
_wrap.onload = function (rpe, xhr) { |
|
var self_ = this; |
|
// just show everything is fine ... |
|
// ... and after a second reset the component |
|
setTimeout(function () { |
|
self_.clean(); // remove files from list |
|
self_.hide(); // hide progress bars and enable input file |
|
|
|
//BI.Msg.toast("onload"); |
|
self.fireEvent(BI.File.EVENT_UPLOADED); |
|
// enable again the submit button/element |
|
}, 1000); |
|
}; |
|
_wrap.url = o.url ? o.url : BI.servletURL |
|
+ '?op=fr_attach&cmd=ah_upload'; |
|
_wrap.fileType = o.accept; //文件类型限制 |
|
_wrap.attach_array = []; |
|
_wrap.attach_names = []; |
|
_wrap.attachNum = 0; |
|
}); |
|
}, |
|
|
|
_events: function (wrap) { |
|
var self = this; |
|
event.add(wrap.dom.input, "change", function () { |
|
event.del(wrap.dom.input, "change", arguments.callee); |
|
for (var input = wrap.dom.input.cloneNode(true), i = 0, files = F(wrap.dom.input); i < files.length; i++) { |
|
var item = files.item(i); |
|
var tempFile = item.value || item.name; |
|
var value = item.fileName || (item.fileName = tempFile.split("\\").pop()), |
|
ext = -1 !== value.indexOf(".") ? value.split(".").pop().toLowerCase() : "unknown", |
|
size = item.fileSize || item.size; |
|
if (wrap.fileType && -1 === wrap.fileType.indexOf("*." + ext)) { |
|
//文件类型不支持 |
|
BI.Msg.toast(BI.i18nText("BI-Upload_File_Type_Error")); |
|
self.fireEvent(BI.File.EVENT_ERROR, { |
|
errorType: 0, |
|
file: item |
|
}); |
|
} else if (wrap.maxSize !== -1 && size && wrap.maxSize < size) { |
|
//文件大小不支持 |
|
BI.Msg.toast(BI.i18nText("BI-Upload_File_Size_Error")); |
|
self.fireEvent(BI.File.EVENT_ERROR, { |
|
errorType: 1, |
|
file: item |
|
}); |
|
} else { |
|
wrap.files.unshift(item); |
|
//BI.Msg.toast(value); |
|
self.fireEvent(BI.File.EVENT_CHANGE, { |
|
file: item |
|
}); |
|
} |
|
} |
|
input.value = ""; |
|
wrap.dom.input.parentNode.replaceChild(input, wrap.dom.input); |
|
wrap.dom.input = input; |
|
event.add(wrap.dom.input, "change", arguments.callee); |
|
}); |
|
return wrap; |
|
}, |
|
|
|
_wrap: function () { |
|
var self = this, o = this.options; |
|
// be sure input accept multiple files |
|
var input = this.element[0]; |
|
if (o.multiple === true) { |
|
this.element.attr("multiple", "multiple"); |
|
} |
|
input.value = ""; |
|
|
|
// wrap Object |
|
return this._events({ |
|
|
|
// DOM namespace |
|
dom: { |
|
input: input, // input file |
|
disabled: false // internal use, checks input file state |
|
}, |
|
name: input.name, // name to send for each file ($_FILES[{name}] in the server) |
|
// maxSize is the maximum amount of bytes for each file |
|
maxSize: o.maxSize ? o.maxSize >> 0 : -1, |
|
files: [], // file list |
|
|
|
// remove every file from the noswfupload component |
|
clean: function () { |
|
this.files = []; |
|
}, |
|
|
|
// upload one file a time (which make progress possible rather than all files in one shot) |
|
// the handler is an object injected into the wrap one, could be the wrap itself or |
|
// something like {onload:function(){alert("OK")},onerror:function(){alert("Error")}, etc ...} |
|
upload: function (handler) { |
|
if (handler) { |
|
for (var key in handler) { |
|
this[key] = handler[key]; |
|
} |
|
} |
|
sendFiles(this, this.maxSize); |
|
return this; |
|
}, |
|
|
|
// hide progress bar (total + current) and enable files selection |
|
hide: function () { |
|
if (this.dom.disabled) { |
|
this.dom.disabled = false; |
|
this.dom.input.removeAttribute("disabled"); |
|
} |
|
}, |
|
|
|
// show progress bar and disable file selection (used during upload) |
|
// total and current are pixels used to style bars |
|
// totalProp and currentProp are properties to change, "height" by default |
|
show: function (total, current, totalProp, currentProp) { |
|
if (!this.dom.disabled) { |
|
this.dom.disabled = true; |
|
this.dom.input.setAttribute("disabled", "disabled"); |
|
} |
|
} |
|
}); |
|
}, |
|
|
|
select: function () { |
|
$(this.wrap.dom.input).click(); |
|
}, |
|
|
|
upload: function (handler) { |
|
this.wrap.upload(handler); |
|
}, |
|
|
|
getValue: function () { |
|
return this.wrap.attach_array; |
|
}, |
|
|
|
reset: function () { |
|
this.wrap.attach_array = []; |
|
this.wrap.attach_names = []; |
|
this.wrap.attachNum = 0; |
|
}, |
|
|
|
setEnable: function (enable) { |
|
BI.MultiFile.superclass.setEnable.apply(this, arguments); |
|
if (enable === true) { |
|
this.element.attr("disabled", "disabled"); |
|
} else { |
|
this.element.removeAttr("disabled"); |
|
} |
|
} |
|
}); |
|
BI.File.EVENT_CHANGE = "BI.File.EVENT_CHANGE"; |
|
BI.File.EVENT_UPLOADSTART = "EVENT_UPLOADSTART"; |
|
BI.File.EVENT_ERROR = "EVENT_ERROR"; |
|
BI.File.EVENT_PROGRESS = "EVENT_PROGRESS"; |
|
BI.File.EVENT_UPLOADED = "EVENT_UPLOADED"; |
|
$.shortcut("bi.file", BI.File); |
|
})();/** |
|
* guy |
|
* @class BI.Input 一个button和一行数 组成的一行listitem |
|
* @extends BI.Single |
|
* @type {*|void|Object} |
|
*/ |
|
BI.Input = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.Input.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-input display-block", |
|
element: "<input/>", |
|
validationChecker: BI.emptyFn, |
|
quitChecker: BI.emptyFn,//按确定键能否退出编辑 |
|
mouseOut: false, |
|
allowBlank: false |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Input.superclass._init.apply(this, arguments); |
|
var self = this; |
|
var ctrlKey = false; |
|
var inputEventValid = false; |
|
var _keydown = BI.debounce(function (keyCode) { |
|
self.onKeyDown(keyCode, ctrlKey); |
|
self._keydown_ = false; |
|
}, 300); |
|
var _clk = BI.debounce(BI.bind(this._click, this), BI.EVENT_RESPONSE_TIME, true); |
|
this._blurDebounce = BI.debounce(BI.bind(this._blur, this), BI.EVENT_RESPONSE_TIME, true); |
|
this.element |
|
.keydown(function (e) { |
|
inputEventValid = false; |
|
ctrlKey = e.ctrlKey; |
|
self.fireEvent(BI.Input.EVENT_QUICK_DOWN); |
|
}) |
|
.keyup(function (e) { |
|
if (!(inputEventValid && e.keyCode === BI.KeyCode.ENTER)) { |
|
self._keydown_ = true; |
|
_keydown(e.keyCode); |
|
} |
|
}) |
|
.on("input propertychange", function (e) { |
|
inputEventValid = true; |
|
self._keydown_ = true; |
|
_keydown(e.keyCode); |
|
}) |
|
.click(function (e) { |
|
e.stopPropagation(); |
|
_clk(); |
|
}) |
|
.mousedown(function (e) { |
|
self.element.val(self.element.val()); |
|
}) |
|
.focusout(function (e) { |
|
self._blurDebounce(); |
|
}); |
|
}, |
|
|
|
_focus: function () { |
|
this.element.addClass("bi-input-focus"); |
|
this._checkValidationOnValueChange(); |
|
this._isEditing = true; |
|
if (this.getValue() == "") { |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EMPTY, this.getValue(), this); |
|
this.fireEvent(BI.Input.EVENT_EMPTY); |
|
} |
|
this.fireEvent(BI.Input.EVENT_FOCUS); |
|
}, |
|
|
|
_blur: function () { |
|
var self = this; |
|
if (self._keydown_ === true) { |
|
BI.delay(blur, 300); |
|
} else { |
|
blur(); |
|
} |
|
function blur() { |
|
if (!self.isValid() && self.options.quitChecker.apply(self, [BI.trim(self.getValue())]) !== false) { |
|
self.element.val(self._lastValidValue ? self._lastValidValue : ""); |
|
self._checkValidationOnValueChange(); |
|
self._defaultState(); |
|
} |
|
self.element.removeClass("bi-input-focus"); |
|
self._isEditing = false; |
|
self._start = false; |
|
if (self.isValid()) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.CONFIRM, self.getValue(), self); |
|
self.fireEvent(BI.Input.EVENT_CONFIRM); |
|
} |
|
self.fireEvent(BI.Input.EVENT_BLUR); |
|
} |
|
}, |
|
|
|
_click: function () { |
|
if (this._isEditing !== true) { |
|
this._focus(); |
|
this.selectAll(); |
|
this.fireEvent(BI.Input.EVENT_CLICK); |
|
} |
|
}, |
|
|
|
onClick: function () { |
|
this._click(); |
|
}, |
|
|
|
onKeyDown: function (keyCode, ctrlKey) { |
|
if (!this.isValid() || BI.trim(this._lastValidValue) !== BI.trim(this.getValue())) { |
|
this._checkValidationOnValueChange(); |
|
} |
|
if (this.isValid() && BI.trim(this.getValue()) !== "") { |
|
if (BI.trim(this.getValue()) !== this._lastValue && (!this._start || this._lastValue == null || this._lastValue === "") |
|
|| (this._pause === true && !/(\s|\u00A0)$/.test(this.getValue()))) { |
|
this._start = true; |
|
this._pause = false; |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.STARTEDIT, this.getValue(), this); |
|
this.fireEvent(BI.Input.EVENT_START); |
|
} |
|
} |
|
if (ctrlKey === true && keyCode === 86) {//ctrlKey+V |
|
this._valueChange(); |
|
} else { |
|
if (keyCode == BI.KeyCode.ENTER) { |
|
if (this.isValid() || this.options.quitChecker.apply(this, [BI.trim(this.getValue())]) !== false) { |
|
this.blur(); |
|
this.fireEvent(BI.Input.EVENT_ENTER); |
|
} else { |
|
this.fireEvent(BI.Input.EVENT_RESTRICT); |
|
} |
|
} |
|
if (keyCode == BI.KeyCode.SPACE) { |
|
this.fireEvent(BI.Input.EVENT_SPACE); |
|
} |
|
if (keyCode == BI.KeyCode.BACKSPACE && this._lastValue == "") { |
|
this.fireEvent(BI.Input.EVENT_REMOVE); |
|
} |
|
if (keyCode == BI.KeyCode.BACKSPACE || keyCode == BI.KeyCode.DELETE) { |
|
this.fireEvent(BI.Input.EVENT_BACKSPACE); |
|
} |
|
} |
|
this.fireEvent(BI.Input.EVENT_KEY_DOWN); |
|
|
|
if (BI.isEndWithBlank(this.getValue())) { |
|
this._pause = true; |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.PAUSE, "", this); |
|
this.fireEvent(BI.Input.EVENT_PAUSE); |
|
this._defaultState(); |
|
} else if ((keyCode === BI.KeyCode.BACKSPACE || keyCode === BI.KeyCode.DELETE) && |
|
BI.trim(this.getValue()) === "" && (this._lastValue !== null && BI.trim(this._lastValue) !== "")) { |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.STOPEDIT, this.getValue(), this); |
|
this.fireEvent(BI.Input.EVENT_STOP); |
|
this._valueChange(); |
|
} else { |
|
this._valueChange(); |
|
} |
|
}, |
|
|
|
//初始状态 |
|
_defaultState: function () { |
|
if (this.getValue() == "") { |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EMPTY, this.getValue(), this); |
|
this.fireEvent(BI.Input.EVENT_EMPTY); |
|
} |
|
this._lastValue = this.getValue(); |
|
this._lastSubmitValue = null; |
|
}, |
|
|
|
_valueChange: function () { |
|
if (this.isValid() && BI.trim(this.getValue()) !== this._lastSubmitValue) { |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.CHANGE, this.getValue(), this); |
|
this.fireEvent(BI.Input.EVENT_CHANGE); |
|
this._lastSubmitValue = BI.trim(this.getValue()); |
|
} |
|
if (this.getValue() == "") { |
|
this.fireEvent(BI.Controller.EVENT_CHANGE, BI.Events.EMPTY, this.getValue(), this); |
|
this.fireEvent(BI.Input.EVENT_EMPTY); |
|
} |
|
this._lastValue = this.getValue(); |
|
}, |
|
|
|
_checkValidationOnValueChange: function () { |
|
var o = this.options; |
|
var v = this.getValue(); |
|
this.setValid( |
|
(o.allowBlank === true && BI.trim(v) == "") || |
|
(BI.isNotEmptyString(BI.trim(v)) |
|
&& (v === this._lastValidValue || |
|
o.validationChecker.apply(this, [BI.trim(v)]) !== false)) |
|
); |
|
}, |
|
|
|
focus: function () { |
|
if (!this.element.is(":visible")) { |
|
throw new Error("input输入框在不可见下不能focus"); |
|
} |
|
if (!this._isEditing === true) { |
|
this.element.focus(); |
|
this._focus(); |
|
this.selectAll(); |
|
} |
|
}, |
|
|
|
blur: function () { |
|
if (!this.element.is(":visible")) { |
|
throw new Error("input输入框在不可见下不能blur"); |
|
} |
|
if (this._isEditing === true) { |
|
this.element.blur(); |
|
this._blurDebounce(); |
|
} |
|
}, |
|
|
|
selectAll: function () { |
|
if (!this.element.is(":visible")) { |
|
throw new Error("input输入框在不可见下不能select"); |
|
} |
|
this.element.select(); |
|
this._isEditing = true; |
|
}, |
|
|
|
setValue: function (textValue) { |
|
this.element.val(textValue); |
|
BI.nextTick(BI.bind(function () { |
|
this._checkValidationOnValueChange(); |
|
this._defaultState(); |
|
if (this.isValid()) { |
|
this._lastSubmitValue = this.getValue(); |
|
} |
|
}, this)); |
|
}, |
|
|
|
getValue: function () { |
|
return this.element.val() || ""; |
|
}, |
|
|
|
isEditing: function () { |
|
return this._isEditing; |
|
}, |
|
|
|
getLastValidValue: function () { |
|
return this._lastValidValue; |
|
}, |
|
|
|
setValid: function () { |
|
BI.Input.superclass.setValid.apply(this, arguments); |
|
if (this.isValid()) { |
|
this._lastValidValue = this.getValue(); |
|
this.element.removeClass("bi-input-error"); |
|
this.fireEvent(BI.Input.EVENT_VALID, BI.trim(this.getValue()), this); |
|
} else { |
|
if (this._lastValidValue === this.getValue()) { |
|
this._lastValidValue = null; |
|
} |
|
this.element.addClass("bi-input-error"); |
|
this.fireEvent(BI.Input.EVENT_ERROR, BI.trim(this.getValue()), this); |
|
} |
|
}, |
|
|
|
setEnable: function (b) { |
|
BI.Input.superclass.setEnable.apply(this, [b]); |
|
this.element[0].disabled = !b; |
|
} |
|
}); |
|
BI.Input.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
BI.Input.EVENT_FOCUS = "EVENT_FOCUS"; |
|
BI.Input.EVENT_CLICK = "EVENT_CLICK"; |
|
BI.Input.EVENT_BLUR = "EVENT_BLUR"; |
|
BI.Input.EVENT_KEY_DOWN = "EVENT_KEY_DOWN"; |
|
BI.Input.EVENT_QUICK_DOWN = "EVENT_QUICK_DOWN"; |
|
BI.Input.EVENT_SPACE = "EVENT_SPACE"; |
|
BI.Input.EVENT_BACKSPACE = "EVENT_BACKSPACE"; |
|
|
|
BI.Input.EVENT_START = "EVENT_START"; |
|
BI.Input.EVENT_PAUSE = "EVENT_PAUSE"; |
|
BI.Input.EVENT_STOP = "EVENT_STOP"; |
|
BI.Input.EVENT_CONFIRM = "EVENT_CONFIRM"; |
|
BI.Input.EVENT_REMOVE = "EVENT_REMOVE"; |
|
BI.Input.EVENT_EMPTY = "EVENT_EMPTY"; |
|
BI.Input.EVENT_VALID = "EVENT_VALID"; |
|
BI.Input.EVENT_ERROR = "EVENT_ERROR"; |
|
BI.Input.EVENT_ENTER = "EVENT_ENTER"; |
|
BI.Input.EVENT_RESTRICT = "EVENT_RESTRICT"; |
|
$.shortcut("bi.input", BI.Input);/** |
|
* guy |
|
* @extends BI.Single |
|
* @type {*|void|Object} |
|
*/ |
|
BI.Radio = BI.inherit(BI.IconButton, { |
|
_defaultConfig: function() { |
|
var conf = BI.Radio.superclass._defaultConfig.apply(this,arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-radio radio-icon", |
|
selected: false, |
|
handler: BI.emptyFn, |
|
width: 16, |
|
height: 16, |
|
iconWidth: 16, |
|
iconHeight: 16 |
|
}) |
|
}, |
|
|
|
_init : function() { |
|
BI.Radio.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
doClick: function(){ |
|
BI.Radio.superclass.doClick.apply(this, arguments); |
|
if(this.isValid()){ |
|
this.fireEvent(BI.Radio.EVENT_CHANGE); |
|
} |
|
} |
|
}); |
|
BI.Radio.EVENT_CHANGE = "Radio.EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.radio", BI.Radio);/** |
|
* Created by GUY on 2015/6/26. |
|
*/ |
|
|
|
BI.Label = BI.inherit(BI.Single, { |
|
_defaultConfig: function () { |
|
var conf = BI.Label.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-label", |
|
textAlign: "center", |
|
whiteSpace: "nowrap", //normal or nowrap |
|
forceCenter: false, //是否无论如何都要居中, 不考虑超出边界的情况, 在未知宽度和高度时有效 |
|
textWidth: null, |
|
textHeight: null, |
|
hgap: 0, |
|
vgap: 0, |
|
lgap: 0, |
|
rgap: 0, |
|
tgap: 0, |
|
bgap: 0, |
|
text: "", |
|
py: "", |
|
keyword: "" |
|
}) |
|
}, |
|
|
|
_createJson: function () { |
|
var o = this.options; |
|
return { |
|
type: "bi.text", |
|
textAlign: o.textAlign, |
|
whiteSpace: o.whiteSpace, |
|
lineHeight: o.textHeight, |
|
text: o.text, |
|
value: o.value, |
|
py: o.py, |
|
keyword: o.keyword |
|
}; |
|
}, |
|
|
|
_init: function () { |
|
BI.Label.superclass._init.apply(this, arguments); |
|
|
|
if (this.options.textAlign === "center") { |
|
this._createCenterEl(); |
|
} else { |
|
this._createNotCenterEl(); |
|
} |
|
}, |
|
|
|
_createCenterEl: function () { |
|
var o = this.options; |
|
var json = this._createJson(); |
|
if (BI.isNumber(o.width) && o.width > 0) { |
|
if (BI.isNumber(o.textWidth) && o.textWidth > 0) { |
|
if (BI.isNumber(o.height) && o.height > 0) { |
|
var gap = (o.width - o.textWidth) / 2; |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
height: o.height, |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [ |
|
{ |
|
el: (this.text = BI.createWidget(json)), |
|
left: gap + o.hgap + o.lgap, |
|
right: gap + o.hgap + o.rgap, |
|
top: o.vgap + o.tgap, |
|
bottom: o.vgap + o.bgap |
|
} |
|
] |
|
}); |
|
this.element.css({"line-height": o.height + "px"}); |
|
return; |
|
} |
|
json.width = o.textWidth; |
|
BI.createWidget({ |
|
type: "bi.center_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [ |
|
{ |
|
el: (this.text = BI.createWidget(json)) |
|
} |
|
] |
|
}); |
|
return; |
|
} |
|
if (o.whiteSpace == "normal") { |
|
this.text = BI.createWidget(json); |
|
BI.createWidget({ |
|
type: "bi.center_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
element: this, |
|
items: [this.text] |
|
}); |
|
return; |
|
} |
|
if (BI.isNumber(o.height) && o.height > 0) { |
|
this.element.css({ |
|
"line-height": o.height + "px" |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [{ |
|
el: (this.text = BI.createWidget(json)), |
|
left: o.hgap + o.lgap, |
|
right: o.hgap + o.rgap, |
|
top: o.vgap + o.tgap, |
|
bottom: o.vgap + o.bgap |
|
}] |
|
}); |
|
return; |
|
} |
|
json.width = o.width - 2 * o.hgap; |
|
BI.createWidget({ |
|
type: "bi.center_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [{ |
|
el: (this.text = BI.createWidget(json)) |
|
}] |
|
}); |
|
return; |
|
} |
|
if (BI.isNumber(o.textWidth) && o.textWidth > 0) { |
|
json.width = o.textWidth; |
|
BI.createWidget({ |
|
type: "bi.center_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [ |
|
{ |
|
el: (this.text = BI.createWidget(json)) |
|
} |
|
] |
|
}); |
|
return; |
|
} |
|
if (o.whiteSpace == "normal") { |
|
this.text = BI.createWidget(json); |
|
this.text = BI.createWidget({ |
|
type: "bi.center_adapt", |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [this.text] |
|
}); |
|
return; |
|
} |
|
if (BI.isNumber(o.height) && o.height > 0) { |
|
if (BI.isNumber(o.textHeight) && o.textHeight > 0) { |
|
this.element.css({ |
|
"line-height": o.height + "px" |
|
}); |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
height: o.height, |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [{ |
|
el: (this.text = BI.createWidget(json)), |
|
left: o.hgap + o.lgap, |
|
right: o.hgap + o.rgap, |
|
top: o.vgap + o.tgap, |
|
bottom: o.vgap + o.bgap |
|
}] |
|
}); |
|
return; |
|
} |
|
BI.extend(json, { |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap |
|
}); |
|
this.element.css({ |
|
"line-height": o.height + "px" |
|
}); |
|
this.text = BI.createWidget(BI.extend(json, { |
|
element: this |
|
})); |
|
BI.createWidget({ |
|
type: "bi.layout", |
|
element: this.text, |
|
scrollable: o.whiteSpace === "normal" |
|
}); |
|
return; |
|
} |
|
BI.extend(json, { |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap |
|
}); |
|
if (o.forceCenter) { |
|
this.text = BI.createWidget(json); |
|
BI.createWidget({ |
|
type: "bi.center_adapt", |
|
element: this, |
|
items: [this.text] |
|
}); |
|
return; |
|
} |
|
this.text = BI.createWidget(BI.extend(json, { |
|
element: this |
|
})); |
|
BI.createWidget({ |
|
type: "bi.layout", |
|
element: this.text, |
|
scrollable: o.whiteSpace === "normal" |
|
}) |
|
}, |
|
|
|
_createNotCenterEl: function () { |
|
var o = this.options; |
|
var json = this._createJson(); |
|
if (BI.isNumber(o.width) && o.width > 0) { |
|
if (BI.isNumber(o.textWidth) && o.textWidth > 0) { |
|
if (BI.isNumber(o.height) && o.height > 0) { |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
height: o.height, |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [ |
|
{ |
|
el: (this.text = BI.createWidget(json)), |
|
left: o.hgap + o.lgap, |
|
right: o.hgap + o.rgap, |
|
top: o.vgap + o.tgap, |
|
bottom: o.vgap + o.bgap |
|
} |
|
] |
|
}); |
|
this.element.css({"line-height": o.height + "px"}); |
|
return; |
|
} |
|
json.width = o.textWidth; |
|
BI.createWidget({ |
|
type: "bi.vertical_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
element: this, |
|
items: [ |
|
{ |
|
el: (this.text = BI.createWidget(json)) |
|
} |
|
] |
|
}); |
|
return; |
|
} |
|
if (o.whiteSpace == "normal") { |
|
this.text = BI.createWidget(json); |
|
BI.createWidget({ |
|
type: "bi.vertical_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
element: this, |
|
items: [this.text] |
|
}); |
|
return; |
|
} |
|
if (BI.isNumber(o.height) && o.height > 0) { |
|
this.element.css({ |
|
"line-height": o.height + "px" |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [{ |
|
el: (this.text = BI.createWidget(json)), |
|
left: o.hgap + o.lgap, |
|
right: o.hgap + o.rgap, |
|
top: o.vgap + o.tgap, |
|
bottom: o.vgap + o.bgap |
|
}] |
|
}); |
|
return; |
|
} |
|
json.width = o.width - 2 * o.hgap - o.lgap - o.rgap; |
|
BI.createWidget({ |
|
type: "bi.vertical_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
element: this, |
|
items: [{ |
|
el: (this.text = BI.createWidget(json)) |
|
}] |
|
}); |
|
return; |
|
} |
|
if (BI.isNumber(o.textWidth) && o.textWidth > 0) { |
|
json.width = o.textWidth; |
|
BI.createWidget({ |
|
type: "bi.vertical_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
element: this, |
|
items: [ |
|
{ |
|
el: (this.text = BI.createWidget(json)) |
|
} |
|
] |
|
}); |
|
return; |
|
} |
|
if (o.whiteSpace == "normal") { |
|
this.text = BI.createWidget(json) |
|
this.text = BI.createWidget({ |
|
type: "bi.vertical_adapt", |
|
scrollable: o.whiteSpace === "normal", |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap, |
|
element: this, |
|
items: [this.text] |
|
}); |
|
return; |
|
} |
|
if (BI.isNumber(o.height) && o.height > 0) { |
|
if (BI.isNumber(o.textHeight) && o.textHeight > 0) { |
|
this.element.css({ |
|
"line-height": o.height + "px" |
|
}); |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
height: o.height, |
|
scrollable: o.whiteSpace === "normal", |
|
element: this, |
|
items: [{ |
|
el: (this.text = BI.createWidget(json)), |
|
left: o.hgap + o.lgap, |
|
right: o.hgap + o.rgap, |
|
top: o.vgap + o.tgap, |
|
bottom: o.vgap + o.bgap |
|
}] |
|
}); |
|
return; |
|
} |
|
BI.extend(json, { |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap |
|
}); |
|
this.element.css({ |
|
"line-height": o.height + "px" |
|
}); |
|
this.text = BI.createWidget(BI.extend(json, { |
|
element: this |
|
})); |
|
BI.createWidget({ |
|
type: "bi.layout", |
|
element: this.text, |
|
scrollable: o.whiteSpace === "normal" |
|
}); |
|
return; |
|
} |
|
BI.extend(json, { |
|
hgap: o.hgap, |
|
vgap: o.vgap, |
|
lgap: o.lgap, |
|
rgap: o.rgap, |
|
tgap: o.tgap, |
|
bgap: o.bgap |
|
}); |
|
if (o.forceCenter) { |
|
this.text = BI.createWidget(json); |
|
BI.createWidget({ |
|
type: "bi.vertical_adapt", |
|
element: this, |
|
items: [this.text] |
|
}); |
|
return; |
|
} |
|
this.text = BI.createWidget(BI.extend(json, { |
|
element: this |
|
})); |
|
BI.createWidget({ |
|
type: "bi.layout", |
|
element: this.text, |
|
scrollable: o.whiteSpace === "normal" |
|
}) |
|
}, |
|
|
|
doRedMark: function () { |
|
this.text.doRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
unRedMark: function () { |
|
this.text.unRedMark.apply(this.text, arguments); |
|
}, |
|
|
|
doHighLight: function () { |
|
this.text.doHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
unHighLight: function () { |
|
this.text.unHighLight.apply(this.text, arguments); |
|
}, |
|
|
|
setText: function (v) { |
|
this.options.text = v; |
|
this.text.setText(v); |
|
}, |
|
|
|
getText: function () { |
|
return this.options.text; |
|
}, |
|
|
|
setStyle: function (css) { |
|
this.text.setStyle(css) |
|
}, |
|
|
|
setValue: function (v) { |
|
BI.Label.superclass.setValue.apply(this, arguments); |
|
if (!this.isReadOnly()) { |
|
this.text.setValue(v); |
|
} |
|
}, |
|
|
|
populate: function () { |
|
BI.Label.superclass.populate.apply(this, arguments); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.label", BI.Label);/** |
|
* guy a元素 |
|
* @class BI.Link |
|
* @extends BI.Text |
|
*/ |
|
BI.Link = BI.inherit(BI.Label, { |
|
_defaultConfig: function() { |
|
var conf = BI.Link.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-link", |
|
href: "", |
|
target: "_blank" |
|
}) |
|
}, |
|
|
|
_createJson: function(){ |
|
var o = this.options; |
|
return { |
|
type:"bi.a", |
|
textAlign: o.textAlign, |
|
whiteSpace: o.whiteSpace, |
|
lineHeight: o.textHeight, |
|
text: o.text, |
|
keyword: o.keyword, |
|
value: o.value, |
|
py: o.py, |
|
href: o.href, |
|
target: o.target |
|
}; |
|
}, |
|
|
|
_init : function() { |
|
BI.Link.superclass._init.apply(this, arguments); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.link", BI.Link);/** |
|
* guy |
|
* tip提示 |
|
* zIndex在10亿级别 |
|
* @class BI.Tip |
|
* @extends BI.Single |
|
* @abstract |
|
*/ |
|
BI.Tip = BI.inherit(BI.Single, { |
|
_defaultConfig: function() { |
|
var conf = BI.Link.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-tip", |
|
zIndex: BI.zIndex_tip |
|
}) |
|
}, |
|
|
|
_init : function() { |
|
BI.Tip.superclass._init.apply(this, arguments); |
|
this.element.css({"zIndex": this.options.zIndex}); |
|
} |
|
});/** |
|
* 下拉 |
|
* @class BI.Trigger |
|
* @extends BI.Single |
|
* @abstract |
|
*/ |
|
BI.Trigger = BI.inherit(BI.Single, { |
|
_defaultConfig: function() { |
|
var conf = BI.Trigger.superclass._defaultConfig.apply(this, arguments); |
|
return BI.extend(conf, { |
|
baseCls: (conf.baseCls || "") + " bi-trigger cursor-pointer", |
|
height: 30 |
|
}) |
|
}, |
|
|
|
_init : function() { |
|
BI.Trigger.superclass._init.apply(this, arguments); |
|
}, |
|
|
|
setKey: function(){ |
|
|
|
}, |
|
|
|
getKey: function(){ |
|
|
|
} |
|
});/** |
|
* svg绘图 |
|
* |
|
* Created by GUY on 2015/12/3. |
|
* @class BI.Svg |
|
* @extends BI.Widget |
|
*/ |
|
BI.Svg = BI.inherit(BI.Widget, { |
|
|
|
_defaultConfig: function () { |
|
return BI.extend(BI.Svg.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-svg" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.Svg.superclass._init.apply(this, arguments); |
|
this.paper = Raphael(this.element[0]); |
|
|
|
$(this.paper.canvas).width("100%").height("100%").css({"left": "0", "top": "0"}).appendTo(this.element); |
|
|
|
this.top = this.paper.top; |
|
this.bottom = this.paper.bottom; |
|
this.customAttributes = this.paper.customAttributes; |
|
this.ca = this.paper.ca; |
|
this.raphael = this.paper.raphael; |
|
}, |
|
|
|
add: function () { |
|
return this.paper.add.apply(this.paper, arguments); |
|
}, |
|
|
|
path: function () { |
|
return this.paper.path.apply(this.paper, arguments); |
|
}, |
|
|
|
image: function () { |
|
return this.paper.image.apply(this.paper, arguments); |
|
}, |
|
|
|
rect: function () { |
|
return this.paper.rect.apply(this.paper, arguments); |
|
}, |
|
|
|
circle: function () { |
|
return this.paper.circle.apply(this.paper, arguments); |
|
}, |
|
|
|
ellipse: function () { |
|
return this.paper.ellipse.apply(this.paper, arguments); |
|
}, |
|
|
|
text: function () { |
|
return this.paper.text.apply(this.paper, arguments); |
|
}, |
|
|
|
print: function () { |
|
return this.paper.print.apply(this.paper, arguments); |
|
}, |
|
|
|
|
|
setStart: function () { |
|
return this.paper.setStart.apply(this.paper, arguments); |
|
}, |
|
|
|
setFinish: function () { |
|
return this.paper.setFinish.apply(this.paper, arguments); |
|
}, |
|
|
|
setSize: function () { |
|
return this.paper.setSize.apply(this.paper, arguments); |
|
}, |
|
|
|
setViewBox: function () { |
|
return this.paper.setViewBox.apply(this.paper, arguments); |
|
}, |
|
|
|
|
|
getById: function () { |
|
return this.paper.getById.apply(this.paper, arguments); |
|
}, |
|
|
|
getElementByPoint: function () { |
|
return this.paper.getElementByPoint.apply(this.paper, arguments); |
|
}, |
|
|
|
getElementsByPoint: function () { |
|
return this.paper.getElementsByPoint.apply(this.paper, arguments); |
|
}, |
|
|
|
getFont: function () { |
|
return this.paper.getFont.apply(this.paper, arguments); |
|
}, |
|
|
|
|
|
set: function () { |
|
return this.paper.set.apply(this.paper, arguments); |
|
}, |
|
remove: function () { |
|
return this.paper.remove.apply(this.paper, arguments); |
|
}, |
|
clear: function () { |
|
return this.paper.clear.apply(this.paper, arguments); |
|
} |
|
}); |
|
$.shortcut("bi.svg", BI.Svg);/** |
|
* |
|
* 表格 |
|
* |
|
* Created by GUY on 2015/9/22. |
|
* @class BI.TableCell |
|
* @extends BI.Single |
|
*/ |
|
BI.TableCell = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.TableCell.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-table-cell", |
|
textAlign: "left", |
|
text: "" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.TableCell.superclass._init.apply(this, arguments); |
|
BI.createWidget({ |
|
type: "bi.label", |
|
element: this, |
|
whiteSpace: "nowrap", |
|
textAlign: this.options.textAlign, |
|
height: this.options.height, |
|
text: this.options.text, |
|
value: this.options.value, |
|
lgap: 5 |
|
}) |
|
} |
|
}); |
|
|
|
$.shortcut("bi.table_cell", BI.TableCell);/** |
|
* |
|
* 表格单元格 |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.CollectionTableCell |
|
* @extends BI.Widget |
|
*/ |
|
BI.CollectionTableCell = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.CollectionTableCell.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-collection-table-cell", |
|
width: 0, |
|
height: 0, |
|
_left: 0, |
|
_top: 0, |
|
cell: {} |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.CollectionTableCell.superclass._init.apply(this, arguments); |
|
var o = this.options; |
|
this.cell = BI.createWidget(BI.extend({ |
|
type: "bi.label" |
|
}, o.cell, { |
|
cls: (o.cell.cls || "") + "collection-table-cell-wrapper", |
|
width: o.width - (o._left === 0 ? 1 : 0) - 1, |
|
height: o.height - (o._top === 0 ? 1 : 0) - 1 |
|
})); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.cell, |
|
left: 0, |
|
right: 0, |
|
top: 0, |
|
bottom: 0 |
|
}] |
|
}); |
|
}, |
|
|
|
setWidth: function (width) { |
|
BI.CollectionTableCell.superclass.setWidth.apply(this, arguments); |
|
var o = this.options; |
|
this.cell.setWidth(o.width - (o._left === 0 ? 1 : 0) - 1); |
|
}, |
|
|
|
setHeight: function (height) { |
|
BI.CollectionTableCell.superclass.setHeight.apply(this, arguments); |
|
var o = this.options; |
|
this.cell.setHeight(o.height - (o._top === 0 ? 1 : 0) - 1); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.collection_table_cell", BI.CollectionTableCell);/** |
|
* CollectionTable |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.CollectionTable |
|
* @extends BI.Widget |
|
*/ |
|
BI.CollectionTable = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.CollectionTable.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-collection-table", |
|
headerRowSize: 25, |
|
rowSize: 25, |
|
columnSize: [], |
|
isNeedFreeze: false, |
|
freezeCols: [], |
|
isNeedMerge: false, |
|
mergeCols: [], |
|
mergeRule: BI.emptyFn, |
|
header: [], |
|
items: [], |
|
regionColumnSize: [] |
|
}); |
|
}, |
|
|
|
_init: function () { |
|
BI.CollectionTable.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this._width = 0; |
|
this._height = 0; |
|
this._scrollBarSize = BI.DOM.getScrollWidth(); |
|
this.topLeftCollection = BI.createWidget({ |
|
type: "bi.collection_view", |
|
cellSizeAndPositionGetter: function (index) { |
|
return self.topLeftItems[index]; |
|
} |
|
}); |
|
this.topLeftCollection.on(BI.Collection.EVENT_SCROLL, function (scroll) { |
|
self.bottomLeftCollection.setScrollLeft(scroll.scrollLeft); |
|
self._populateScrollbar(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.topRightCollection = BI.createWidget({ |
|
type: "bi.collection_view", |
|
cellSizeAndPositionGetter: function (index) { |
|
return self.topRightItems[index]; |
|
} |
|
}); |
|
this.topRightCollection.on(BI.Collection.EVENT_SCROLL, function (scroll) { |
|
self.bottomRightCollection.setScrollLeft(scroll.scrollLeft); |
|
self._populateScrollbar(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.bottomLeftCollection = BI.createWidget({ |
|
type: "bi.collection_view", |
|
cellSizeAndPositionGetter: function (index) { |
|
return self.bottomLeftItems[index]; |
|
} |
|
}); |
|
this.bottomLeftCollection.on(BI.Collection.EVENT_SCROLL, function (scroll) { |
|
self.bottomRightCollection.setScrollTop(scroll.scrollTop); |
|
self.topLeftCollection.setScrollLeft(scroll.scrollLeft); |
|
self._populateScrollbar(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.bottomRightCollection = BI.createWidget({ |
|
type: "bi.collection_view", |
|
cellSizeAndPositionGetter: function (index) { |
|
return self.bottomRightItems[index]; |
|
} |
|
}); |
|
this.bottomRightCollection.on(BI.Collection.EVENT_SCROLL, function (scroll) { |
|
self.bottomLeftCollection.setScrollTop(scroll.scrollTop); |
|
self.topRightCollection.setScrollLeft(scroll.scrollLeft); |
|
self._populateScrollbar(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.topLeft = BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: false, |
|
scrolly: false, |
|
items: [this.topLeftCollection] |
|
}); |
|
this.topRight = BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: false, |
|
scrolly: false, |
|
items: [this.topRightCollection] |
|
}); |
|
this.bottomLeft = BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: false, |
|
scrolly: false, |
|
items: [this.bottomLeftCollection] |
|
}); |
|
this.bottomRight = BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: false, |
|
scrolly: false, |
|
items: [this.bottomRightCollection] |
|
}); |
|
this.contextLayout = BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.topLeft, |
|
top: 0, |
|
left: 0 |
|
}, { |
|
el: this.topRight, |
|
top: 0 |
|
}, { |
|
el: this.bottomLeft, |
|
left: 0 |
|
}, { |
|
el: this.bottomRight |
|
}] |
|
}); |
|
|
|
this.topScrollbar = BI.createWidget({ |
|
type: "bi.grid_table_scrollbar", |
|
width: BI.GridTableScrollbar.SIZE |
|
}); |
|
this.topScrollbar.on(BI.GridTableScrollbar.EVENT_SCROLL, function (scrollTop) { |
|
self.bottomLeftCollection.setScrollTop(scrollTop); |
|
self.bottomRightCollection.setScrollTop(scrollTop); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.leftScrollbar = BI.createWidget({ |
|
type: "bi.grid_table_horizontal_scrollbar", |
|
height: BI.GridTableScrollbar.SIZE |
|
}); |
|
this.leftScrollbar.on(BI.GridTableScrollbar.EVENT_SCROLL, function (scrollLeft) { |
|
self.topLeftCollection.setScrollLeft(scrollLeft); |
|
self.bottomLeftCollection.setScrollLeft(scrollLeft); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.rightScrollbar = BI.createWidget({ |
|
type: "bi.grid_table_horizontal_scrollbar", |
|
height: BI.GridTableScrollbar.SIZE |
|
}); |
|
this.rightScrollbar.on(BI.GridTableScrollbar.EVENT_SCROLL, function (scrollLeft) { |
|
self.topRightCollection.setScrollLeft(scrollLeft); |
|
self.bottomRightCollection.setScrollLeft(scrollLeft); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.scrollBarLayout = BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.topScrollbar, |
|
right: 0, |
|
top: 0 |
|
}, { |
|
el: this.leftScrollbar, |
|
left: 0 |
|
}, { |
|
el: this.rightScrollbar, |
|
}] |
|
}); |
|
this._width = o.width - BI.GridTableScrollbar.SIZE; |
|
this._height = o.height - BI.GridTableScrollbar.SIZE; |
|
if (o.items.length > 0 || o.header.length < 0) { |
|
this._digest(); |
|
this._populate(); |
|
} |
|
}, |
|
|
|
_getFreezeColLength: function () { |
|
return this.options.isNeedFreeze ? this.options.freezeCols.length : 0; |
|
}, |
|
|
|
_populateScrollbar: function () { |
|
var o = this.options; |
|
var regionSize = this.getRegionSize(), totalLeftColumnSize = 0, totalRightColumnSize = 0, totalColumnSize = 0, summaryColumnSizeArray = [], totalRowSize = o.items.length * o.rowSize; |
|
var freezeColLength = this._getFreezeColLength(); |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.isNeedFreeze === true && o.freezeCols.contains(i)) { |
|
totalLeftColumnSize += size; |
|
} else { |
|
totalRightColumnSize += size; |
|
} |
|
totalColumnSize += size; |
|
if (i === 0) { |
|
summaryColumnSizeArray[i] = size; |
|
} else { |
|
summaryColumnSizeArray[i] = summaryColumnSizeArray[i - 1] + size; |
|
} |
|
}); |
|
this.topScrollbar.setContentSize(o.items.length * o.rowSize); |
|
this.topScrollbar.setSize(this._height - o.header.length * o.headerRowSize); |
|
this.topScrollbar.setPosition(this.bottomRightCollection.getScrollTop()); |
|
this.topScrollbar.populate(); |
|
|
|
this.leftScrollbar.setContentSize(totalLeftColumnSize); |
|
this.leftScrollbar.setSize(regionSize); |
|
this.leftScrollbar.setPosition(this.bottomLeftCollection.getScrollLeft()); |
|
this.leftScrollbar.populate(); |
|
|
|
this.rightScrollbar.setContentSize(totalRightColumnSize); |
|
this.rightScrollbar.setSize(this._width - regionSize); |
|
this.rightScrollbar.setPosition(this.bottomRightCollection.getScrollLeft()); |
|
this.rightScrollbar.populate(); |
|
|
|
var items = this.scrollBarLayout.attr("items"); |
|
items[0].top = o.header.length * o.headerRowSize; |
|
items[1].top = this._height; |
|
items[2].top = this._height; |
|
items[2].left = regionSize; |
|
this.scrollBarLayout.attr("items", items); |
|
this.scrollBarLayout.resize(); |
|
}, |
|
|
|
_populateTable: function () { |
|
var self = this, o = this.options; |
|
var regionSize = this.getRegionSize(), totalLeftColumnSize = 0, totalRightColumnSize = 0, totalColumnSize = 0, summaryColumnSizeArray = [], totalRowSize = o.items.length * o.rowSize; |
|
var freezeColLength = this._getFreezeColLength(); |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.isNeedFreeze === true && o.freezeCols.contains(i)) { |
|
totalLeftColumnSize += size; |
|
} else { |
|
totalRightColumnSize += size; |
|
} |
|
totalColumnSize += size; |
|
if (i === 0) { |
|
summaryColumnSizeArray[i] = size; |
|
} else { |
|
summaryColumnSizeArray[i] = summaryColumnSizeArray[i - 1] + size; |
|
} |
|
}); |
|
|
|
var tlw = regionSize; |
|
var tlh = regionSize >= summaryColumnSizeArray[freezeColLength - 1] ? (o.header.length * o.headerRowSize) : (o.header.length * o.headerRowSize + this._scrollBarSize); |
|
var trw = this._width - regionSize; |
|
var trh = (this._width - regionSize >= totalColumnSize - (summaryColumnSizeArray[freezeColLength - 1] || 0)) ? (o.header.length * o.headerRowSize) : (o.header.length * o.headerRowSize + this._scrollBarSize); |
|
var blw = (this._height - o.header.length * o.headerRowSize >= totalRowSize) ? regionSize : (regionSize + this._scrollBarSize); |
|
var blh = (regionSize >= (summaryColumnSizeArray[freezeColLength - 1] || 0)) ? (this._height - o.header.length * o.headerRowSize) : (this._height - o.header.length * o.headerRowSize + this._scrollBarSize); |
|
var brw = (this._height - o.header.length * o.headerRowSize >= totalRowSize) ? (this._width - regionSize) : (this._width - regionSize + this._scrollBarSize); |
|
var brh = (this._width - regionSize >= totalColumnSize - (summaryColumnSizeArray[freezeColLength - 1] || 0)) ? (this._height - o.header.length * o.headerRowSize) : (this._height - o.header.length * o.headerRowSize + this._scrollBarSize); |
|
|
|
var otlw = regionSize; |
|
var otlh = o.header.length * o.headerRowSize; |
|
var otrw = this._width - regionSize; |
|
var otrh = o.header.length * o.headerRowSize; |
|
var oblw = regionSize; |
|
var oblh = this._height - o.header.length * o.headerRowSize; |
|
var obrw = this._width - regionSize; |
|
var obrh = this._height - o.header.length * o.headerRowSize; |
|
|
|
var digest = function (w, h, tw, th, el) { |
|
if (w >= tw && h >= th) { |
|
el.element.css({ |
|
overflow: "hidden", |
|
overflowX: "hidden", |
|
overflowY: "hidden" |
|
}) |
|
} else if (w >= tw) { |
|
el.element.css({ |
|
overflow: "hidden", |
|
overflowX: "hidden", |
|
overflowY: "auto" |
|
}) |
|
} else if (h >= th) { |
|
el.element.css({ |
|
overflow: "hidden", |
|
overflowX: "auto", |
|
overflowY: "hidden" |
|
}) |
|
} else { |
|
el.element.css({ |
|
overflow: "auto", |
|
overflowX: "auto", |
|
overflowY: "auto" |
|
}) |
|
} |
|
}; |
|
|
|
this.topLeft.setWidth(otlw); |
|
this.topLeft.setHeight(otlh); |
|
this.topRight.setWidth(otrw); |
|
this.topRight.setHeight(otrh); |
|
this.bottomLeft.setWidth(oblw); |
|
this.bottomLeft.setHeight(oblh); |
|
this.bottomRight.setWidth(obrw); |
|
this.bottomRight.setHeight(obrh); |
|
|
|
this.topLeftCollection.setWidth(tlw); |
|
this.topLeftCollection.setHeight(tlh); |
|
this.topRightCollection.setWidth(trw); |
|
this.topRightCollection.setHeight(trh); |
|
this.bottomLeftCollection.setWidth(blw); |
|
this.bottomLeftCollection.setHeight(blh); |
|
this.bottomRightCollection.setWidth(brw); |
|
this.bottomRightCollection.setHeight(brh); |
|
|
|
digest(tlw, tlh, totalLeftColumnSize, o.header.length * o.headerRowSize, this.topLeftCollection); |
|
digest(trw, trh, totalRightColumnSize, o.header.length * o.headerRowSize, this.topRightCollection); |
|
digest(blw, blh, totalLeftColumnSize, o.items.length * o.rowSize, this.bottomLeftCollection); |
|
digest(brw, brh, totalRightColumnSize, o.items.length * o.rowSize, this.bottomRightCollection); |
|
|
|
var items = this.contextLayout.attr("items"); |
|
items[1].left = regionSize; |
|
items[2].top = o.header.length * o.headerRowSize; |
|
items[3].left = regionSize; |
|
items[3].top = o.header.length * o.headerRowSize; |
|
this.contextLayout.attr("items", items); |
|
this.contextLayout.resize(); |
|
|
|
var leftHeader = [], rightHeader = [], leftItems = [], rightItems = []; |
|
var run = function (positions, items, rendered) { |
|
BI.each(positions, function (i, item) { |
|
var cell = { |
|
type: "bi.collection_table_cell", |
|
cell: items[item.row][item.col] |
|
}; |
|
rendered.push(cell); |
|
}); |
|
}; |
|
run(this.topLeftItems, o.header, leftHeader); |
|
run(this.topRightItems, o.header, rightHeader); |
|
run(this.bottomLeftItems, o.items, leftItems); |
|
run(this.bottomRightItems, o.items, rightItems); |
|
|
|
this.topLeftCollection.populate(leftHeader); |
|
this.topRightCollection.populate(rightHeader); |
|
this.bottomLeftCollection.populate(leftItems); |
|
this.bottomRightCollection.populate(rightItems); |
|
}, |
|
|
|
_digest: function () { |
|
var o = this.options; |
|
var freezeColLength = this._getFreezeColLength(); |
|
this.topLeftItems = this._serialize(o.header, 0, freezeColLength, o.headerRowSize, o.columnSize, o.mergeCols); |
|
this.topRightItems = this._serialize(o.header, freezeColLength, o.columnSize.length, o.headerRowSize, o.columnSize, true); |
|
this.bottomLeftItems = this._serialize(o.items, 0, freezeColLength, o.rowSize, o.columnSize, o.mergeCols); |
|
this.bottomRightItems = this._serialize(o.items, freezeColLength, o.columnSize.length, o.rowSize, o.columnSize, o.mergeCols); |
|
}, |
|
|
|
_serialize: function (items, startCol, endCol, rowHeight, columnSize, mergeCols) { |
|
var self = this, o = this.options; |
|
var result = [], cache = {}, preCol = {}, preRow = {}, map = {}; |
|
var summaryColumnSize = []; |
|
for (var i = startCol; i < endCol; i++) { |
|
if (i === startCol) { |
|
summaryColumnSize[i] = columnSize[i]; |
|
} else { |
|
summaryColumnSize[i] = summaryColumnSize[i - 1] + columnSize[i]; |
|
} |
|
} |
|
var mergeRow = function (i, j) { |
|
preCol[j]._height += rowHeight; |
|
preCol[j].__mergeRows.push(i); |
|
}; |
|
|
|
var mergeCol = function (i, j) { |
|
preRow[i]._width += columnSize[j]; |
|
preRow[i].__mergeCols.push(j); |
|
}; |
|
|
|
var createOneEl = function (r, c) { |
|
var width = columnSize[c]; |
|
var height = rowHeight; |
|
map[r][c]._row = r; |
|
map[r][c]._col = c; |
|
map[r][c]._width = width; |
|
map[r][c]._height = height; |
|
preCol[c] = map[r][c]; |
|
preCol[c].__mergeRows = [r]; |
|
preRow[r] = map[r][c]; |
|
preRow[r].__mergeCols = [c]; |
|
|
|
result.push({ |
|
x: summaryColumnSize[c] - columnSize[c], |
|
y: +r * rowHeight, |
|
item: map[r][c] |
|
}); |
|
}; |
|
|
|
BI.each(items, function (i, cols) { |
|
for (var j = startCol; j < endCol; j++) { |
|
if (!cache[i]) { |
|
cache[i] = {}; |
|
} |
|
if (!map[i]) { |
|
map[i] = {}; |
|
} |
|
cache[i][j] = cols[j]; |
|
map[i][j] = {}; |
|
if (mergeCols === true || mergeCols.indexOf(j) > -1) { |
|
if (i === 0 && j === startCol) { |
|
createOneEl(0, startCol); |
|
} else if (j === startCol && i > 0) { |
|
var isNeedMergeRow = o.mergeRule(cache[i][j], cache[i - 1][j]); |
|
if (isNeedMergeRow === true) { |
|
mergeRow(i, j); |
|
preRow[i] = preCol[j]; |
|
} else { |
|
createOneEl(i, j); |
|
} |
|
} else if (i === 0 && j > startCol) { |
|
var isNeedMergeCol = o.mergeRule(cache[i][j], cache[i][j - 1]); |
|
if (isNeedMergeCol === true) { |
|
mergeCol(i, j); |
|
preCol[j] = preRow[i]; |
|
} else { |
|
createOneEl(i, j); |
|
} |
|
} else { |
|
var isNeedMergeRow = o.mergeRule(cache[i][j], cache[i - 1][j]); |
|
var isNeedMergeCol = o.mergeRule(cache[i][j], cache[i][j - 1]); |
|
if (isNeedMergeCol && isNeedMergeRow) { |
|
continue; |
|
//mergeRow(i, j);//优先合并列 |
|
} |
|
if (isNeedMergeCol) { |
|
mergeCol(i, j); |
|
} |
|
if (isNeedMergeRow) { |
|
mergeRow(i, j); |
|
} |
|
if (!isNeedMergeCol && !isNeedMergeRow) { |
|
createOneEl(i, j); |
|
} |
|
} |
|
} else { |
|
createOneEl(i, j); |
|
} |
|
} |
|
}); |
|
return BI.map(result, function (i, item) { |
|
return { |
|
x: item.x, |
|
y: item.y, |
|
row: item.item._row, |
|
col: item.item._col, |
|
width: item.item._width, |
|
height: item.item._height |
|
} |
|
}); |
|
}, |
|
|
|
_populate: function () { |
|
if (this._width <= 0 || this._height <= 0) { |
|
return; |
|
} |
|
if (this._isNeedDigest === true) { |
|
this._digest(); |
|
} |
|
this._populateTable(); |
|
this._populateScrollbar(); |
|
}, |
|
|
|
getRegionSize: function () { |
|
var o = this.options; |
|
var regionSize = o.regionColumnSize[0] || 0; |
|
if (o.isNeedFreeze === false || o.freezeCols.length === 0) { |
|
return 0; |
|
} |
|
if (!regionSize) { |
|
BI.each(o.freezeCols, function (i, col) { |
|
regionSize += o.columnSize[col]; |
|
}); |
|
} |
|
return regionSize; |
|
}, |
|
|
|
setVerticalScroll: function (scrollTop) { |
|
this.bottomLeftCollection.setScrollTop(scrollTop); |
|
this.bottomRightCollection.setScrollTop(scrollTop); |
|
}, |
|
|
|
setLeftHorizontalScroll: function (scrollLeft) { |
|
this.topLeftCollection.setScrollLeft(scrollLeft); |
|
this.bottomLeftCollection.setScrollLeft(scrollLeft); |
|
}, |
|
|
|
setRightHorizontalScroll: function (scrollLeft) { |
|
this.topRightCollection.setScrollLeft(scrollLeft); |
|
this.bottomRightCollection.setScrollLeft(scrollLeft); |
|
}, |
|
|
|
getVerticalScroll: function () { |
|
return this.bottomRightCollection.getScrollTop(); |
|
}, |
|
|
|
getLeftHorizontalScroll: function () { |
|
return this.bottomLeftCollection.getScrollLeft(); |
|
}, |
|
|
|
getRightHorizontalScroll: function () { |
|
return this.bottomRightCollection.getScrollLeft(); |
|
}, |
|
|
|
setWidth: function (width) { |
|
BI.CollectionTable.superclass.setWidth.apply(this, arguments); |
|
this._width = this.options.width - BI.GridTableScrollbar.SIZE; |
|
}, |
|
|
|
setHeight: function (height) { |
|
BI.CollectionTable.superclass.setHeight.apply(this, arguments); |
|
this._height = this.options.height - BI.GridTableScrollbar.SIZE; |
|
}, |
|
|
|
setColumnSize: function (columnSize) { |
|
this._isNeedDigest = true; |
|
this.options.columnSize = columnSize; |
|
}, |
|
|
|
setRegionColumnSize: function (regionColumnSize) { |
|
this._isNeedDigest = true; |
|
this.options.regionColumnSize = regionColumnSize; |
|
}, |
|
|
|
getColumnSize: function () { |
|
return this.options.columnSize; |
|
}, |
|
|
|
getRegionColumnSize: function () { |
|
return this.options.regionColumnSize; |
|
}, |
|
|
|
populate: function (items, header) { |
|
if (items && items !== this.options.items) { |
|
this._isNeedDigest = true; |
|
this.options.items = items; |
|
this._restore(); |
|
} |
|
if (header && header !== this.options.header) { |
|
this._isNeedDigest = true; |
|
this.options.header = header; |
|
this._restore(); |
|
} |
|
this._populate(); |
|
}, |
|
|
|
_restore: function () { |
|
this.topLeftCollection.restore(); |
|
this.topRightCollection.restore(); |
|
this.bottomLeftCollection.restore(); |
|
this.bottomRightCollection.restore(); |
|
}, |
|
|
|
restore: function () { |
|
this._restore(); |
|
} |
|
}); |
|
$.shortcut('bi.collection_table', BI.CollectionTable);/** |
|
* QuickCollectionTable |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.QuickCollectionTable |
|
* @extends BI.CollectionTable |
|
*/ |
|
BI.QuickCollectionTable = BI.inherit(BI.CollectionTable, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.QuickCollectionTable.superclass._defaultConfig.apply(this, arguments), { |
|
extraCls: "bi-quick-collection-table" |
|
}); |
|
}, |
|
|
|
_init: function () { |
|
BI.QuickCollectionTable.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.topLeftCollection.setOverflowX(false); |
|
this.topLeftCollection.setOverflowY(false); |
|
this.topRightCollection.setOverflowX(false); |
|
this.topRightCollection.setOverflowY(false); |
|
this.bottomLeftCollection.setOverflowX(false); |
|
this.bottomLeftCollection.setOverflowY(false); |
|
this.bottomRightCollection.setOverflowX(false); |
|
this.bottomRightCollection.setOverflowY(false); |
|
this._leftWheelHandler = new BI.WheelHandler( |
|
BI.bind(this._onWheelY, this), |
|
BI.bind(this._shouldHandleX, this), |
|
BI.bind(this._shouldHandleY, this) |
|
); |
|
this._rightWheelHandler = new BI.WheelHandler( |
|
BI.bind(this._onWheelY, this), |
|
BI.bind(this._shouldHandleX, this), |
|
BI.bind(this._shouldHandleY, this) |
|
); |
|
this.bottomLeftCollection.element.mousewheel(function (e) { |
|
self._leftWheelHandler.onWheel(e.originalEvent); |
|
}); |
|
this.bottomRightCollection.element.mousewheel(function (e) { |
|
self._rightWheelHandler.onWheel(e.originalEvent); |
|
}); |
|
}, |
|
|
|
_shouldHandleX: function (delta) { |
|
return false; |
|
}, |
|
|
|
_shouldHandleY: function (delta) { |
|
if (delta > 0) { |
|
return this.bottomRightCollection.getScrollTop() < this.bottomRightCollection.getMaxScrollTop(); |
|
} else { |
|
return this.bottomRightCollection.getScrollTop() > 0; |
|
} |
|
}, |
|
|
|
_onWheelY: function (deltaX, deltaY) { |
|
var self = this; |
|
var scrollTop = this.bottomRightCollection.getScrollTop(); |
|
scrollTop += deltaY; |
|
this.bottomLeftCollection.setScrollTop(scrollTop); |
|
this.bottomRightCollection.setScrollTop(scrollTop); |
|
self._populateScrollbar(); |
|
this.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}, |
|
|
|
_populateTable: function () { |
|
var self = this, o = this.options; |
|
var regionSize = this.getRegionSize(), totalLeftColumnSize = 0, totalRightColumnSize = 0, totalColumnSize = 0, summaryColumnSizeArray = [], totalRowSize = o.items.length * o.rowSize; |
|
var freezeColLength = this._getFreezeColLength(); |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.isNeedFreeze === true && o.freezeCols.contains(i)) { |
|
totalLeftColumnSize += size; |
|
} else { |
|
totalRightColumnSize += size; |
|
} |
|
totalColumnSize += size; |
|
if (i === 0) { |
|
summaryColumnSizeArray[i] = size; |
|
} else { |
|
summaryColumnSizeArray[i] = summaryColumnSizeArray[i - 1] + size; |
|
} |
|
}); |
|
|
|
var otlw = regionSize; |
|
var otlh = o.header.length * o.headerRowSize; |
|
var otrw = this._width - regionSize; |
|
var otrh = o.header.length * o.headerRowSize; |
|
var oblw = regionSize; |
|
var oblh = this._height - o.header.length * o.headerRowSize; |
|
var obrw = this._width - regionSize; |
|
var obrh = this._height - o.header.length * o.headerRowSize; |
|
|
|
this.topLeft.setWidth(otlw); |
|
this.topLeft.setHeight(otlh); |
|
this.topRight.setWidth(otrw); |
|
this.topRight.setHeight(otrh); |
|
this.bottomLeft.setWidth(oblw); |
|
this.bottomLeft.setHeight(oblh); |
|
this.bottomRight.setWidth(obrw); |
|
this.bottomRight.setHeight(obrh); |
|
|
|
this.topLeftCollection.setWidth(otlw); |
|
this.topLeftCollection.setHeight(otlh); |
|
this.topRightCollection.setWidth(otrw); |
|
this.topRightCollection.setHeight(otrh); |
|
this.bottomLeftCollection.setWidth(oblw); |
|
this.bottomLeftCollection.setHeight(oblh); |
|
this.bottomRightCollection.setWidth(obrw); |
|
this.bottomRightCollection.setHeight(obrh); |
|
|
|
var items = this.contextLayout.attr("items"); |
|
items[1].left = regionSize; |
|
items[2].top = o.header.length * o.headerRowSize; |
|
items[3].left = regionSize; |
|
items[3].top = o.header.length * o.headerRowSize; |
|
this.contextLayout.attr("items", items); |
|
this.contextLayout.resize(); |
|
|
|
var leftHeader = [], rightHeader = [], leftItems = [], rightItems = []; |
|
var run = function (positions, items, rendered) { |
|
BI.each(positions, function (i, item) { |
|
var cell = { |
|
type: "bi.collection_table_cell", |
|
cell: items[item.row][item.col] |
|
}; |
|
rendered.push(cell); |
|
}); |
|
}; |
|
run(this.topLeftItems, o.header, leftHeader); |
|
run(this.topRightItems, o.header, rightHeader); |
|
run(this.bottomLeftItems, o.items, leftItems); |
|
run(this.bottomRightItems, o.items, rightItems); |
|
|
|
this.topLeftCollection.populate(leftHeader); |
|
this.topRightCollection.populate(rightHeader); |
|
this.bottomLeftCollection.populate(leftItems); |
|
this.bottomRightCollection.populate(rightItems); |
|
} |
|
}); |
|
$.shortcut('bi.quick_collection_table', BI.QuickCollectionTable);/** |
|
* |
|
* 表格单元格 |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.GridTableCell |
|
* @extends BI.Widget |
|
*/ |
|
BI.GridTableCell = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.GridTableCell.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-grid-table-cell", |
|
width: 0, |
|
height: 0, |
|
_rowIndex: 0, |
|
_columnIndex: 0, |
|
_left: 0, |
|
_top: 0, |
|
cell: {} |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.GridTableCell.superclass._init.apply(this, arguments); |
|
var o = this.options; |
|
this.cell = BI.createWidget(BI.extend({ |
|
type: "bi.label" |
|
}, o.cell, { |
|
cls: (o.cell.cls || "") + "grid-table-cell-wrapper", |
|
width: o.width - (o._columnIndex === 0 ? 1 : 0) - 1, |
|
height: o.height - (o._rowIndex === 0 ? 1 : 0) - 1 |
|
})); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.cell, |
|
left: 0, |
|
right: 0, |
|
top: 0, |
|
bottom: 0 |
|
}] |
|
}); |
|
}, |
|
|
|
setWidth: function (width) { |
|
BI.GridTableCell.superclass.setWidth.apply(this, arguments); |
|
var o = this.options; |
|
this.cell.setWidth(o.width - (o._columnIndex === 0 ? 1 : 0) - 1); |
|
}, |
|
|
|
setHeight: function (height) { |
|
BI.GridTableCell.superclass.setHeight.apply(this, arguments); |
|
var o = this.options; |
|
this.cell.setHeight(o.height - (o._rowIndex === 0 ? 1 : 0) - 1); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.grid_table_cell", BI.GridTableCell);/** |
|
* GridTable |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.GridTable |
|
* @extends BI.Widget |
|
*/ |
|
BI.GridTable = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.GridTable.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-grid-table", |
|
headerRowSize: 25, |
|
rowSize: 25, |
|
columnSize: [], |
|
isNeedFreeze: false, |
|
freezeCols: [], |
|
header: [], |
|
items: [], |
|
regionColumnSize: [] |
|
}); |
|
}, |
|
|
|
_init: function () { |
|
BI.GridTable.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this._width = 0; |
|
this._height = 0; |
|
this._scrollBarSize = BI.DOM.getScrollWidth(); |
|
var rowHeightGetter = function () { |
|
return o.rowSize; |
|
}; |
|
var columnLeftWidthGetter = function (index) { |
|
return o.columnSize[index]; |
|
}; |
|
var columnRightWidthGetter = function (index) { |
|
return o.columnSize[index + self._getFreezeColLength()]; |
|
}; |
|
this.topLeftGrid = BI.createWidget({ |
|
type: "bi.grid_view", |
|
rowHeightGetter: rowHeightGetter, |
|
columnWidthGetter: columnLeftWidthGetter, |
|
}); |
|
this.topLeftGrid.on(BI.Grid.EVENT_SCROLL, function (scroll) { |
|
self.bottomLeftGrid.setScrollLeft(scroll.scrollLeft); |
|
self._populateScrollbar(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.topRightGrid = BI.createWidget({ |
|
type: "bi.grid_view", |
|
rowHeightGetter: rowHeightGetter, |
|
columnWidthGetter: columnRightWidthGetter, |
|
}); |
|
this.topRightGrid.on(BI.Grid.EVENT_SCROLL, function (scroll) { |
|
self.bottomRightGrid.setScrollLeft(scroll.scrollLeft); |
|
self._populateScrollbar(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.bottomLeftGrid = BI.createWidget({ |
|
type: "bi.grid_view", |
|
rowHeightGetter: rowHeightGetter, |
|
columnWidthGetter: columnLeftWidthGetter, |
|
}); |
|
this.bottomLeftGrid.on(BI.Grid.EVENT_SCROLL, function (scroll) { |
|
self.bottomRightGrid.setScrollTop(scroll.scrollTop); |
|
self.topLeftGrid.setScrollLeft(scroll.scrollLeft); |
|
self._populateScrollbar(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.bottomRightGrid = BI.createWidget({ |
|
type: "bi.grid_view", |
|
rowHeightGetter: rowHeightGetter, |
|
columnWidthGetter: columnRightWidthGetter, |
|
}); |
|
this.bottomRightGrid.on(BI.Grid.EVENT_SCROLL, function (scroll) { |
|
self.bottomLeftGrid.setScrollTop(scroll.scrollTop); |
|
self.topRightGrid.setScrollLeft(scroll.scrollLeft); |
|
self._populateScrollbar(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.topLeft = BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: false, |
|
scrolly: false, |
|
items: [this.topLeftGrid] |
|
}); |
|
this.topRight = BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: false, |
|
scrolly: false, |
|
items: [this.topRightGrid] |
|
}); |
|
this.bottomLeft = BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: false, |
|
scrolly: false, |
|
items: [this.bottomLeftGrid] |
|
}); |
|
this.bottomRight = BI.createWidget({ |
|
type: "bi.vertical", |
|
scrollable: false, |
|
scrolly: false, |
|
items: [this.bottomRightGrid] |
|
}); |
|
this.contextLayout = BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.topLeft, |
|
top: 0, |
|
left: 0 |
|
}, { |
|
el: this.topRight, |
|
top: 0 |
|
}, { |
|
el: this.bottomLeft, |
|
left: 0 |
|
}, { |
|
el: this.bottomRight |
|
}] |
|
}); |
|
|
|
this.topScrollbar = BI.createWidget({ |
|
type: "bi.grid_table_scrollbar", |
|
width: BI.GridTableScrollbar.SIZE |
|
}); |
|
this.topScrollbar.on(BI.GridTableScrollbar.EVENT_SCROLL, function (scrollTop) { |
|
self.bottomLeftGrid.setScrollTop(scrollTop); |
|
self.bottomRightGrid.setScrollTop(scrollTop); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.leftScrollbar = BI.createWidget({ |
|
type: "bi.grid_table_horizontal_scrollbar", |
|
height: BI.GridTableScrollbar.SIZE |
|
}); |
|
this.leftScrollbar.on(BI.GridTableHorizontalScrollbar.EVENT_SCROLL, function (scrollLeft) { |
|
self.topLeftGrid.setScrollLeft(scrollLeft); |
|
self.bottomLeftGrid.setScrollLeft(scrollLeft); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.rightScrollbar = BI.createWidget({ |
|
type: "bi.grid_table_horizontal_scrollbar", |
|
height: BI.GridTableScrollbar.SIZE |
|
}); |
|
this.rightScrollbar.on(BI.GridTableHorizontalScrollbar.EVENT_SCROLL, function (scrollLeft) { |
|
self.topRightGrid.setScrollLeft(scrollLeft); |
|
self.bottomRightGrid.setScrollLeft(scrollLeft); |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
this.scrollBarLayout = BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.topScrollbar, |
|
right: 0, |
|
top: 0 |
|
}, { |
|
el: this.leftScrollbar, |
|
left: 0 |
|
}, { |
|
el: this.rightScrollbar, |
|
}] |
|
}); |
|
this._width = o.width - BI.GridTableScrollbar.SIZE; |
|
this._height = o.height - BI.GridTableScrollbar.SIZE; |
|
this.header = this._getHeader(); |
|
this.items = this._getItems(); |
|
if (o.items.length > 0) { |
|
this._populate(); |
|
} |
|
}, |
|
|
|
_getFreezeColLength: function () { |
|
return this.options.isNeedFreeze ? this.options.freezeCols.length : 0; |
|
}, |
|
|
|
_populateScrollbar: function () { |
|
var o = this.options; |
|
var regionSize = this.getRegionSize(), totalLeftColumnSize = 0, totalRightColumnSize = 0, totalColumnSize = 0, summaryColumnSizeArray = [], totalRowSize = o.items.length * o.rowSize; |
|
var freezeColLength = this._getFreezeColLength(); |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.isNeedFreeze === true && o.freezeCols.contains(i)) { |
|
totalLeftColumnSize += size; |
|
} else { |
|
totalRightColumnSize += size; |
|
} |
|
totalColumnSize += size; |
|
if (i === 0) { |
|
summaryColumnSizeArray[i] = size; |
|
} else { |
|
summaryColumnSizeArray[i] = summaryColumnSizeArray[i - 1] + size; |
|
} |
|
}); |
|
this.topScrollbar.setContentSize(o.items.length * o.rowSize); |
|
this.topScrollbar.setSize(this._height - o.header.length * o.headerRowSize); |
|
this.topScrollbar.setPosition(Math.min(this.bottomLeftGrid.getScrollTop(), this.bottomRightGrid.getScrollTop())); |
|
this.topScrollbar.populate(); |
|
|
|
this.leftScrollbar.setContentSize(totalLeftColumnSize); |
|
this.leftScrollbar.setSize(regionSize); |
|
this.leftScrollbar.setPosition(this.bottomLeftGrid.getScrollLeft()); |
|
this.leftScrollbar.populate(); |
|
|
|
this.rightScrollbar.setContentSize(totalRightColumnSize); |
|
this.rightScrollbar.setSize(this._width - regionSize); |
|
this.rightScrollbar.setPosition(this.bottomRightGrid.getScrollLeft()); |
|
this.rightScrollbar.populate(); |
|
|
|
var items = this.scrollBarLayout.attr("items"); |
|
items[0].top = o.header.length * o.headerRowSize; |
|
items[1].top = this._height; |
|
items[2].top = this._height; |
|
items[2].left = regionSize; |
|
this.scrollBarLayout.attr("items", items); |
|
this.scrollBarLayout.resize(); |
|
}, |
|
|
|
_getHeader: function () { |
|
var o = this.options; |
|
var freezeColLength = this._getFreezeColLength(); |
|
var leftHeader = [], rightHeader = []; |
|
BI.each(o.header, function (i, cols) { |
|
leftHeader[i] = []; |
|
rightHeader[i] = []; |
|
BI.each(cols, function (j, col) { |
|
var cell = { |
|
type: "bi.grid_table_cell", |
|
cell: col |
|
}; |
|
if (j < freezeColLength) { |
|
leftHeader[i].push(cell); |
|
} else { |
|
rightHeader[i].push(cell); |
|
} |
|
}); |
|
}); |
|
return [leftHeader, rightHeader]; |
|
}, |
|
|
|
_getItems: function () { |
|
var o = this.options; |
|
var freezeColLength = this._getFreezeColLength(); |
|
var leftItems = [], rightItems = []; |
|
BI.each(o.items, function (i, cols) { |
|
leftItems[i] = []; |
|
rightItems[i] = []; |
|
BI.each(cols, function (j, col) { |
|
var cell = { |
|
type: "bi.grid_table_cell", |
|
cell: col |
|
}; |
|
if (j < freezeColLength) { |
|
leftItems[i].push(cell); |
|
} else { |
|
rightItems[i].push(cell); |
|
} |
|
}); |
|
}); |
|
return [leftItems, rightItems]; |
|
}, |
|
|
|
_populateTable: function () { |
|
var self = this, o = this.options; |
|
var regionSize = this.getRegionSize(), totalLeftColumnSize = 0, totalRightColumnSize = 0, totalColumnSize = 0, summaryColumnSizeArray = [], totalRowSize = o.items.length * o.rowSize; |
|
var freezeColLength = this._getFreezeColLength(); |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.isNeedFreeze === true && o.freezeCols.contains(i)) { |
|
totalLeftColumnSize += size; |
|
} else { |
|
totalRightColumnSize += size; |
|
} |
|
totalColumnSize += size; |
|
if (i === 0) { |
|
summaryColumnSizeArray[i] = size; |
|
} else { |
|
summaryColumnSizeArray[i] = summaryColumnSizeArray[i - 1] + size; |
|
} |
|
}); |
|
|
|
var tlw = regionSize; |
|
var tlh = regionSize >= summaryColumnSizeArray[freezeColLength - 1] ? (o.header.length * o.headerRowSize) : (o.header.length * o.headerRowSize + this._scrollBarSize); |
|
var trw = this._width - regionSize; |
|
var trh = (this._width - regionSize >= totalColumnSize - (summaryColumnSizeArray[freezeColLength - 1] || 0)) ? (o.header.length * o.headerRowSize) : (o.header.length * o.headerRowSize + this._scrollBarSize); |
|
var blw = (this._height - o.header.length * o.headerRowSize >= totalRowSize) ? regionSize : (regionSize + this._scrollBarSize); |
|
var blh = (regionSize >= (summaryColumnSizeArray[freezeColLength - 1] || 0)) ? (this._height - o.header.length * o.headerRowSize) : (this._height - o.header.length * o.headerRowSize + this._scrollBarSize); |
|
var brw = (this._height - o.header.length * o.headerRowSize >= totalRowSize) ? (this._width - regionSize) : (this._width - regionSize + this._scrollBarSize); |
|
var brh = (this._width - regionSize >= totalColumnSize - (summaryColumnSizeArray[freezeColLength - 1] || 0)) ? (this._height - o.header.length * o.headerRowSize) : (this._height - o.header.length * o.headerRowSize + this._scrollBarSize); |
|
|
|
var otlw = regionSize; |
|
var otlh = o.header.length * o.headerRowSize; |
|
var otrw = this._width - regionSize; |
|
var otrh = o.header.length * o.headerRowSize; |
|
var oblw = regionSize; |
|
var oblh = this._height - o.header.length * o.headerRowSize; |
|
var obrw = this._width - regionSize; |
|
var obrh = this._height - o.header.length * o.headerRowSize; |
|
|
|
var digest = function (w, h, tw, th, el) { |
|
if (w >= tw && h >= th) { |
|
el.element.css({ |
|
overflow: "hidden", |
|
overflowX: "hidden", |
|
overflowY: "hidden" |
|
}) |
|
} else if (w >= tw) { |
|
el.element.css({ |
|
overflow: "hidden", |
|
overflowX: "hidden", |
|
overflowY: "auto" |
|
}) |
|
} else if (h >= th) { |
|
el.element.css({ |
|
overflow: "hidden", |
|
overflowX: "auto", |
|
overflowY: "hidden" |
|
}) |
|
} else { |
|
el.element.css({ |
|
overflow: "auto", |
|
overflowX: "auto", |
|
overflowY: "auto" |
|
}) |
|
} |
|
}; |
|
|
|
this.topLeft.setWidth(otlw); |
|
this.topLeft.setHeight(otlh); |
|
this.topRight.setWidth(otrw); |
|
this.topRight.setHeight(otrh); |
|
this.bottomLeft.setWidth(oblw); |
|
this.bottomLeft.setHeight(oblh); |
|
this.bottomRight.setWidth(obrw); |
|
this.bottomRight.setHeight(obrh); |
|
|
|
this.topLeftGrid.setWidth(tlw); |
|
this.topLeftGrid.setHeight(tlh); |
|
this.topRightGrid.setWidth(trw); |
|
this.topRightGrid.setHeight(trh); |
|
this.bottomLeftGrid.setWidth(blw); |
|
this.bottomLeftGrid.setHeight(blh); |
|
this.bottomRightGrid.setWidth(brw); |
|
this.bottomRightGrid.setHeight(brh); |
|
|
|
digest(tlw, tlh, totalLeftColumnSize, o.header.length * o.headerRowSize, this.topLeftGrid); |
|
digest(trw, trh, totalRightColumnSize, o.header.length * o.headerRowSize, this.topRightGrid); |
|
digest(blw, blh, totalLeftColumnSize, o.items.length * o.rowSize, this.bottomLeftGrid); |
|
digest(brw, brh, totalRightColumnSize, o.items.length * o.rowSize, this.bottomRightGrid); |
|
|
|
this.topLeftGrid.setEstimatedColumnSize(freezeColLength > 0 ? totalLeftColumnSize / freezeColLength : 0); |
|
this.topLeftGrid.setEstimatedRowSize(o.headerRowSize); |
|
this.topRightGrid.setEstimatedColumnSize(totalRightColumnSize / (o.columnSize.length - freezeColLength)); |
|
this.topRightGrid.setEstimatedRowSize(o.headerRowSize); |
|
this.bottomLeftGrid.setEstimatedColumnSize(freezeColLength > 0 ? totalLeftColumnSize / freezeColLength : 0); |
|
this.bottomLeftGrid.setEstimatedRowSize(o.rowSize); |
|
this.bottomRightGrid.setEstimatedColumnSize(totalRightColumnSize / (o.columnSize.length - freezeColLength)); |
|
this.bottomRightGrid.setEstimatedRowSize(o.rowSize); |
|
|
|
var items = this.contextLayout.attr("items"); |
|
items[1].left = regionSize; |
|
items[2].top = o.header.length * o.headerRowSize; |
|
items[3].left = regionSize; |
|
items[3].top = o.header.length * o.headerRowSize; |
|
this.contextLayout.attr("items", items); |
|
this.contextLayout.resize(); |
|
|
|
this.topLeftGrid.populate(this.header[0]); |
|
this.topRightGrid.populate(this.header[1]); |
|
this.bottomLeftGrid.populate(this.items[0]); |
|
this.bottomRightGrid.populate(this.items[1]); |
|
}, |
|
|
|
_populate: function () { |
|
if (this._width <= 0 || this._height <= 0) { |
|
return; |
|
} |
|
this._populateTable(); |
|
this._populateScrollbar(); |
|
}, |
|
|
|
getRegionSize: function () { |
|
var o = this.options; |
|
var regionSize = o.regionColumnSize[0] || 0; |
|
if (o.isNeedFreeze === false || o.freezeCols.length === 0) { |
|
return 0; |
|
} |
|
if (!regionSize) { |
|
BI.each(o.freezeCols, function (i, col) { |
|
regionSize += o.columnSize[col]; |
|
}); |
|
} |
|
return regionSize; |
|
}, |
|
|
|
setVerticalScroll: function (scrollTop) { |
|
this.bottomLeftGrid.setScrollTop(scrollTop); |
|
this.bottomRightGrid.setScrollTop(scrollTop); |
|
}, |
|
|
|
setLeftHorizontalScroll: function (scrollLeft) { |
|
this.topLeftGrid.setScrollLeft(scrollLeft); |
|
this.bottomLeftGrid.setScrollLeft(scrollLeft); |
|
}, |
|
|
|
setRightHorizontalScroll: function (scrollLeft) { |
|
this.topRightGrid.setScrollLeft(scrollLeft); |
|
this.bottomRightGrid.setScrollLeft(scrollLeft); |
|
}, |
|
|
|
getVerticalScroll: function () { |
|
return this.bottomRightGrid.getScrollTop(); |
|
}, |
|
|
|
getLeftHorizontalScroll: function () { |
|
return this.bottomLeftGrid.getScrollLeft(); |
|
}, |
|
|
|
getRightHorizontalScroll: function () { |
|
return this.bottomRightGrid.getScrollLeft(); |
|
}, |
|
|
|
setWidth: function (width) { |
|
BI.GridTable.superclass.setWidth.apply(this, arguments); |
|
this._width = this.options.width - BI.GridTableScrollbar.SIZE; |
|
}, |
|
|
|
setHeight: function (height) { |
|
BI.GridTable.superclass.setHeight.apply(this, arguments); |
|
this._height = this.options.height - BI.GridTableScrollbar.SIZE; |
|
}, |
|
|
|
setColumnSize: function (columnSize) { |
|
this.options.columnSize = columnSize; |
|
}, |
|
|
|
setRegionColumnSize: function (regionColumnSize) { |
|
this.options.regionColumnSize = regionColumnSize; |
|
}, |
|
|
|
getColumnSize: function () { |
|
return this.options.columnSize; |
|
}, |
|
|
|
getRegionColumnSize: function () { |
|
return this.options.regionColumnSize; |
|
}, |
|
|
|
populate: function (items, header) { |
|
if (items && this.options.items !== items) { |
|
this.options.items = items; |
|
this.items = this._getItems(); |
|
this._restore(); |
|
} |
|
if (header && this.options.header !== header) { |
|
this.options.header = header; |
|
this.header = this._getHeader(); |
|
this._restore(); |
|
} |
|
this._populate(); |
|
}, |
|
|
|
_restore: function () { |
|
this.topLeftGrid.restore(); |
|
this.topRightGrid.restore(); |
|
this.bottomLeftGrid.restore(); |
|
this.bottomRightGrid.restore(); |
|
}, |
|
|
|
restore: function () { |
|
this._restore(); |
|
} |
|
}); |
|
$.shortcut('bi.grid_table', BI.GridTable);/** |
|
* QuickGridTable |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.QuickGridTable |
|
* @extends BI.GridTable |
|
*/ |
|
BI.QuickGridTable = BI.inherit(BI.GridTable, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.QuickGridTable.superclass._defaultConfig.apply(this, arguments), { |
|
extraCls: "bi-quick-grid-table" |
|
}); |
|
}, |
|
|
|
_init: function () { |
|
BI.QuickGridTable.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.topLeftGrid.setOverflowX(false); |
|
this.topLeftGrid.setOverflowY(false); |
|
this.topRightGrid.setOverflowX(false); |
|
this.topRightGrid.setOverflowY(false); |
|
this.bottomLeftGrid.setOverflowX(false); |
|
this.bottomLeftGrid.setOverflowY(false); |
|
this.bottomRightGrid.setOverflowX(false); |
|
this.bottomRightGrid.setOverflowY(false); |
|
this._leftWheelHandler = new BI.WheelHandler( |
|
BI.bind(this._onWheelY, this), |
|
BI.bind(this._shouldHandleX, this), |
|
BI.bind(this._shouldHandleY, this) |
|
); |
|
this._rightWheelHandler = new BI.WheelHandler( |
|
BI.bind(this._onWheelY, this), |
|
BI.bind(this._shouldHandleX, this), |
|
BI.bind(this._shouldHandleY, this) |
|
); |
|
this.bottomLeftGrid.element.mousewheel(function (e) { |
|
self._leftWheelHandler.onWheel(e.originalEvent); |
|
}); |
|
this.bottomRightGrid.element.mousewheel(function (e) { |
|
self._rightWheelHandler.onWheel(e.originalEvent); |
|
}); |
|
}, |
|
|
|
_shouldHandleX: function (delta) { |
|
return false; |
|
}, |
|
|
|
_shouldHandleY: function (delta) { |
|
if (delta > 0) { |
|
return this.bottomRightGrid.getScrollTop() < this.bottomRightGrid.getMaxScrollTop(); |
|
} else { |
|
return this.bottomRightGrid.getScrollTop() > 0; |
|
} |
|
}, |
|
|
|
_onWheelY: function (deltaX, deltaY) { |
|
var self = this; |
|
var scrollTop = this.bottomRightGrid.getScrollTop(); |
|
scrollTop += deltaY; |
|
this.bottomLeftGrid.setScrollTop(scrollTop); |
|
this.bottomRightGrid.setScrollTop(scrollTop); |
|
self._populateScrollbar(); |
|
this.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}, |
|
|
|
_populateTable: function () { |
|
var self = this, o = this.options; |
|
var regionSize = this.getRegionSize(), totalLeftColumnSize = 0, totalRightColumnSize = 0, totalColumnSize = 0, summaryColumnSizeArray = [], totalRowSize = o.items.length * o.rowSize; |
|
var freezeColLength = this._getFreezeColLength(); |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.isNeedFreeze === true && o.freezeCols.contains(i)) { |
|
totalLeftColumnSize += size; |
|
} else { |
|
totalRightColumnSize += size; |
|
} |
|
totalColumnSize += size; |
|
if (i === 0) { |
|
summaryColumnSizeArray[i] = size; |
|
} else { |
|
summaryColumnSizeArray[i] = summaryColumnSizeArray[i - 1] + size; |
|
} |
|
}); |
|
|
|
var otlw = regionSize; |
|
var otlh = o.header.length * o.headerRowSize; |
|
var otrw = this._width - regionSize; |
|
var otrh = o.header.length * o.headerRowSize; |
|
var oblw = regionSize; |
|
var oblh = this._height - o.header.length * o.headerRowSize; |
|
var obrw = this._width - regionSize; |
|
var obrh = this._height - o.header.length * o.headerRowSize; |
|
|
|
this.topLeft.setWidth(otlw); |
|
this.topLeft.setHeight(otlh); |
|
this.topRight.setWidth(otrw); |
|
this.topRight.setHeight(otrh); |
|
this.bottomLeft.setWidth(oblw); |
|
this.bottomLeft.setHeight(oblh); |
|
this.bottomRight.setWidth(obrw); |
|
this.bottomRight.setHeight(obrh); |
|
|
|
this.topLeftGrid.setWidth(otlw); |
|
this.topLeftGrid.setHeight(otlh); |
|
this.topRightGrid.setWidth(otrw); |
|
this.topRightGrid.setHeight(otrh); |
|
this.bottomLeftGrid.setWidth(oblw); |
|
this.bottomLeftGrid.setHeight(oblh); |
|
this.bottomRightGrid.setWidth(obrw); |
|
this.bottomRightGrid.setHeight(obrh); |
|
|
|
this.topLeftGrid.setEstimatedColumnSize(freezeColLength > 0 ? totalLeftColumnSize / freezeColLength : 0); |
|
this.topLeftGrid.setEstimatedRowSize(o.headerRowSize); |
|
this.topRightGrid.setEstimatedColumnSize(totalRightColumnSize / (o.columnSize.length - freezeColLength)); |
|
this.topRightGrid.setEstimatedRowSize(o.headerRowSize); |
|
this.bottomLeftGrid.setEstimatedColumnSize(freezeColLength > 0 ? totalLeftColumnSize / freezeColLength : 0); |
|
this.bottomLeftGrid.setEstimatedRowSize(o.rowSize); |
|
this.bottomRightGrid.setEstimatedColumnSize(totalRightColumnSize / (o.columnSize.length - freezeColLength)); |
|
this.bottomRightGrid.setEstimatedRowSize(o.rowSize); |
|
|
|
var items = this.contextLayout.attr("items"); |
|
items[1].left = regionSize; |
|
items[2].top = o.header.length * o.headerRowSize; |
|
items[3].left = regionSize; |
|
items[3].top = o.header.length * o.headerRowSize; |
|
this.contextLayout.attr("items", items); |
|
this.contextLayout.resize(); |
|
|
|
var leftHeader = [], rightHeader = [], leftItems = [], rightItems = []; |
|
BI.each(o.header, function (i, cols) { |
|
leftHeader[i] = []; |
|
rightHeader[i] = []; |
|
BI.each(cols, function (j, col) { |
|
var cell = { |
|
type: "bi.grid_table_cell", |
|
cell: col |
|
}; |
|
if (j < freezeColLength) { |
|
leftHeader[i].push(cell); |
|
} else { |
|
rightHeader[i].push(cell); |
|
} |
|
}); |
|
}); |
|
BI.each(o.items, function (i, cols) { |
|
leftItems[i] = []; |
|
rightItems[i] = []; |
|
BI.each(cols, function (j, col) { |
|
var cell = { |
|
type: "bi.grid_table_cell", |
|
cell: col |
|
}; |
|
if (j < freezeColLength) { |
|
leftItems[i].push(cell); |
|
} else { |
|
rightItems[i].push(cell); |
|
} |
|
}); |
|
}); |
|
this.topLeftGrid.populate(leftHeader); |
|
this.topRightGrid.populate(rightHeader); |
|
this.bottomLeftGrid.populate(leftItems); |
|
this.bottomRightGrid.populate(rightItems); |
|
} |
|
}); |
|
$.shortcut('bi.quick_grid_table', BI.QuickGridTable);/** |
|
* |
|
* 表格滚动条 |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.GridTableScrollbar |
|
* @extends BI.Widget |
|
*/ |
|
BI.GridTableScrollbar = BI.inherit(BI.Widget, { |
|
_const: { |
|
FACE_MARGIN: 4, |
|
FACE_MARGIN_2: 4 * 2, |
|
FACE_SIZE_MIN: 30, |
|
KEYBOARD_SCROLL_AMOUNT: 40 |
|
}, |
|
_defaultConfig: function () { |
|
return BI.extend(BI.GridTableScrollbar.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "scrollbar-layout-main public-scrollbar-main", |
|
attributes: { |
|
tabIndex: 0 |
|
}, |
|
contentSize: 0, |
|
defaultPosition: 0, |
|
isOpaque: false, |
|
orientation: "vertical", |
|
position: 0, |
|
size: 0 |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.GridTableScrollbar.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.focused = false; |
|
this.isDragging = false; |
|
this.face = BI.createWidget({ |
|
type: "bi.layout", |
|
cls: "scrollbar-layout-face public-scrollbar-face " |
|
+ (this._isHorizontal() ? "scrollbar-layout-face-horizontal" : "scrollbar-layout-face-vertical") |
|
}); |
|
this.contextLayout = BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.face, |
|
left: 0, |
|
top: 0 |
|
}] |
|
}); |
|
var onWheel = o.orientation === 'horizontal' ? this._onWheelX : this._onWheelY; |
|
this._wheelHandler = new BI.WheelHandler( |
|
BI.bind(onWheel, this), |
|
BI.bind(this._shouldHandleX, this), |
|
BI.bind(this._shouldHandleY, this) |
|
); |
|
this._mouseMoveTracker = new BI.MouseMoveTracker( |
|
BI.bind(this._onMouseMove, this), |
|
BI.bind(this._onMouseMoveEnd, this), |
|
document |
|
); |
|
this.element.on("mousedown", BI.bind(this._onMouseDown, this)); |
|
this.element.on("mousewheel", function (e) { |
|
self._wheelHandler.onWheel(e.originalEvent); |
|
}); |
|
this.element.on("keydown", BI.bind(this._onKeyDown, this)); |
|
this.element.on("focus", function () { |
|
self.focused = true; |
|
self._populate(); |
|
}); |
|
this.element.on("blur", function () { |
|
self.focused = false; |
|
self._populate(); |
|
}); |
|
if (this._isHorizontal()) { |
|
this.element.addClass("scrollbar-layout-main-horizontal"); |
|
} else { |
|
this.element.addClass("scrollbar-layout-main-vertical"); |
|
} |
|
this._populate(); |
|
}, |
|
|
|
_isHorizontal: function () { |
|
return this.options.orientation === 'horizontal' |
|
}, |
|
|
|
_getScale: function () { |
|
var o = this.options; |
|
var scale = o.size / o.contentSize; |
|
var faceSize = o.size * scale; |
|
|
|
if (faceSize < this._const.FACE_SIZE_MIN) { |
|
scale = (o.size - this._const.FACE_SIZE_MIN) / (o.contentSize - o.size); |
|
} |
|
return scale; |
|
}, |
|
|
|
_getFaceSize: function () { |
|
var o = this.options; |
|
var scale = o.size / o.contentSize; |
|
var faceSize = o.size * scale; |
|
|
|
if (faceSize < this._const.FACE_SIZE_MIN) { |
|
faceSize = this._const.FACE_SIZE_MIN; |
|
} |
|
return faceSize; |
|
}, |
|
|
|
_shouldHandleX: function (delta) { |
|
return this.options.orientation === 'horizontal' ? |
|
this._shouldHandleChange(delta) : |
|
false; |
|
}, |
|
|
|
_shouldHandleY: function (delta) { |
|
return this.options.orientation !== 'horizontal' ? |
|
this._shouldHandleChange(delta) : |
|
false; |
|
}, |
|
|
|
_shouldHandleChange: function (delta) { |
|
return this.options.position + delta !== this.options.position; |
|
}, |
|
|
|
_onWheelY: function (deltaX, deltaY) { |
|
this._onWheel(deltaY); |
|
}, |
|
|
|
_onWheelX: function (deltaX, deltaY) { |
|
this._onWheel(deltaX); |
|
}, |
|
|
|
_onWheel: function (delta) { |
|
var maxPosition = this.options.contentSize - this.options.size; |
|
this.options.position += delta; |
|
if (this.options.position < 0) { |
|
this.options.position = 0; |
|
} else if (this.options.position > maxPosition) { |
|
this.options.position = maxPosition; |
|
} |
|
this._populate(); |
|
this.fireEvent(BI.GridTableScrollbar.EVENT_SCROLL, this.options.position); |
|
}, |
|
|
|
_onMouseDown: function (e) { |
|
if (e.target !== this.face.element[0]) { |
|
var position = this._isHorizontal() ? e.offsetX : e.offsetY; |
|
position /= this._getScale(); |
|
this.options.position = BI.clamp(position - (this._getFaceSize() * 0.5 / this._getScale()), 0, this.options.contentSize - this.options.size); |
|
this._populate(); |
|
this.fireEvent(BI.GridTableScrollbar.EVENT_SCROLL, this.options.position); |
|
} else { |
|
this._mouseMoveTracker.captureMouseMoves(e); |
|
} |
|
this.element[0].focus(); |
|
}, |
|
|
|
_onMouseMove: function (deltaX, deltaY) { |
|
var delta = this._isHorizontal() ? deltaX : deltaY; |
|
delta /= this._getScale(); |
|
this.options.position = BI.clamp(this.options.position + delta, 0, this.options.contentSize - this.options.size); |
|
this.isDragging = this._mouseMoveTracker.isDragging(); |
|
this._populate(); |
|
this.fireEvent(BI.GridTableScrollbar.EVENT_SCROLL, this.options.position); |
|
}, |
|
|
|
_onMouseMoveEnd: function (event) { |
|
this._mouseMoveTracker.releaseMouseMoves(); |
|
if (this.isDragging === true) { |
|
this.isDragging = false; |
|
this._populate(); |
|
} |
|
}, |
|
|
|
_onKeyDown: function (event) { |
|
var Keys = { |
|
BACKSPACE: 8, |
|
TAB: 9, |
|
RETURN: 13, |
|
ALT: 18, |
|
ESC: 27, |
|
SPACE: 32, |
|
PAGE_UP: 33, |
|
PAGE_DOWN: 34, |
|
END: 35, |
|
HOME: 36, |
|
LEFT: 37, |
|
UP: 38, |
|
RIGHT: 39, |
|
DOWN: 40, |
|
DELETE: 46, |
|
COMMA: 188, |
|
PERIOD: 190, |
|
A: 65, |
|
Z: 90, |
|
ZERO: 48, |
|
NUMPAD_0: 96, |
|
NUMPAD_9: 105 |
|
}; |
|
var keyCode = event.keyCode; |
|
|
|
if (keyCode === Keys.TAB) { |
|
return; |
|
} |
|
|
|
var distance = 40; |
|
var direction = 0; |
|
|
|
if (this._isHorizontal()) { |
|
switch (keyCode) { |
|
case Keys.HOME: |
|
direction = -1; |
|
distance = this.options.contentSize; |
|
break; |
|
|
|
case Keys.LEFT: |
|
direction = -1; |
|
break; |
|
|
|
case Keys.RIGHT: |
|
direction = 1; |
|
break; |
|
|
|
default: |
|
return; |
|
} |
|
} |
|
|
|
if (!this._isHorizontal()) { |
|
switch (keyCode) { |
|
case Keys.SPACE: |
|
if (event.shiftKey) { |
|
direction = -1; |
|
} else { |
|
direction = 1; |
|
} |
|
break; |
|
|
|
case Keys.HOME: |
|
direction = -1; |
|
distance = this.options.contentSize; |
|
break; |
|
|
|
case Keys.UP: |
|
direction = -1; |
|
break; |
|
|
|
case Keys.DOWN: |
|
direction = 1; |
|
break; |
|
|
|
case Keys.PAGE_UP: |
|
direction = -1; |
|
distance = this.options.size; |
|
break; |
|
|
|
case Keys.PAGE_DOWN: |
|
direction = 1; |
|
distance = this.options.size; |
|
break; |
|
|
|
default: |
|
return; |
|
} |
|
} |
|
|
|
this.options.position = BI.clamp(this.options.position + (distance * direction), 0, this.options.contentSize - this.options.size); |
|
event.preventDefault(); |
|
this._populate(); |
|
this.fireEvent(BI.GridTableScrollbar.EVENT_SCROLL, this.options.position); |
|
}, |
|
|
|
_populate: function () { |
|
var self = this, o = this.options; |
|
if (o.size < 1 || o.contentSize <= o.size) { |
|
this.setVisible(false); |
|
return; |
|
} |
|
this.setVisible(true); |
|
|
|
var size = o.size; |
|
var isHorizontal = this._isHorizontal(); |
|
var isActive = this.focused || this.isDragging; |
|
|
|
var faceSize = this._getFaceSize(); |
|
var isOpaque = o.isOpaque; |
|
this.element[isOpaque === true ? "addClass" : "removeClass"]("public-scrollbar-main-opaque"); |
|
this.element[isActive === true ? "addClass" : "removeClass"]("public-scrollbar-main-active"); |
|
|
|
this.face.element[isActive === true ? "addClass" : "removeClass"]("public-scrollbar-face-active"); |
|
|
|
var position = o.position * this._getScale() + this._const.FACE_MARGIN; |
|
|
|
var items = this.contextLayout.attr("items"); |
|
if (isHorizontal) { |
|
this.setWidth(size); |
|
this.face.setWidth(faceSize - this._const.FACE_MARGIN_2); |
|
items[0].left = position; |
|
items[0].top = 0; |
|
} else { |
|
this.setHeight(size); |
|
this.face.setHeight(faceSize - this._const.FACE_MARGIN_2); |
|
items[0].left = 0; |
|
items[0].top = position; |
|
} |
|
this.contextLayout.attr("items", items); |
|
this.contextLayout.resize(); |
|
}, |
|
|
|
setContentSize: function (contentSize) { |
|
this.options.contentSize = contentSize; |
|
}, |
|
|
|
setPosition: function (position) { |
|
this.options.position = position; |
|
}, |
|
|
|
setSize: function (size) { |
|
this.options.size = size; |
|
}, |
|
|
|
populate: function () { |
|
this._populate(); |
|
} |
|
}); |
|
BI.GridTableScrollbar.SIZE = 10; |
|
BI.GridTableScrollbar.EVENT_SCROLL = "EVENT_SCROLL"; |
|
$.shortcut("bi.grid_table_scrollbar", BI.GridTableScrollbar); |
|
|
|
|
|
BI.GridTableHorizontalScrollbar = BI.inherit(BI.Widget, { |
|
_const: { |
|
FACE_MARGIN: 4, |
|
FACE_MARGIN_2: 4 * 2, |
|
FACE_SIZE_MIN: 30, |
|
KEYBOARD_SCROLL_AMOUNT: 40 |
|
}, |
|
_defaultConfig: function () { |
|
return BI.extend(BI.GridTableHorizontalScrollbar.superclass._defaultConfig.apply(this, arguments), { |
|
attributes: { |
|
tabIndex: 0 |
|
}, |
|
contentSize: 0, |
|
position: 0, |
|
size: 0 |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.GridTableHorizontalScrollbar.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.scrollbar = BI.createWidget({ |
|
type: "bi.grid_table_scrollbar", |
|
orientation: "horizontal", |
|
isOpaque: true, |
|
position: o.position, |
|
contentSize: o.contentSize, |
|
size: o.size |
|
}); |
|
this.scrollbar.on(BI.GridTableScrollbar.EVENT_SCROLL, function () { |
|
self.fireEvent(BI.GridTableHorizontalScrollbar.EVENT_SCROLL, arguments); |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
cls: "horizontal-scrollbar", |
|
element: this, |
|
width: o.size, |
|
height: BI.GridTableScrollbar.SIZE, |
|
items: [{ |
|
el: { |
|
type: "bi.absolute", |
|
scrollable: false, |
|
height: BI.GridTableScrollbar.SIZE, |
|
items: [{ |
|
el: this.scrollbar, |
|
left: 0, |
|
top: 0 |
|
}] |
|
}, |
|
top: 0, |
|
left: 0, |
|
right: 0 |
|
}] |
|
}); |
|
}, |
|
|
|
setContentSize: function (contentSize) { |
|
this.options.contentSize = contentSize; |
|
this.scrollbar.setContentSize(contentSize); |
|
}, |
|
|
|
setPosition: function (position) { |
|
this.options.position = position; |
|
this.scrollbar.setPosition(position); |
|
}, |
|
|
|
setSize: function (size) { |
|
this.setWidth(size); |
|
this.options.size = size; |
|
this.scrollbar.setSize(size); |
|
}, |
|
|
|
populate: function () { |
|
this.scrollbar.populate(); |
|
} |
|
}); |
|
BI.GridTableHorizontalScrollbar.EVENT_SCROLL = "EVENT_SCROLL"; |
|
$.shortcut("bi.grid_table_horizontal_scrollbar", BI.GridTableHorizontalScrollbar);/** |
|
* |
|
* 表格 |
|
* |
|
* Created by GUY on 2015/9/22. |
|
* @class BI.TableHeaderCell |
|
* @extends BI.Single |
|
*/ |
|
BI.TableHeaderCell = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.TableHeaderCell.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-table-header-cell", |
|
text: "" |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.TableHeaderCell.superclass._init.apply(this, arguments); |
|
BI.createWidget({ |
|
type: "bi.label", |
|
element: this, |
|
textAlign: "center", |
|
height: this.options.height, |
|
text: this.options.text, |
|
value: this.options.value |
|
}) |
|
} |
|
}); |
|
|
|
$.shortcut("bi.table_header_cell", BI.TableHeaderCell);/** |
|
* |
|
* 表格 |
|
* |
|
* 能处理静态宽度以及动态宽度的表, 百分比宽度的表请使用PreviewTable |
|
* |
|
* Created by GUY on 2015/9/22. |
|
* @class BI.Table |
|
* @extends BI.Widget |
|
*/ |
|
BI.Table = BI.inherit(BI.Widget, { |
|
|
|
_defaultConfig: function () { |
|
return BI.extend(BI.Table.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-table", |
|
logic: { //冻结的页面布局逻辑 |
|
dynamic: false |
|
}, |
|
|
|
isNeedResize: false,//是否需要调整列宽 |
|
isResizeAdapt: true,//是否需要在调整列宽或区域宽度的时候它们自适应变化 |
|
|
|
isNeedFreeze: false,//是否需要冻结单元格 |
|
freezeCols: [], //冻结的列号,从0开始,isNeedFreeze为true时生效 |
|
|
|
isNeedMerge: false,//是否需要合并单元格 |
|
mergeCols: [], //合并的单元格列号 |
|
mergeRule: function (row1, row2) { //合并规则, 默认相等时合并 |
|
return BI.isEqual(row1, row2); |
|
}, |
|
|
|
columnSize: [], |
|
headerRowSize: 25, |
|
footerRowSize: 25, |
|
rowSize: 25, |
|
|
|
regionColumnSize: false, |
|
|
|
header: [], |
|
footer: false, |
|
items: [] //二维数组 |
|
}) |
|
}, |
|
|
|
_calculateWidth: function (width) { |
|
if (!width || width === "0") { |
|
return ""; |
|
} |
|
width = BI.parseFloat(width); |
|
if (width < 0) { |
|
width = 0; |
|
} |
|
return width > 1.01 ? width : (width * 100 + "%"); |
|
}, |
|
|
|
_calculateHeight: function (height) { |
|
return height ? height : ""; |
|
}, |
|
|
|
_isRightFreeze: function () { |
|
return BI.isNotEmptyArray(this.options.freezeCols) && BI.first(this.options.freezeCols) !== 0; |
|
}, |
|
|
|
_createTopLeft: function () { |
|
var o = this.options, isRight = this._isRightFreeze(); |
|
this.topLeftColGroupTds = {}; |
|
this.topLeftBodyTds = {}; |
|
this.topLeftBodyItems = {}; |
|
var table = this._table(); |
|
var colgroup = this._createColGroup(this.columnLeft, this.topLeftColGroupTds); |
|
var body = this.topLeftBody = this._body(); |
|
body.element.append(this._createHeaderCells(this.topLeftItems, this.columnLeft, this.mergeLeft, this.topLeftBodyTds, this.topLeftBodyItems)); |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
element: table, |
|
items: [colgroup, body] |
|
}); |
|
if (isRight) { |
|
var w = 0; |
|
BI.each(o.columnSize, function (i, col) { |
|
if (!o.freezeCols.contains(i)) { |
|
w += col; |
|
} |
|
}); |
|
if (BI.isNumeric(w) && w > 1) { |
|
w = BI.parseFloat(w) + o.columnSize.length - o.freezeCols.length; |
|
} |
|
} |
|
return (this.topLeftContainer = BI.createWidget({ |
|
type: "bi.adaptive", |
|
width: this._calculateWidth(w), |
|
items: [table] |
|
})); |
|
}, |
|
|
|
_createTopRight: function () { |
|
var o = this.options, isRight = this._isRightFreeze(); |
|
this.topRightColGroupTds = {}; |
|
this.topRightBodyTds = {}; |
|
this.topRightBodyItems = {}; |
|
var table = this._table(); |
|
var colgroup = this._createColGroup(this.columnRight, this.topRightColGroupTds); |
|
var body = this.topRightBody = this._body(); |
|
body.element.append(this._createHeaderCells(this.topRightItems, this.columnRight, this.mergeRight, this.topRightBodyTds, this.topRightBodyItems, this.columnLeft.length)); |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
element: table, |
|
items: [colgroup, body] |
|
}); |
|
if (!isRight) { |
|
var w = 0; |
|
BI.each(o.columnSize, function (i, col) { |
|
if (!o.freezeCols.contains(i)) { |
|
w += col; |
|
} |
|
}); |
|
if (BI.isNumeric(w)) { |
|
w = BI.parseFloat(w) + o.columnSize.length - o.freezeCols.length; |
|
} |
|
} |
|
return (this.topRightContainer = BI.createWidget({ |
|
type: "bi.adaptive", |
|
width: w || undefined, |
|
items: [table] |
|
})); |
|
}, |
|
|
|
_createBottomLeft: function () { |
|
var o = this.options, isRight = this._isRightFreeze(); |
|
this.bottomLeftColGroupTds = {}; |
|
this.bottomLeftBodyTds = {}; |
|
this.bottomLeftBodyItems = {}; |
|
var table = this._table(); |
|
var colgroup = this._createColGroup(this.columnLeft, this.bottomLeftColGroupTds); |
|
var body = this._createBottomLeftBody(); |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
element: table, |
|
items: [colgroup, body] |
|
}); |
|
if (isRight) { |
|
var w = 0; |
|
BI.each(o.columnSize, function (i, col) { |
|
if (!o.freezeCols.contains(i)) { |
|
w += col; |
|
} |
|
}); |
|
if (BI.isNumeric(w) && w > 1) { |
|
w = BI.parseFloat(w) + o.columnSize.length - o.freezeCols.length; |
|
} |
|
} |
|
return (this.bottomLeftContainer = BI.createWidget({ |
|
type: "bi.adaptive", |
|
width: this._calculateWidth(w), |
|
items: [table] |
|
})); |
|
}, |
|
|
|
_createBottomLeftBody: function () { |
|
var body = this.bottomLeftBody = this._body(); |
|
body.element.append(this._createCells(this.bottomLeftItems, this.columnLeft, this.mergeLeft, this.bottomLeftBodyTds, this.bottomLeftBodyItems)); |
|
return body; |
|
}, |
|
|
|
_createBottomRight: function () { |
|
var o = this.options, isRight = this._isRightFreeze(); |
|
this.bottomRightColGroupTds = {}; |
|
this.bottomRightBodyTds = {}; |
|
this.bottomRightBodyItems = {}; |
|
var table = this._table(); |
|
var colgroup = this._createColGroup(this.columnRight, this.bottomRightColGroupTds); |
|
var body = this._createBottomRightBody(); |
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
element: table, |
|
items: [colgroup, body] |
|
}); |
|
if (!isRight) { |
|
var w = 0; |
|
BI.each(o.columnSize, function (i, col) { |
|
if (!o.freezeCols.contains(i)) { |
|
w += col; |
|
} |
|
}); |
|
if (BI.isNumeric(w) && w > 1) { |
|
w = BI.parseFloat(w) + o.columnSize.length - o.freezeCols.length; |
|
} |
|
} |
|
return (this.bottomRightContainer = BI.createWidget({ |
|
type: "bi.adaptive", |
|
width: this._calculateWidth(w), |
|
items: [table] |
|
})); |
|
}, |
|
|
|
_createBottomRightBody: function () { |
|
var body = this.bottomRightBody = this._body(); |
|
body.element.append(this._createCells(this.bottomRightItems, this.columnRight, this.mergeRight, this.bottomRightBodyTds, this.bottomRightBodyItems, this.columnLeft.length)); |
|
return body; |
|
}, |
|
|
|
_createFreezeTable: function () { |
|
var self = this, o = this.options; |
|
var isRight = this._isRightFreeze(); |
|
var split = this._split(o.header); |
|
this.topLeftItems = split.left; |
|
this.topRightItems = split.right; |
|
split = this._split(o.items); |
|
this.bottomLeftItems = split.left; |
|
this.bottomRightItems = split.right; |
|
|
|
this.columnLeft = []; |
|
this.columnRight = []; |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.freezeCols.contains(i)) { |
|
self[isRight ? "columnRight" : "columnLeft"].push(size); |
|
} else { |
|
self[isRight ? "columnLeft" : "columnRight"].push(size); |
|
} |
|
}); |
|
this.mergeLeft = []; |
|
this.mergeRight = []; |
|
BI.each(o.mergeCols, function (i, col) { |
|
if (o.freezeCols.contains(col)) { |
|
self[isRight ? "mergeRight" : "mergeLeft"].push(col); |
|
} else { |
|
self[isRight ? "mergeLeft" : "mergeRight"].push(col); |
|
} |
|
}); |
|
|
|
var topLeft = this._createTopLeft(); |
|
var topRight = this._createTopRight(); |
|
var bottomLeft = this._createBottomLeft(); |
|
var bottomRight = this._createBottomRight(); |
|
|
|
this.scrollTopLeft = BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "scroll-top-left", |
|
width: "100%", |
|
height: "100%", |
|
scrollable: false, |
|
items: [topLeft] |
|
}); |
|
this.scrollTopRight = BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "scroll-top-right", |
|
width: "100%", |
|
height: "100%", |
|
scrollable: false, |
|
items: [topRight] |
|
}); |
|
this.scrollBottomLeft = BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "scroll-bottom-left", |
|
width: "100%", |
|
height: "100%", |
|
scrollable: isRight || null, |
|
scrollx: !isRight, |
|
items: [bottomLeft] |
|
}); |
|
this.scrollBottomRight = BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "scroll-bottom-right", |
|
width: "100%", |
|
height: "100%", |
|
scrollable: !isRight || null, |
|
scrollx: isRight, |
|
items: [bottomRight] |
|
}); |
|
this.topLeft = BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "top-left", |
|
scrollable: false, |
|
items: [this.scrollTopLeft] |
|
}); |
|
this.topRight = BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "top-right", |
|
scrollable: false, |
|
items: [this.scrollTopRight] |
|
}); |
|
this.bottomLeft = BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "bottom-left", |
|
// scrollable: false, |
|
items: [this.scrollBottomLeft] |
|
}); |
|
this.bottomRight = BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "bottom-right", |
|
scrollable: false, |
|
items: [this.scrollBottomRight] |
|
}); |
|
|
|
var headerHeight = o.header.length * ((o.headerRowSize || o.rowSize) + 1) + 1; |
|
var leftWidth = BI.sum(o.freezeCols, function (i, col) { |
|
return o.columnSize[col] > 1 ? o.columnSize[col] + 1 : o.columnSize[col]; |
|
}); |
|
|
|
if (o.isNeedResize) { |
|
var resizer; |
|
var createResizer = function (size, position) { |
|
var rowSize = self.getCalculateRegionRowSize(); |
|
resizer = BI.createWidget({ |
|
type: "bi.layout", |
|
cls: "bi-resizer", |
|
width: size.width, |
|
height: rowSize[0] + rowSize[1] |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: "body", |
|
items: [{ |
|
el: resizer, |
|
left: position.left, |
|
top: position.top - rowSize[0] |
|
}] |
|
}); |
|
}; |
|
var resizeResizer = function (size, position) { |
|
var rowSize = self.getCalculateRegionRowSize(); |
|
var columnSize = self.getCalculateRegionColumnSize(); |
|
var height = rowSize[0] + rowSize[1]; |
|
var sumSize = columnSize[0] + columnSize[1]; |
|
if (size.width > sumSize / 5 * 4) { |
|
size.width = sumSize / 5 * 4; |
|
} |
|
if (size.width < 10) { |
|
size.width = 10; |
|
} |
|
resizer.element.css({ |
|
"left": position.left + "px", |
|
"width": size.width + "px", |
|
"height": height + "px" |
|
}); |
|
}; |
|
var stopResizer = function () { |
|
resizer && resizer.destroy(); |
|
resizer = null; |
|
}; |
|
var handle; |
|
if (o.freezeCols.length > 0 && o.freezeCols.length < o.columnSize.length) { |
|
if (isRight) { |
|
var options = { |
|
handles: "w", |
|
minWidth: 15, |
|
helper: "clone", |
|
start: function (event, ui) { |
|
createResizer(ui.size, ui.position); |
|
self.fireEvent(BI.Table.EVENT_TABLE_BEFORE_REGION_RESIZE); |
|
}, |
|
resize: function (e, ui) { |
|
resizeResizer(ui.size, ui.position); |
|
self.fireEvent(BI.Table.EVENT_TABLE_REGION_RESIZE); |
|
e.stopPropagation(); |
|
//return false; |
|
}, |
|
stop: function (e, ui) { |
|
stopResizer(); |
|
if (o.isResizeAdapt) { |
|
var increment = ui.size.width - (BI.sum(self.columnRight) + self.columnRight.length); |
|
o.columnSize[self.columnLeft.length] += increment; |
|
} else { |
|
self.setRegionColumnSize(["fill", ui.size.width]); |
|
} |
|
self._resize(); |
|
ui.element.css("left", ""); |
|
self.fireEvent(BI.Table.EVENT_TABLE_AFTER_REGION_RESIZE); |
|
} |
|
}; |
|
self.bottomRight.element.resizable(options); |
|
handle = $(".ui-resizable-handle", this.bottomRight.element).css("top", -1 * headerHeight); |
|
} else { |
|
var options = { |
|
handles: "e", |
|
minWidth: 15, |
|
helper: "clone", |
|
start: function (event, ui) { |
|
createResizer(ui.size, ui.position); |
|
self.fireEvent(BI.Table.EVENT_TABLE_BEFORE_REGION_RESIZE); |
|
}, |
|
resize: function (e, ui) { |
|
resizeResizer(ui.size, ui.position); |
|
self.fireEvent(BI.Table.EVENT_TABLE_REGION_RESIZE); |
|
e.stopPropagation(); |
|
//return false; |
|
}, |
|
stop: function (e, ui) { |
|
stopResizer(); |
|
if (o.isResizeAdapt) { |
|
var increment = ui.size.width - (BI.sum(self.columnLeft) + self.columnLeft.length); |
|
o.columnSize[self.columnLeft.length - 1] += increment; |
|
} else { |
|
self.setRegionColumnSize([ui.size.width, "fill"]); |
|
} |
|
self._resize(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_AFTER_REGION_RESIZE); |
|
} |
|
}; |
|
self.bottomLeft.element.resizable(options); |
|
handle = $(".ui-resizable-handle", this.bottomLeft.element).css("top", -1 * headerHeight); |
|
} |
|
} |
|
} |
|
this._resize = function () { |
|
if (self.scrollBottomLeft.element.is(":visible")) { |
|
self.scrollBottomLeft.element.css({"overflow-x": "auto"}); |
|
self.scrollBottomRight.element.css({"overflow-x": "auto"}); |
|
self.setColumnSize(o.columnSize); |
|
if (isRight) { |
|
self.scrollBottomLeft.element.css({"overflow-y": "auto"}); |
|
} else { |
|
self.scrollBottomRight.element.css({"overflow-y": "auto"}); |
|
} |
|
if (self.scrollBottomLeft.element.hasHorizonScroll() || self.scrollBottomRight.element.hasHorizonScroll()) { |
|
self.scrollBottomLeft.element.css("overflow-x", "scroll"); |
|
self.scrollBottomRight.element.css("overflow-x", "scroll"); |
|
} |
|
if (self.scrollBottomRight.element.hasVerticalScroll()) { |
|
self.scrollTopRight.element.css("overflow-y", "scroll"); |
|
} else { |
|
self.scrollTopRight.element.css("overflow-y", "hidden"); |
|
} |
|
if (self.scrollBottomLeft.element.hasVerticalScroll()) { |
|
self.scrollTopLeft.element.css("overflow-y", "scroll"); |
|
} else { |
|
self.scrollTopLeft.element.css("overflow-y", "hidden"); |
|
} |
|
self.scrollTopLeft.element[0].scrollLeft = self.scrollBottomLeft.element[0].scrollLeft; |
|
self.scrollTopRight.element[0].scrollLeft = self.scrollBottomRight.element[0].scrollLeft; |
|
self.scrollBottomLeft.element[0].scrollTop = self.scrollBottomRight.element[0].scrollTop; |
|
//调整拖拽handle的高度 |
|
if (o.isNeedResize) { |
|
handle && handle.css("height", self.bottomLeft.element.height() + headerHeight); |
|
} |
|
} |
|
}; |
|
|
|
var regionColumnSize = o.regionColumnSize; |
|
if (o.freezeCols.length === 0) { |
|
regionColumnSize = isRight ? ['fill', 0] : [0, 'fill']; |
|
} else if (o.freezeCols.length >= o.columnSize.length) { |
|
regionColumnSize = isRight ? [0, 'fill'] : ['fill', 0]; |
|
} |
|
this.partitions = BI.createWidget(BI.extend({ |
|
element: this |
|
}, BI.LogicFactory.createLogic("table", BI.extend({}, o.logic, { |
|
rows: 2, |
|
columns: 2, |
|
columnSize: regionColumnSize || (isRight ? ['fill', leftWidth] : [leftWidth, 'fill']), |
|
rowSize: [headerHeight, 'fill'], |
|
items: [[{ |
|
el: this.topLeft |
|
}, { |
|
el: this.topRight |
|
}], [{ |
|
el: this.bottomLeft |
|
}, { |
|
el: this.bottomRight |
|
}]] |
|
})))); |
|
|
|
//var scrollElement = isRight ? scrollBottomLeft.element : scrollBottomRight.element; |
|
//var scrollTopElement = isRight ? scrollTopLeft.element : scrollTopRight.element; |
|
//var otherElement = isRight ? scrollBottomRight.element : scrollBottomLeft.element; |
|
|
|
this._initFreezeScroll(); |
|
BI.nextTick(function () { |
|
if (self.element.is(":visible")) { |
|
self._resize(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_AFTER_INIT); |
|
} |
|
}); |
|
BI.Resizers.add(this.getName(), function (e) { |
|
if (BI.isWindow(e.target) && self.element.is(":visible")) { |
|
self._resize(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_RESIZE); |
|
} |
|
}); |
|
}, |
|
|
|
_initFreezeScroll: function () { |
|
var self = this, o = this.options; |
|
scroll(this.scrollBottomRight.element, this.scrollTopRight.element, this.scrollBottomLeft.element); |
|
scroll(this.scrollBottomLeft.element, this.scrollTopLeft.element, this.scrollBottomRight.element); |
|
|
|
function scroll(scrollElement, scrollTopElement, otherElement) { |
|
var scrolling, scrollingX; |
|
var fn = function (event, delta, deltaX, deltaY) { |
|
var inf = self._getScrollOffsetAndDur(event); |
|
if (deltaY < 0 || deltaY > 0) { |
|
if (scrolling) { |
|
scrollElement[0].scrollTop = scrolling; |
|
} |
|
scrolling = scrollElement[0].scrollTop - delta * inf.offset; |
|
var stopPropagation = false; |
|
var st = scrollElement[0].scrollTop; |
|
scrollElement[0].scrollTop = scrolling; |
|
if (scrollElement[0].scrollTop !== st) { |
|
stopPropagation = true; |
|
} |
|
scrollElement[0].scrollTop = st; |
|
self._animateScrollTo(scrollElement, scrollElement[0].scrollTop, scrolling, inf.dur, "linear", { |
|
onStart: function () { |
|
}, |
|
onUpdate: function (top) { |
|
otherElement[0].scrollTop = top; |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, top); |
|
}, |
|
onComplete: function () { |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, scrolling); |
|
scrolling = null; |
|
} |
|
}); |
|
|
|
|
|
//otherElement[0].scrollTop = scrollTop; |
|
//scrollElement[0].scrollTop = scrollTop; |
|
//self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, scrollTop); |
|
if (stopPropagation === true) { |
|
event.stopPropagation(); |
|
return false; |
|
} |
|
return; |
|
} |
|
//if (deltaX < 0 || deltaX > 0) { |
|
// if (scrollingX) { |
|
// scrollElement[0].scrollLeft = scrollingX; |
|
// } |
|
// scrollingX = scrollElement[0].scrollLeft + delta * inf.offset; |
|
// var stopPropagation = false; |
|
// var sl = scrollElement[0].scrollLeft; |
|
// scrollElement[0].scrollLeft = scrollingX; |
|
// if (scrollElement[0].scrollLeft !== sl) { |
|
// stopPropagation = true; |
|
// } |
|
// scrollElement[0].scrollLeft = sl; |
|
// self._animateScrollTo(scrollElement, scrollElement[0].scrollLeft, scrollingX, inf.dur, "linear", { |
|
// direction: "left", |
|
// onStart: function () { |
|
// }, |
|
// onUpdate: function (left) { |
|
// scrollTopElement[0].scrollLeft = left; |
|
// self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, null, left); |
|
// }, |
|
// onComplete: function () { |
|
// self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, null, scrollingX); |
|
// scrollingX = null; |
|
// } |
|
// }); |
|
// |
|
// |
|
// //otherElement[0].scrollTop = scrollTop; |
|
// //scrollElement[0].scrollTop = scrollTop; |
|
// //self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, scrollTop); |
|
// if (stopPropagation === true) { |
|
// event.stopPropagation(); |
|
// return false; |
|
// } |
|
//} |
|
}; |
|
scrollElement.mousewheel(fn); |
|
var scrollTop = 0, scrollLeft = 0; |
|
scrollElement.scroll(function (e) { |
|
var change = false; |
|
if (scrollElement.scrollTop() != scrollTop) { |
|
var old = otherElement.scrollTop(); |
|
otherElement.scrollTop(scrollElement.scrollTop()); |
|
scrollTop = scrollElement.scrollTop(); |
|
if (Math.abs(old - otherElement[0].scrollTop) > 0.1) { |
|
e.stopPropagation(); |
|
change = true; |
|
} |
|
} |
|
if (scrollElement.scrollLeft() != scrollLeft) { |
|
var old = scrollTopElement.scrollLeft(); |
|
scrollTopElement.scrollLeft(scrollElement.scrollLeft()); |
|
scrollLeft = scrollElement.scrollLeft(); |
|
if (Math.abs(old - scrollTopElement[0].scrollLeft) > 0.1) { |
|
e.stopPropagation(); |
|
change = true; |
|
} |
|
} |
|
// self.fireEvent(BI.Table.EVENT_TABLE_SCROLL); |
|
if (change === true) { |
|
e.stopPropagation(); |
|
//return false; |
|
} |
|
}); |
|
} |
|
}, |
|
|
|
_animateScrollTo: function (el, from, to, duration, easing, op) { |
|
var self = this; |
|
var onStart = op.onStart, onComplete = op.onComplete, onUpdate = op.onUpdate; |
|
var startTime = BI.getTime(), _delay, progress = 0, _request; |
|
_cancelTween(); |
|
_startTween(); |
|
var diff = to - from; |
|
el._stop = 0; |
|
function _step() { |
|
if (el._stop) { |
|
return; |
|
} |
|
if (!progress) { |
|
onStart.call(); |
|
} |
|
progress = BI.getTime() - startTime; |
|
_tween(); |
|
if (progress >= el.time) { |
|
el.time = (progress > el.time) ? progress + _delay - (progress - el.time) : progress + _delay - 1; |
|
if (el.time < progress + 1) { |
|
el.time = progress + 1; |
|
} |
|
} |
|
if (el.time < duration) { |
|
el._id = _request(_step); |
|
} else { |
|
el[op.direction == 'left' ? "scrollLeft" : "scrollTop"](to); |
|
onComplete.call(); |
|
} |
|
} |
|
|
|
function _tween() { |
|
var top = to; |
|
if (duration > 0) { |
|
el.currVal = _ease(el.time, from, diff, duration, easing); |
|
el[op.direction == 'left' ? "scrollLeft" : "scrollTop"](top = Math.round(el.currVal)); |
|
} else { |
|
el[op.direction == 'left' ? "scrollLeft" : "scrollTop"](to); |
|
} |
|
onUpdate(top); |
|
} |
|
|
|
function _startTween() { |
|
_delay = 1000 / 60; |
|
el.time = progress + _delay; |
|
_request = (!requestAnimationFrame()) ? function (f) { |
|
_tween(); |
|
return setTimeout(f, 0.01); |
|
} : requestAnimationFrame(); |
|
el._id = _request(_step); |
|
} |
|
|
|
function requestAnimationFrame() { |
|
return window.requestAnimationFrame || |
|
window.webkitRequestAnimationFrame || |
|
window.mozRequestAnimationFrame || |
|
window.msRequestAnimationFrame || |
|
window.oRequestAnimationFrame; |
|
} |
|
|
|
function cancelAnimationFrame() { |
|
return window.cancelAnimationFrame || |
|
window.webkitCancelAnimationFrame || |
|
window.mozCancelAnimationFrame || |
|
window.msCancelAnimationFrame || |
|
window.oCancelAnimationFrame || |
|
window.cancelRequestAnimationFrame || |
|
window.webkitCancelRequestAnimationFrame || |
|
window.mozCancelRequestAnimationFrame || |
|
window.msCancelRequestAnimationFrame || |
|
window.oCancelRequestAnimationFrame |
|
} |
|
|
|
function _cancelTween() { |
|
if (el._id == null) { |
|
return; |
|
} |
|
if (!cancelAnimationFrame()) { |
|
clearTimeout(el._id); |
|
} else { |
|
cancelAnimationFrame()(el._id); |
|
} |
|
el._id = null; |
|
} |
|
|
|
function _ease(t, b, c, d, type) { |
|
switch (type) { |
|
case "linear": |
|
return c * t / d + b; |
|
break; |
|
case "mcsLinearOut": |
|
t /= d; |
|
t--; |
|
return c * Math.sqrt(1 - t * t) + b; |
|
break; |
|
case "easeInOutSmooth": |
|
t /= d / 2; |
|
if (t < 1) { |
|
return c / 2 * t * t + b; |
|
} |
|
t--; |
|
return -c / 2 * (t * (t - 2) - 1) + b; |
|
break; |
|
case "easeInOutStrong": |
|
t /= d / 2; |
|
if (t < 1) { |
|
return c / 2 * Math.pow(2, 10 * (t - 1)) + b; |
|
} |
|
t--; |
|
return c / 2 * ( -Math.pow(2, -10 * t) + 2 ) + b; |
|
break; |
|
case "easeInOut": |
|
case "mcsEaseInOut": |
|
t /= d / 2; |
|
if (t < 1) { |
|
return c / 2 * t * t * t + b; |
|
} |
|
t -= 2; |
|
return c / 2 * (t * t * t + 2) + b; |
|
break; |
|
case "easeOutSmooth": |
|
t /= d; |
|
t--; |
|
return -c * (t * t * t * t - 1) + b; |
|
break; |
|
case "easeOutStrong": |
|
return c * ( -Math.pow(2, -10 * t / d) + 1 ) + b; |
|
break; |
|
case "easeOut": |
|
case "mcsEaseOut": |
|
default: |
|
var ts = (t /= d) * t, tc = ts * t; |
|
return b + c * (0.499999999999997 * tc * ts + -2.5 * ts * ts + 5.5 * tc + -6.5 * ts + 4 * t); |
|
} |
|
} |
|
}, |
|
|
|
_getScrollOffsetAndDur: function (event) { |
|
var offset = 40, dur = 200; |
|
if (event.originalEvent.wheelDelta) { |
|
offset = Math.abs(event.originalEvent.wheelDelta); |
|
} |
|
if (event.deltaFactor < 2) { |
|
offset = 3; |
|
dur = 17; |
|
} |
|
return { |
|
offset: offset, |
|
dur: dur |
|
}; |
|
}, |
|
|
|
resize: function () { |
|
this._resize(); |
|
}, |
|
|
|
_createCells: function (items, columnSize, mergeCols, TDs, Ws, start, rowSize) { |
|
var self = this, o = this.options, preCol = {}, preRow = {}, preRW = {}, preCW = {}, map = {}; |
|
columnSize = columnSize || o.columnSize; |
|
mergeCols = mergeCols || o.mergeCols; |
|
TDs = TDs || {}; |
|
Ws = Ws || {}; |
|
start = start || 0; |
|
rowSize || (rowSize = o.rowSize); |
|
var frag = document.createDocumentFragment(); |
|
BI.each(items, function (i, rows) { |
|
var tr = $("<tr>").addClass((i & 1) === 0 ? "odd" : "even"); |
|
BI.each(rows, function (j, row) { |
|
if (!map[i]) { |
|
map[i] = {}; |
|
} |
|
if (!TDs[i]) { |
|
TDs[i] = {}; |
|
} |
|
if (!Ws[i]) { |
|
Ws[i] = {}; |
|
} |
|
map[i][j] = row; |
|
|
|
if (o.isNeedMerge && mergeCols.contains(j)) { |
|
if (i === 0 && j === 0) { |
|
createOneEl(0, 0); |
|
} else if (j === 0 && i > 0) { |
|
var isNeedMergeRow = o.mergeRule(map[i][j], map[i - 1][j]); |
|
if (isNeedMergeRow === true) { |
|
mergeRow(i, j); |
|
preRow[i] = preCol[j]; |
|
preRW[i] = preCW[j]; |
|
} else { |
|
createOneEl(i, j); |
|
} |
|
} else if (i === 0 && j > 0) { |
|
var isNeedMergeCol = o.mergeRule(map[i][j], map[i][j - 1]); |
|
if (isNeedMergeCol === true) { |
|
mergeCol(i, j); |
|
preCol[j] = preRow[j - 1]; |
|
preCW[j] = preRW[j - 1]; |
|
} else { |
|
createOneEl(i, j); |
|
} |
|
} else { |
|
var isNeedMergeRow = o.mergeRule(map[i][j], map[i - 1][j]); |
|
var isNeedMergeCol = o.mergeRule(map[i][j], map[i][j - 1]); |
|
if (isNeedMergeCol && isNeedMergeRow) { |
|
return; |
|
} |
|
if (isNeedMergeCol) { |
|
mergeCol(i, j); |
|
} |
|
if (isNeedMergeRow) { |
|
mergeRow(i, j); |
|
} |
|
if (!isNeedMergeCol && !isNeedMergeRow) { |
|
createOneEl(i, j); |
|
} |
|
} |
|
} else { |
|
createOneEl(i, j); |
|
} |
|
}); |
|
function mergeRow(i, j) { |
|
var height = (preCol[j].attr("height") | 0) + rowSize + 1; |
|
preCol[j].attr("height", height).css("height", height); |
|
//preCW[j].element.css("height", height); |
|
var rowspan = ((preCol[j].attr("rowspan") || 1) | 0) + 1; |
|
preCol[j].attr("rowspan", rowspan); |
|
preCol[j].__mergeRows.pushDistinct(i); |
|
TDs[i][j] = preCol[j]; |
|
Ws[i][j] = preCW[j]; |
|
} |
|
|
|
function mergeCol(i, j) { |
|
if (columnSize[j]) { |
|
var width = preRow[i].attr("width") | 0; |
|
if (width > 1.05 && columnSize[j]) { |
|
width = width + columnSize[j] + 1; |
|
if (j === columnSize.length - 1) { |
|
width--; |
|
} |
|
} else { |
|
width = width + columnSize[j] |
|
} |
|
width = self._calculateWidth(width); |
|
preRow[i].attr("width", width).css("width", width); |
|
preRW[i].element.width(width); |
|
} |
|
var colspan = ((preRow[i].attr("colspan") || 1) | 0) + 1; |
|
preRow[i].attr("colspan", colspan); |
|
preRow[i].__mergeCols.pushDistinct(j); |
|
TDs[i][j] = preRow[i]; |
|
Ws[i][j] = preRW[i]; |
|
} |
|
|
|
function createOneEl(r, c) { |
|
var width = self._calculateWidth(columnSize[c]); |
|
if (width > 1.05 && c === columnSize.length - 1) { |
|
width--; |
|
} |
|
var height = self._calculateHeight(rowSize); |
|
var td = $("<td>").attr("height", height) |
|
.attr("width", width).css({"width": width, "height": height, "position": "relative"}) |
|
.addClass((c & 1) === 0 ? "odd-col" : "even-col") |
|
.addClass(r === 0 ? "first-row" : "") |
|
.addClass(c === 0 ? "first-col" : "") |
|
.addClass(c === rows.length - 1 ? "last-col" : ""); |
|
var w = BI.createWidget(map[r][c], { |
|
type: "bi.table_cell", |
|
textAlign: "left", |
|
width: BI.isNumeric(width) ? width : "", |
|
height: BI.isNumeric(height) ? height : "", |
|
_row: r, |
|
_col: c + start |
|
}); |
|
w.element.css("position", "relative"); |
|
td.append(w.element); |
|
tr.append(td); |
|
preCol[c] = td; |
|
preCol[c].__mergeRows = [r]; |
|
preCW[c] = w; |
|
preRow[r] = td; |
|
preRow[r].__mergeCols = [c]; |
|
preRW[r] = w; |
|
TDs[r][c] = td; |
|
Ws[r][c] = w; |
|
} |
|
|
|
frag.appendChild(tr[0]); |
|
}); |
|
return frag; |
|
}, |
|
|
|
_createColGroupCells: function (columnSize, store) { |
|
var self = this, o = this.options; |
|
columnSize = columnSize || o.columnSize; |
|
store = store || {}; |
|
var frag = document.createDocumentFragment(); |
|
BI.each(columnSize, function (i, size) { |
|
var width = self._calculateWidth(size); |
|
var col = $("<col>").attr("width", width).css("width", width); |
|
store[i] = col; |
|
frag.appendChild(col[0]); |
|
}); |
|
return frag; |
|
}, |
|
|
|
_createHeaderCells: function (items, columnSize, mergeCols, TDs, Ws, start) { |
|
var self = this, o = this.options; |
|
start || (start = 0); |
|
var frag = this._createCells(items, columnSize, BI.range(o.columnSize.length), TDs, Ws, start, o.headerRowSize || o.rowSize); |
|
|
|
if (o.isNeedResize === true) { |
|
var tds = TDs[BI.size(TDs) - 1]; |
|
BI.each(tds, function (j, td) { |
|
j = j | 0; |
|
var resizer; |
|
var getHeight = function (size, position) { |
|
var rowSize = self.getCalculateRegionRowSize(); |
|
if (o.isNeedFreeze === true) { |
|
var tableHeight = self.bottomRightContainer.element.outerHeight(); |
|
return size.height + Math.min(rowSize[1], tableHeight); |
|
} else { |
|
var tableHeight = self.tableContainer.element.outerHeight(); |
|
var offset = self.tableContainer.element.offset(); |
|
var offsetTop = position.top - offset.top; |
|
var height = tableHeight - offsetTop; |
|
height = Math.min(height, rowSize[0] - offsetTop); |
|
return height; |
|
} |
|
}; |
|
if (j < BI.size(tds) - 1) { |
|
td.resizable({ |
|
handles: "e", |
|
minWidth: 15, |
|
helper: "clone", |
|
start: function (event, ui) { |
|
var height = getHeight(ui.size, ui.position); |
|
resizer = BI.createWidget({ |
|
type: "bi.layout", |
|
cls: "bi-resizer", |
|
width: ui.size.width, |
|
height: height |
|
}); |
|
|
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: "body", |
|
items: [{ |
|
el: resizer, |
|
left: ui.position.left, |
|
top: ui.position.top |
|
}] |
|
}); |
|
self.fireEvent(BI.Table.EVENT_TABLE_BEFORE_COLUMN_RESIZE); |
|
}, |
|
resize: function (e, ui) { |
|
var height = getHeight(ui.size, ui.position); |
|
resizer.element.css({"width": ui.size.width + "px", "height": height + "px"}); |
|
//o.columnSize[start + j] = ui.size.width; |
|
//self.setColumnSize(o.columnSize); |
|
self.fireEvent(BI.Table.EVENT_TABLE_COLUMN_RESIZE); |
|
e.stopPropagation(); |
|
//return false; |
|
}, |
|
stop: function (e, ui) { |
|
resizer.destroy(); |
|
resizer = null; |
|
o.columnSize[start + j] = ui.size.width - 1; |
|
self.setColumnSize(o.columnSize); |
|
self.fireEvent(BI.Table.EVENT_TABLE_AFTER_COLUMN_RESIZE); |
|
} |
|
}) |
|
} |
|
}) |
|
} |
|
return frag; |
|
}, |
|
|
|
_createFooterCells: function (items, columnSize, TDs, Ws) { |
|
var o = this.options; |
|
var frag = this._createCells(items, columnSize, [], TDs, Ws, 0); |
|
return frag; |
|
}, |
|
|
|
_createColGroup: function (columnSize, store, widgets) { |
|
var self = this, o = this.options; |
|
this.colgroup = this._colgroup(); |
|
this.colgroup.element.append(this._createColGroupCells(columnSize, store, widgets)); |
|
return this.colgroup; |
|
}, |
|
|
|
_createHeader: function () { |
|
var self = this, o = this.options; |
|
if (o.header === false) { |
|
return; |
|
} |
|
this.header = this._header(); |
|
this.header.element.append(this._createHeaderCells(o.header, null, null, this.headerTds, this.headerItems)); |
|
return this.header; |
|
}, |
|
|
|
_createFooter: function (columnSize, store, widgets) { |
|
var self = this, o = this.options; |
|
if (o.footer === false) { |
|
return; |
|
} |
|
this.footer = this._footer(); |
|
this.footer.element.append(this._createFooterCells(o.footer, null, this.footerTds, this.footerItems)); |
|
return this.footer; |
|
}, |
|
|
|
_createBody: function () { |
|
var self = this, o = this.options; |
|
this.body = this._body(); |
|
this.body.element.append(this._createCells(o.items, null, null, this.bodyTds, this.bodyItems)); |
|
return this.body; |
|
}, |
|
|
|
_createNormalTable: function () { |
|
var self = this, o = this.options, table = this._table(); |
|
this.colgroupTds = {}; |
|
this.headerTds = {}; |
|
this.footerTds = {}; |
|
this.bodyTds = {}; |
|
|
|
this.headerItems = {}; |
|
this.footerItems = {}; |
|
this.bodyItems = {}; |
|
var colgroup = this._createColGroup(null, this.colgroupTds); |
|
var header = this._createHeader(); |
|
var footer = this._createFooter(); |
|
var body = this._createBody(); |
|
|
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
element: table, |
|
items: [colgroup, header, footer, body] |
|
}); |
|
|
|
var w = BI.sum(this.options.columnSize) || undefined; |
|
w = this._calculateWidth(w); |
|
if (BI.isNumeric(w) && w > 1) { |
|
w += o.columnSize.length; |
|
} |
|
this.tableContainer = BI.createWidget({ |
|
type: "bi.adaptive", |
|
width: this._calculateWidth(w), |
|
items: [table] |
|
}); |
|
|
|
this.scrollBottomRight = BI.createWidget({ |
|
type: "bi.adaptive", |
|
width: "100%", |
|
height: "100%", |
|
cls: "scroll-bottom-right", |
|
scrollable: true, |
|
items: [this.tableContainer] |
|
}); |
|
|
|
BI.createWidget({ |
|
type: "bi.adaptive", |
|
cls: "bottom-right", |
|
element: this, |
|
scrollable: false, |
|
items: [this.scrollBottomRight] |
|
}); |
|
|
|
this._resize = function () { |
|
if (self.element.is(":visible")) { |
|
self.setColumnSize(o.columnSize); |
|
} |
|
}; |
|
|
|
this._initNormalScroll(); |
|
BI.Resizers.add(this.getName(), function (e) { |
|
if (self.element.is(":visible") && BI.isWindow(e.target)) { |
|
self._resize(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_RESIZE); |
|
} |
|
}); |
|
BI.nextTick(function () { |
|
if (self.element.is(":visible")) { |
|
self.fireEvent(BI.Table.EVENT_TABLE_AFTER_INIT); |
|
} |
|
}); |
|
}, |
|
|
|
_initNormalScroll: function () { |
|
var self = this; |
|
var scrolling, scrollX; |
|
this.scrollBottomRight.element.mousewheel(function (event, delta, deltaX, deltaY) { |
|
var inf = self._getScrollOffsetAndDur(event); |
|
if (deltaY < 0 || deltaY > 0) { |
|
var ele = self.scrollBottomRight.element; |
|
if (scrolling) { |
|
ele[0].scrollTop = scrolling; |
|
} |
|
|
|
scrolling = ele[0].scrollTop - delta * inf.offset; |
|
var stopPropagation = false; |
|
var st = ele[0].scrollTop; |
|
ele[0].scrollTop = scrolling; |
|
if (ele[0].scrollTop !== st) { |
|
stopPropagation = true; |
|
} |
|
ele[0].scrollTop = st; |
|
self._animateScrollTo(ele, ele[0].scrollTop, scrolling, inf.dur, "linear", { |
|
onStart: function () { |
|
}, |
|
onUpdate: function (top) { |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, top); |
|
}, |
|
onComplete: function () { |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, scrolling); |
|
scrolling = null; |
|
} |
|
}); |
|
//var scrollTop = self.scrollBottomRight.element[0].scrollTop = self.scrollBottomRight.element[0].scrollTop - delta * offset; |
|
//self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, scrollTop); |
|
if (stopPropagation === true) { |
|
event.stopPropagation(); |
|
return false; |
|
} |
|
} |
|
}); |
|
var scrollTop = 0, scrollLeft = 0; |
|
this.scrollBottomRight.element.scroll(function (e) { |
|
var change = false; |
|
var scrollElement = self.scrollBottomRight.element; |
|
if (scrollElement.scrollTop() != scrollTop) { |
|
if (Math.abs(scrollElement.scrollTop() - scrollTop) > 0.1) { |
|
e.stopPropagation(); |
|
change = true; |
|
} |
|
scrollTop = scrollElement.scrollTop(); |
|
} |
|
if (scrollElement.scrollLeft() != scrollLeft) { |
|
if (Math.abs(scrollElement.scrollLeft() - scrollLeft) > 0.1) { |
|
e.stopPropagation(); |
|
change = true; |
|
} |
|
scrollLeft = scrollElement.scrollLeft(); |
|
} |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL); |
|
if (change === true) { |
|
e.stopPropagation(); |
|
//return false; |
|
} |
|
return false; |
|
}); |
|
}, |
|
|
|
_split: function (items) { |
|
var o = this.options, left = [], right = [], isRight = this._isRightFreeze(); |
|
BI.each(items, function (i, rows) { |
|
left.push([]); |
|
right.push([]); |
|
BI.each(rows, function (j, cell) { |
|
if (o.freezeCols.contains(j)) { |
|
(isRight ? right : left)[i].push(cell); |
|
} else { |
|
(isRight ? left : right)[i].push(cell); |
|
} |
|
}) |
|
}); |
|
return { |
|
left: left, |
|
right: right |
|
} |
|
}, |
|
|
|
_table: function () { |
|
return BI.createWidget({ |
|
type: "bi.layout", |
|
tagName: "table", |
|
cls: "table", |
|
attribute: {"cellspacing": 0, "cellpadding": 0} |
|
}); |
|
}, |
|
|
|
_header: function () { |
|
return BI.createWidget({ |
|
type: "bi.layout", |
|
cls: "header", |
|
tagName: "thead" |
|
}); |
|
}, |
|
|
|
_footer: function () { |
|
return BI.createWidget({ |
|
type: "bi.layout", |
|
cls: "footer", |
|
tagName: "tfoot" |
|
}); |
|
}, |
|
|
|
_body: function () { |
|
return BI.createWidget({ |
|
type: "bi.layout", |
|
tagName: "tbody", |
|
cls: "body" |
|
}); |
|
}, |
|
|
|
_colgroup: function () { |
|
return BI.createWidget({ |
|
type: "bi.layout", |
|
tagName: "colgroup" |
|
}); |
|
}, |
|
|
|
_init: function () { |
|
BI.Table.superclass._init.apply(this, arguments); |
|
|
|
this.populate(this.options.items); |
|
}, |
|
|
|
setColumnSize: function (columnSize) { |
|
var self = this, o = this.options; |
|
var isRight = this._isRightFreeze(); |
|
o.columnSize = columnSize || []; |
|
if (o.isNeedFreeze) { |
|
var columnLeft = []; |
|
var columnRight = []; |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.freezeCols.contains(i)) { |
|
isRight ? columnRight.push(size) : columnLeft.push(size); |
|
} else { |
|
isRight ? columnLeft.push(size) : columnRight.push(size); |
|
} |
|
}); |
|
var topleft = 0, topright = 1, bottomleft = 2, bottomright = 3; |
|
var run = function (direction) { |
|
var colgroupTds, bodyTds, bodyItems, sizes; |
|
switch (direction) { |
|
case topleft: |
|
colgroupTds = self.topLeftColGroupTds; |
|
bodyTds = self.topLeftBodyTds; |
|
bodyItems = self.topLeftBodyItems; |
|
sizes = columnLeft; |
|
break; |
|
case topright: |
|
colgroupTds = self.topRightColGroupTds; |
|
bodyTds = self.topRightBodyTds; |
|
bodyItems = self.topRightBodyItems; |
|
sizes = columnRight; |
|
break; |
|
case bottomleft: |
|
colgroupTds = self.bottomLeftColGroupTds; |
|
bodyTds = self.bottomLeftBodyTds; |
|
bodyItems = self.bottomLeftBodyItems; |
|
sizes = columnLeft; |
|
break; |
|
case bottomright: |
|
colgroupTds = self.bottomRightColGroupTds; |
|
bodyTds = self.bottomRightBodyTds; |
|
bodyItems = self.bottomRightBodyItems; |
|
sizes = columnRight; |
|
break; |
|
} |
|
BI.each(colgroupTds, function (i, colgroup) { |
|
var width = colgroup.attr("width") | 0; |
|
if (width !== sizes[i]) { |
|
var w = self._calculateWidth(sizes[i]); |
|
colgroup.attr("width", w).css("width", w); |
|
BI.each(bodyTds, function (j, items) { |
|
if (items[i]) { |
|
if (items[i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(sizes, function (t, s) { |
|
if (items[i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += items[i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} |
|
} |
|
}); |
|
BI.each(bodyItems, function (j, items) { |
|
if (items[i]) { |
|
if (bodyTds[j][i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(sizes, function (t, s) { |
|
if (bodyTds[j][i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += bodyTds[j][i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].element.attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (BI.isNumeric(w)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].element.attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
}) |
|
}; |
|
run(topleft); |
|
run(topright); |
|
run(bottomleft); |
|
run(bottomright); |
|
|
|
var lw = 0, rw = 0; |
|
this.columnLeft = []; |
|
this.columnRight = []; |
|
BI.each(o.columnSize, function (i, size) { |
|
if (o.freezeCols.contains(i)) { |
|
lw += size; |
|
self[isRight ? "columnRight" : "columnLeft"].push(size); |
|
} else { |
|
rw += size; |
|
self[isRight ? "columnLeft" : "columnRight"].push(size); |
|
} |
|
}); |
|
lw = this._calculateWidth(lw); |
|
rw = this._calculateWidth(rw); |
|
|
|
if (BI.isNumeric(lw)) { |
|
lw = BI.parseFloat(lw) + o.freezeCols.length; |
|
} |
|
if (BI.isNumeric(rw)) { |
|
rw = BI.parseFloat(rw) + o.columnSize.length - o.freezeCols.length; |
|
} |
|
this.topLeftContainer.element.width(isRight ? rw : lw); |
|
this.bottomLeftContainer.element.width(isRight ? rw : lw); |
|
this.topRightContainer.element.width(isRight ? lw : rw); |
|
this.bottomRightContainer.element.width(isRight ? lw : rw); |
|
this.scrollTopLeft.element[0].scrollLeft = this.scrollBottomLeft.element[0].scrollLeft; |
|
this.scrollTopRight.element[0].scrollLeft = this.scrollBottomRight.element[0].scrollLeft; |
|
if (o.isNeedResize && o.isResizeAdapt) { |
|
var leftWidth = BI.sum(o.freezeCols, function (i, col) { |
|
return o.columnSize[col] > 1 ? o.columnSize[col] + 1 : o.columnSize[col]; |
|
}); |
|
this.partitions.attr("columnSize", isRight ? ['fill', leftWidth] : [leftWidth, 'fill']); |
|
this.partitions.resize(); |
|
} |
|
} else { |
|
BI.each(this.colgroupTds, function (i, colgroup) { |
|
var width = colgroup.attr("width") | 0; |
|
if (width !== o.columnSize[i]) { |
|
var w = self._calculateWidth(o.columnSize[i]); |
|
colgroup.attr("width", w).css("width", w); |
|
BI.each(self.bodyTds, function (j, items) { |
|
if (items[i]) { |
|
if (items[i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(o.columnSize, function (t, s) { |
|
if (items[i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += items[i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} |
|
} |
|
}); |
|
BI.each(self.headerTds, function (j, items) { |
|
if (items[i]) { |
|
if (items[i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(o.columnSize, function (t, s) { |
|
if (items[i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += items[i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} |
|
} |
|
}); |
|
BI.each(self.footerTds, function (j, items) { |
|
if (items[i]) { |
|
if (items[i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(o.columnSize, function (t, s) { |
|
if (items[i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += items[i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} |
|
} |
|
}); |
|
BI.each(self.bodyItems, function (j, items) { |
|
if (items[i]) { |
|
if (self.bodyTds[j][i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(o.columnSize, function (t, s) { |
|
if (self.bodyTds[j][i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += self.bodyTds[j][i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].element.attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (BI.isNumeric(w)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].element.attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} |
|
} |
|
}); |
|
BI.each(self.headerItems, function (j, items) { |
|
if (items[i]) { |
|
if (self.headerTds[j][i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(o.columnSize, function (t, s) { |
|
if (self.headerTds[j][i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += self.headerTds[j][i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].element.attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (BI.isNumeric(w)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].element.attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} |
|
} |
|
}); |
|
BI.each(self.footerItems, function (j, items) { |
|
if (items[i]) { |
|
if (self.footerTds[j][i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(o.columnSize, function (t, s) { |
|
if (self.footerTds[j][i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += self.footerTds[j][i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].element.attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (BI.isNumeric(w)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].element.attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
}); |
|
var w = this._calculateWidth(BI.sum(o.columnSize)); |
|
if (w > 1.05) { |
|
w += o.columnSize.length; |
|
} |
|
this.tableContainer.element.width(w); |
|
} |
|
}, |
|
|
|
getColumnSize: function () { |
|
return this.options.columnSize; |
|
}, |
|
|
|
getCalculateColumnSize: function () { |
|
var self = this, o = this.options; |
|
var columnSize = []; |
|
if (o.isNeedFreeze === true) { |
|
if (BI.size(this.bottomLeftBodyTds) > 0 || BI.size(this.bottomRightBodyTds) > 0) { |
|
if (!BI.any(this.bottomLeftBodyTds, function (i, tds) { |
|
if (!BI.any(tds, function (i, item) { |
|
if (item.__mergeCols.length > 1) { |
|
return true; |
|
} |
|
})) { |
|
BI.each(tds, function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(tds) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
return true; |
|
} |
|
})) { |
|
BI.each(this.bottomLeftBodyTds[0], function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(self.bottomLeftBodyTds[0]) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
} |
|
if (!BI.any(this.bottomRightBodyTds, function (i, tds) { |
|
if (!BI.any(tds, function (i, item) { |
|
if (item.__mergeCols.length > 1) { |
|
return true; |
|
} |
|
})) { |
|
BI.each(tds, function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(tds) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
return true; |
|
} |
|
})) { |
|
BI.each(this.bottomRightBodyTds[0], function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(self.bottomRightBodyTds[0]) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
} |
|
return columnSize; |
|
} |
|
if (!BI.any(this.topLeftBodyTds, function (i, tds) { |
|
if (!BI.any(tds, function (i, item) { |
|
if (item.__mergeCols.length > 1) { |
|
return true; |
|
} |
|
})) { |
|
BI.each(tds, function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(tds) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
return true; |
|
} |
|
})) { |
|
BI.each(this.topLeftBodyTds[BI.size(this.topLeftBodyTds) - 1], function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(self.topLeftBodyTds[BI.size(self.topLeftBodyTds) - 1]) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
} |
|
if (!BI.any(this.topRightBodyTds, function (i, tds) { |
|
if (!BI.any(tds, function (i, item) { |
|
if (item.__mergeCols.length > 1) { |
|
return true; |
|
} |
|
})) { |
|
BI.each(tds, function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(tds) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
return true; |
|
} |
|
})) { |
|
BI.each(this.topRightBodyTds[BI.size(this.topRightBodyTds) - 1], function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(self.topRightBodyTds[BI.size(self.topRightBodyTds) - 1]) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
} |
|
} else { |
|
BI.each(this.headerTds[BI.size(this.headerTds) - 1], function (i, item) { |
|
var width = item.width() / item.__mergeCols.length; |
|
if (i == BI.size(self.headerTds[BI.size(self.headerTds) - 1]) - 1) { |
|
width++; |
|
} |
|
columnSize.push(width); |
|
}); |
|
} |
|
return columnSize; |
|
}, |
|
|
|
setHeaderColumnSize: function (columnSize) { |
|
var self = this, o = this.options; |
|
var isRight = this._isRightFreeze(); |
|
if (o.isNeedFreeze) { |
|
var columnLeft = []; |
|
var columnRight = []; |
|
BI.each(columnSize, function (i, size) { |
|
if (o.freezeCols.contains(i)) { |
|
isRight ? columnRight.push(size) : columnLeft.push(size); |
|
} else { |
|
isRight ? columnLeft.push(size) : columnRight.push(size); |
|
} |
|
}); |
|
var topleft = 0, topright = 1; |
|
var run = function (direction) { |
|
var colgroupTds, bodyTds, bodyItems, sizes; |
|
switch (direction) { |
|
case topleft: |
|
colgroupTds = self.topLeftColGroupTds; |
|
bodyTds = self.topLeftBodyTds; |
|
bodyItems = self.topLeftBodyItems; |
|
sizes = columnLeft; |
|
break; |
|
case topright: |
|
colgroupTds = self.topRightColGroupTds; |
|
bodyTds = self.topRightBodyTds; |
|
bodyItems = self.topRightBodyItems; |
|
sizes = columnRight; |
|
break; |
|
} |
|
BI.each(colgroupTds, function (i, colgroup) { |
|
var width = colgroup.attr("width") | 0; |
|
if (width !== sizes[i]) { |
|
var w = self._calculateWidth(sizes[i]); |
|
colgroup.attr("width", w).css("width", w); |
|
BI.each(bodyTds, function (j, items) { |
|
if (items[i]) { |
|
if (items[i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(sizes, function (t, s) { |
|
if (items[i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += items[i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} |
|
} |
|
}); |
|
BI.each(bodyItems, function (j, items) { |
|
if (items[i]) { |
|
if (bodyTds[j][i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(sizes, function (t, s) { |
|
if (bodyTds[j][i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += bodyTds[j][i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].element.attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (BI.isNumeric(w)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].element.attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
}) |
|
}; |
|
run(topleft); |
|
run(topright); |
|
|
|
var lw = 0, rw = 0; |
|
BI.each(columnSize, function (i, size) { |
|
if (o.freezeCols.contains(i)) { |
|
lw += size; |
|
} else { |
|
rw += size; |
|
} |
|
}); |
|
lw = this._calculateWidth(lw); |
|
rw = this._calculateWidth(rw); |
|
|
|
if (BI.isNumeric(lw)) { |
|
lw = BI.parseFloat(lw) + o.freezeCols.length; |
|
} |
|
if (BI.isNumeric(rw)) { |
|
rw = BI.parseFloat(rw) + columnSize.length - o.freezeCols.length; |
|
} |
|
this.topLeftContainer.element.width(isRight ? rw : lw); |
|
this.topRightContainer.element.width(isRight ? lw : rw); |
|
this.scrollTopLeft.element[0].scrollLeft = this.scrollBottomLeft.element[0].scrollLeft; |
|
this.scrollTopRight.element[0].scrollLeft = this.scrollBottomRight.element[0].scrollLeft; |
|
if (o.isNeedResize && o.isResizeAdapt) { |
|
var leftWidth = BI.sum(o.freezeCols, function (i, col) { |
|
return columnSize[col] > 1 ? columnSize[col] + 1 : columnSize[col]; |
|
}); |
|
this.partitions.attr("columnSize", isRight ? ['fill', leftWidth] : [leftWidth, 'fill']); |
|
this.partitions.resize(); |
|
} |
|
} else { |
|
BI.each(this.colgroupTds, function (i, colgroup) { |
|
var width = colgroup.attr("width") | 0; |
|
if (width !== columnSize[i]) { |
|
var w = self._calculateWidth(columnSize[i]); |
|
colgroup.attr("width", w).css("width", w); |
|
BI.each(self.headerTds, function (j, items) { |
|
if (items[i]) { |
|
if (items[i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(columnSize, function (t, s) { |
|
if (items[i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += items[i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].element.attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (i == BI.size(items) - 1) { |
|
items[i].attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].attr("width", w).css("width", w); |
|
} |
|
} |
|
} |
|
}); |
|
BI.each(self.headerItems, function (j, items) { |
|
if (items[i]) { |
|
if (self.headerTds[j][i].__mergeCols.length > 1) { |
|
var wid = 0; |
|
BI.each(columnSize, function (t, s) { |
|
if (self.headerTds[j][i].__mergeCols.contains(t)) { |
|
wid += s; |
|
} |
|
}); |
|
wid = self._calculateWidth(wid); |
|
if (wid > 1) { |
|
wid += self.headerTds[j][i].__mergeCols.length - 1; |
|
} |
|
if (BI.isNumeric(wid)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", wid - 1).css("width", wid - 1); |
|
} else { |
|
items[i].element.attr("width", wid).css("width", wid); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} else { |
|
if (BI.isNumeric(w)) { |
|
if (i == BI.size(items) - 1) { |
|
items[i].element.attr("width", w - 1).css("width", w - 1); |
|
} else { |
|
items[i].element.attr("width", w).css("width", w); |
|
} |
|
} else { |
|
items[i].element.attr("width", "").css("width", ""); |
|
} |
|
} |
|
} |
|
}); |
|
} |
|
}); |
|
var cW = this._calculateWidth(BI.sum(columnSize)); |
|
if (cW > 1.05) { |
|
cW = cW + columnSize.length; |
|
} |
|
this.tableContainer.element.width(cW); |
|
} |
|
}, |
|
|
|
setRegionColumnSize: function (columnSize) { |
|
var self = this, o = this.options; |
|
o.regionColumnSize = columnSize; |
|
if (o.freezeCols.length === 0) { |
|
if (o.isNeedFreeze) { |
|
this.partitions.attr("columnSize", this._isRightFreeze() ? ['fill', 0] : [0, 'fill']); |
|
this.partitions.resize(); |
|
} else { |
|
this.tableContainer.element.width(columnSize[0]); |
|
} |
|
} else if (o.freezeCols.length > 0 && o.freezeCols.length < o.columnSize.length) { |
|
if (o.isNeedFreeze) { |
|
this.partitions.attr("columnSize", columnSize); |
|
this.partitions.resize(); |
|
} else { |
|
this.tableContainer.element.width(columnSize[0]); |
|
} |
|
} else { |
|
if (o.isNeedFreeze) { |
|
this.partitions.attr("columnSize", this._isRightFreeze() ? [0, 'fill'] : ['fill', 0]); |
|
this.partitions.resize(); |
|
} else { |
|
this.tableContainer.element.width(columnSize[0]); |
|
} |
|
} |
|
}, |
|
|
|
getRegionColumnSize: function () { |
|
return this.options.regionColumnSize; |
|
}, |
|
|
|
getCalculateRegionColumnSize: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return [this.scrollBottomLeft.element.width(), this.scrollBottomRight.element.width()]; |
|
} |
|
return [this.scrollBottomRight.element.width()]; |
|
}, |
|
|
|
getCalculateRegionRowSize: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return [this.scrollTopRight.element.height(), this.scrollBottomRight.element.height()]; |
|
} |
|
return [this.scrollBottomRight.element.height()]; |
|
}, |
|
|
|
getClientRegionColumnSize: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return [this.scrollBottomLeft.element[0].clientWidth, this.scrollBottomRight.element[0].clientWidth]; |
|
} |
|
return [this.scrollBottomRight.element[0].clientWidth]; |
|
}, |
|
|
|
getClientRegionRowSize: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return [this.scrollBottomLeft.element[0].clientHeight, this.scrollBottomRight.element[0].clientHeight]; |
|
} |
|
return [this.scrollBottomRight.element[0].clientHeight]; |
|
}, |
|
|
|
getScrollRegionColumnSize: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return [this.scrollBottomLeft.element[0].scrollWidth, this.scrollBottomRight.element[0].scrollWidth]; |
|
} |
|
return [this.scrollBottomRight.element[0].scrollWidth]; |
|
}, |
|
|
|
getScrollRegionRowSize: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
if (o.freezeCols.length < o.columnSize.length) { |
|
return [this.scrollTopRight.element[0].scrollHeight, this.scrollBottomRight.element[0].scrollHeight]; |
|
} else { |
|
return [this.scrollTopLeft.element[0].scrollHeight, this.scrollBottomLeft.element[0].scrollHeight]; |
|
} |
|
} |
|
return [this.scrollBottomRight.element[0].scrollHeight]; |
|
}, |
|
|
|
hasVerticalScroll: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return this.scrollBottomRight.element.hasVerticalScroll() || this.scrollBottomLeft.element.hasVerticalScroll(); |
|
} |
|
return this.scrollBottomRight.element.hasVerticalScroll(); |
|
}, |
|
|
|
setVerticalScroll: function (scrollTop) { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
if (this.scrollBottomRight.element[0].scrollTop !== scrollTop) { |
|
this.scrollBottomRight.element[0].scrollTop = scrollTop; |
|
} |
|
if (this.scrollBottomLeft.element[0].scrollTop !== scrollTop) { |
|
this.scrollBottomLeft.element[0].scrollTop = scrollTop; |
|
} |
|
} else { |
|
if (this.scrollBottomRight.element[0].scrollTop !== scrollTop) { |
|
this.scrollBottomRight.element[0].scrollTop = scrollTop; |
|
} |
|
} |
|
}, |
|
|
|
setLeftHorizontalScroll: function (scrollLeft) { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
if (this.scrollBottomLeft.element[0].scrollLeft !== scrollLeft) { |
|
this.scrollBottomLeft.element[0].scrollLeft = scrollLeft; |
|
} |
|
if (this.scrollTopLeft.element[0].scrollLeft !== scrollLeft) { |
|
this.scrollTopLeft.element[0].scrollLeft = scrollLeft; |
|
} |
|
} else { |
|
if (this.scrollBottomRight.element[0].scrollLeft !== scrollLeft) { |
|
this.scrollBottomRight.element[0].scrollLeft = scrollLeft; |
|
} |
|
} |
|
}, |
|
|
|
setRightHorizontalScroll: function (scrollLeft) { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
if (this.scrollBottomRight.element[0].scrollLeft !== scrollLeft) { |
|
this.scrollBottomRight.element[0].scrollLeft = scrollLeft; |
|
} |
|
if (this.scrollTopRight.element[0].scrollLeft !== scrollLeft) { |
|
this.scrollTopRight.element[0].scrollLeft = scrollLeft; |
|
} |
|
} else { |
|
if (this.scrollBottomRight.element[0].scrollLeft !== scrollLeft) { |
|
this.scrollBottomRight.element[0].scrollLeft = scrollLeft; |
|
} |
|
} |
|
}, |
|
|
|
getVerticalScroll: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return this.scrollBottomRight.element[0].scrollTop || this.scrollBottomLeft.element[0].scrollTop; |
|
} |
|
return this.scrollBottomRight.element[0].scrollTop; |
|
}, |
|
|
|
getLeftHorizontalScroll: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return this.scrollBottomLeft.element[0].scrollLeft; |
|
} |
|
return this.scrollBottomRight.element[0].scrollLeft; |
|
}, |
|
|
|
getRightHorizontalScroll: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return this.scrollBottomRight.element[0].scrollLeft; |
|
} |
|
return this.scrollBottomRight.element[0].scrollLeft; |
|
}, |
|
|
|
getColumns: function () { |
|
var o = this.options; |
|
if (o.isNeedFreeze) { |
|
return { |
|
topLeft: this.topLeftBodyItems, |
|
topRight: this.topRightBodyItems, |
|
bottomLeft: this.bottomLeftBodyItems, |
|
bottomRight: this.bottomRightBodyItems |
|
} |
|
} else { |
|
return { |
|
header: this.headerItems, |
|
body: this.bodyItems, |
|
footer: this.footerItems |
|
} |
|
} |
|
}, |
|
|
|
populate: function (items, header) { |
|
this.options.items = items || []; |
|
if (header) { |
|
this.options.header = header; |
|
} |
|
this.empty(); |
|
if (this.options.isNeedFreeze) { |
|
this._createFreezeTable(); |
|
} else { |
|
this._createNormalTable(); |
|
} |
|
}, |
|
|
|
empty: function () { |
|
BI.Resizers.remove(this.getName()); |
|
BI.Table.superclass.empty.apply(this, arguments); |
|
}, |
|
|
|
destroy: function () { |
|
BI.Table.superclass.destroy.apply(this, arguments); |
|
} |
|
}) |
|
; |
|
BI.Table.EVENT_TABLE_AFTER_INIT = "EVENT_TABLE_AFTER_INIT"; |
|
BI.Table.EVENT_TABLE_RESIZE = "EVENT_TABLE_RESIZE"; |
|
BI.Table.EVENT_TABLE_SCROLL = "EVENT_TABLE_SCROLL"; |
|
BI.Table.EVENT_TABLE_BEFORE_COLUMN_RESIZE = "EVENT_TABLE_BEFORE_COLUMN_RESIZE"; |
|
BI.Table.EVENT_TABLE_COLUMN_RESIZE = "EVENT_TABLE_COLUMN_RESIZE"; |
|
BI.Table.EVENT_TABLE_AFTER_COLUMN_RESIZE = "EVENT_TABLE_AFTER_COLUMN_RESIZE"; |
|
|
|
BI.Table.EVENT_TABLE_BEFORE_REGION_RESIZE = "EVENT_TABLE_BEFORE_REGION_RESIZE"; |
|
BI.Table.EVENT_TABLE_REGION_RESIZE = "EVENT_TABLE_REGION_RESIZE"; |
|
BI.Table.EVENT_TABLE_AFTER_REGION_RESIZE = "EVENT_TABLE_AFTER_REGION_RESIZE"; |
|
$.shortcut("bi.table_view", BI.Table); |
|
/** |
|
* |
|
* 表格单元格 |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.ResizableTableCell |
|
* @extends BI.Widget |
|
*/ |
|
BI.ResizableTableCell = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.ResizableTableCell.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-resizable-table-cell", |
|
cell: {}, |
|
start: BI.emptyFn, |
|
resize: BI.emptyFn, |
|
stop: BI.emptyFn |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.ResizableTableCell.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.cell = BI.createWidget(BI.extend({type: "bi.label"}, o.cell, {width: o.width, height: o.height})); |
|
|
|
var startDrag = false; |
|
var size = 0, offset = 0, defaultSize = o.width; |
|
var mouseMoveTracker = new BI.MouseMoveTracker(function (deltaX, deltaY) { |
|
if (mouseMoveTracker.isDragging()) { |
|
startDrag = true; |
|
offset += deltaX; |
|
size = BI.clamp(defaultSize + offset, 15, Number.MAX_VALUE); |
|
self.handler.element.addClass("dragging"); |
|
o.resize(size); |
|
} |
|
}, function () { |
|
if (startDrag === true) { |
|
size = BI.clamp(size, 15, Number.MAX_VALUE); |
|
o.stop(size); |
|
size = 0; |
|
offset = 0; |
|
defaultSize = o.width; |
|
self.handler.element.removeClass("dragging"); |
|
startDrag = false; |
|
} |
|
mouseMoveTracker.releaseMouseMoves(); |
|
}, document); |
|
this.handler = BI.createWidget({ |
|
type: "bi.absolute", |
|
cls: "resizable-table-cell-resizer-container", |
|
width: 6, |
|
items: [{ |
|
el: { |
|
type: "bi.layout", |
|
cls: "resizable-table-cell-resizer-knob", |
|
width: 4 |
|
}, |
|
right: 0, |
|
top: 0, |
|
bottom: 0 |
|
}] |
|
}); |
|
this.handler.element.on("mousedown", function (event) { |
|
defaultSize = o.width; |
|
mouseMoveTracker.captureMouseMoves(event); |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.cell, |
|
left: 0, |
|
right: 0, |
|
top: 0, |
|
bottom: 0 |
|
}, { |
|
el: this.handler, |
|
right: 0, |
|
top: 0, |
|
bottom: 0 |
|
}] |
|
}) |
|
}, |
|
|
|
setWidth: function (width) { |
|
BI.ResizableTableCell.superclass.setWidth.apply(this, arguments); |
|
var o = this.options; |
|
this.cell.setWidth(o.width); |
|
}, |
|
|
|
setHeight: function (height) { |
|
BI.ResizableTableCell.superclass.setHeight.apply(this, arguments); |
|
var o = this.options; |
|
this.cell.setHeight(o.height); |
|
} |
|
}); |
|
$.shortcut("bi.resizable_table_cell", BI.ResizableTableCell);/** |
|
* |
|
* 可调整列宽的grid表格 |
|
* |
|
* Created by GUY on 2016/1/12. |
|
* @class BI.ResizableTable |
|
* @extends BI.Widget |
|
*/ |
|
BI.ResizableTable = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.ResizableTable.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-resizable-table", |
|
el: { |
|
type: "bi.grid_table" |
|
}, |
|
isNeedFreeze: false, |
|
isNeedResize: true, |
|
isResizeAdapt: false, |
|
headerRowSize: 25, |
|
rowSize: 25, |
|
isNeedMerge: true,//是否需要合并单元格 |
|
mergeCols: [], |
|
mergeRule: BI.emptyFn, |
|
columnSize: [], |
|
freezeCols: [], |
|
header: [], |
|
items: [], |
|
regionColumnSize: [] |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.ResizableTable.superclass._init.apply(this, arguments); |
|
var self = this, o = this.options; |
|
this.resizer = BI.createWidget({ |
|
type: "bi.layout", |
|
cls: "resizable-table-resizer", |
|
invisible: true, |
|
width: 2 |
|
}); |
|
this.regionResizerHandler = this._createResizerHandler(); |
|
this.table = BI.createWidget(o.el, { |
|
type: "bi.grid_table", |
|
element: this, |
|
width: o.width, |
|
height: o.height, |
|
headerRowSize: o.headerRowSize, |
|
rowSize: o.rowSize, |
|
columnSize: o.columnSize, |
|
|
|
isNeedFreeze: o.isNeedFreeze, |
|
freezeCols: o.freezeCols, |
|
isNeedMerge: o.isNeedMerge, |
|
mergeCols: o.mergeCols, |
|
mergeRule: BI.bind(this._mergeRule, this), |
|
|
|
header: this._formatHeader(o.header), |
|
items: o.items, |
|
regionColumnSize: o.regionColumnSize |
|
}); |
|
this.table.on(BI.Table.EVENT_TABLE_SCROLL, function () { |
|
self.fireEvent(BI.Table.EVENT_TABLE_SCROLL, arguments); |
|
}); |
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: this, |
|
items: [{ |
|
el: this.regionResizerHandler, |
|
left: 0, |
|
top: 0, |
|
bottom: 0 |
|
}, { |
|
el: this.resizer, |
|
left: 0, |
|
top: 0 |
|
}] |
|
}); |
|
this._populate(); |
|
}, |
|
|
|
_mergeRule: function (row1, row2) { |
|
var o = this.options; |
|
if (row1.type === "bi.resizable_table_cell") { |
|
row1 = row1.cell; |
|
} |
|
if (row2.type === "bi.resizable_table_cell") { |
|
row2 = row2.cell; |
|
} |
|
return o.mergeRule(row1, row2); |
|
}, |
|
|
|
_createResizerHandler: function () { |
|
var self = this, o = this.options; |
|
var regionResizerHandler = BI.createWidget({ |
|
type: "bi.absolute", |
|
cls: "resizable-table-region-resizer", |
|
invisible: true, |
|
width: 6, |
|
items: [{ |
|
el: { |
|
type: "bi.layout", |
|
width: 2, |
|
cls: "resizable-table-region-resizer-knob" |
|
}, |
|
left: 2, |
|
top: 0, |
|
bottom: 0 |
|
}] |
|
}); |
|
var size = 0, offset = 0, defaultSize = 0, start = false; |
|
var mouseMoveTracker = new BI.MouseMoveTracker(function (deltaX, deltaY) { |
|
if (mouseMoveTracker.isDragging()) { |
|
start = true; |
|
offset += deltaX; |
|
size = BI.clamp(defaultSize + offset, 15, o.width - 15); |
|
|
|
self.regionResizerHandler.element.addClass("dragging"); |
|
self._setRegionResizerHandlerPosition(size - 3, 0); |
|
} |
|
|
|
}, function () { |
|
if (start === true) { |
|
o.regionColumnSize[0] = BI.clamp(size, 15, o.width - 15); |
|
self.table.setRegionColumnSize(o.regionColumnSize); |
|
if (o.isResizeAdapt === true) { |
|
var freezeColumnSize = self._getFreezeColumnSize(); |
|
o.columnSize[self._getFreezeColLength() - 1] += o.regionColumnSize[0] - freezeColumnSize; |
|
self.table.setColumnSize(o.columnSize); |
|
} |
|
self.table.populate(); |
|
self._populate(); |
|
self.regionResizerHandler.element.removeClass("dragging"); |
|
self.fireEvent(BI.Table.EVENT_TABLE_AFTER_REGION_RESIZE); |
|
start = false; |
|
} |
|
mouseMoveTracker.releaseMouseMoves(); |
|
}, document); |
|
regionResizerHandler.element.on("mousedown", function (event) { |
|
defaultSize = size = self._getRegionSize(); |
|
offset = 0; |
|
self._setResizerPosition(0, 0); |
|
mouseMoveTracker.captureMouseMoves(event); |
|
}); |
|
return regionResizerHandler; |
|
}, |
|
|
|
_setResizerPosition: function (left, top) { |
|
this.resizer.element.css({ |
|
left: left + "px", |
|
top: top + "px" |
|
}); |
|
}, |
|
|
|
_setRegionResizerHandlerPosition: function (left, top) { |
|
this.regionResizerHandler.element.css({ |
|
left: left + "px", |
|
top: top + "px" |
|
}); |
|
}, |
|
|
|
_getRegionSize: function () { |
|
var o = this.options; |
|
var regionSize = o.regionColumnSize[0] || 0; |
|
if (o.isNeedFreeze === false || o.freezeCols.length === 0) { |
|
return 0; |
|
} |
|
if (!regionSize) { |
|
BI.each(o.freezeCols, function (i, col) { |
|
regionSize += o.columnSize[col]; |
|
}); |
|
} |
|
return regionSize; |
|
}, |
|
|
|
_getRegionRowSize: function () { |
|
var o = this.options; |
|
return [o.header.length * o.headerRowSize, |
|
Math.min(o.height - o.header.length * o.headerRowSize, o.items.length * o.rowSize)]; |
|
}, |
|
|
|
_getFreezeColLength: function () { |
|
return this.options.freezeCols.length; |
|
}, |
|
|
|
_getFreezeColumnSize: function () { |
|
var columnSize = this.options.columnSize; |
|
var sum = 0; |
|
for (var i = 0, len = this._getFreezeColLength(); i < len; i++) { |
|
sum += columnSize[i]; |
|
} |
|
return sum; |
|
}, |
|
|
|
_getResizerLeft: function (j) { |
|
var left = 0; |
|
var columnSize = this.options.columnSize; |
|
var freezeColLength = this._getFreezeColLength(); |
|
for (var i = (j >= freezeColLength ? freezeColLength : 0); i < j; i++) { |
|
left += columnSize[i] || 0; |
|
} |
|
if (j >= freezeColLength) { |
|
left += this.table.getRegionSize(); |
|
left -= this.table.getRightHorizontalScroll(); |
|
} else { |
|
left -= this.table.getLeftHorizontalScroll(); |
|
} |
|
return left; |
|
}, |
|
|
|
_formatHeader: function (header) { |
|
var self = this, o = this.options; |
|
var result = []; |
|
var resize = function (j, size) { |
|
self.resizer.setVisible(true); |
|
var height = o.headerRowSize + self._getRegionRowSize()[1]; |
|
self.resizer.setHeight(height); |
|
|
|
self._setResizerPosition(self._getResizerLeft(j) + size, (o.header.length - 1) * o.headerRowSize); |
|
}; |
|
var stop = function (j, size) { |
|
self.resizer.setVisible(false); |
|
o.columnSize[j] = size; |
|
self.table.setColumnSize(o.columnSize); |
|
self.table.populate(); |
|
self._populate(); |
|
self.fireEvent(BI.Table.EVENT_TABLE_AFTER_COLUMN_RESIZE); |
|
}; |
|
BI.each(header, function (i, cols) { |
|
if (i === header.length - 1) { |
|
result[i] = []; |
|
BI.each(cols, function (j, col) { |
|
if (j === self._getFreezeColLength() - 1 || j === cols.length - 1) { |
|
result[i][j] = col; |
|
} else { |
|
result[i][j] = { |
|
type: "bi.resizable_table_cell", |
|
cell: col, |
|
resize: BI.bind(resize, null, j), |
|
stop: BI.bind(stop, null, j) |
|
}; |
|
if (o.isNeedMerge) { |
|
var r = i; |
|
while (r > 0 && self._mergeRule(result[r][j], result[r - 1][j])) { |
|
result[r - 1][j] = { |
|
type: "bi.resizable_table_cell", |
|
cell: result[r - 1][j], |
|
resize: BI.bind(resize, null, j), |
|
stop: BI.bind(stop, null, j) |
|
}; |
|
r--; |
|
} |
|
} |
|
} |
|
}); |
|
} else { |
|
result.push(cols); |
|
} |
|
}); |
|
return result; |
|
}, |
|
|
|
_populate: function () { |
|
var o = this.options; |
|
var regionSize = this._getRegionSize(); |
|
if (regionSize > 0) { |
|
this.regionResizerHandler.setVisible(true); |
|
this._setRegionResizerHandlerPosition(regionSize - 3, 0); |
|
} else { |
|
this.regionResizerHandler.setVisible(false); |
|
} |
|
}, |
|
|
|
setWidth: function (width) { |
|
BI.ResizableTable.superclass.setWidth.apply(this, arguments); |
|
this.table.setWidth(width) |
|
}, |
|
|
|
setHeight: function (height) { |
|
BI.ResizableTable.superclass.setHeight.apply(this, arguments); |
|
this.table.setHeight(height); |
|
}, |
|
|
|
setVerticalScroll: function (scrollTop) { |
|
this.table.setVerticalScroll(scrollTop); |
|
}, |
|
|
|
setLeftHorizontalScroll: function (scrollLeft) { |
|
this.table.setLeftHorizontalScroll(scrollLeft); |
|
}, |
|
|
|
setRightHorizontalScroll: function (scrollLeft) { |
|
this.table.setRightHorizontalScroll(scrollLeft); |
|
}, |
|
|
|
setColumnSize: function (columnSize) { |
|
this.options.columnSize = columnSize; |
|
this.table.setColumnSize(columnSize); |
|
}, |
|
|
|
getColumnSize: function () { |
|
return this.table.getColumnSize(); |
|
}, |
|
|
|
setRegionColumnSize: function (columnSize) { |
|
this.options.regionColumnSize = columnSize; |
|
this.table.setRegionColumnSize(columnSize); |
|
}, |
|
|
|
getRegionColumnSize: function () { |
|
return this.table.getRegionColumnSize(); |
|
}, |
|
|
|
getVerticalScroll: function () { |
|
return this.table.getVerticalScroll(); |
|
}, |
|
|
|
getLeftHorizontalScroll: function () { |
|
return this.table.getLeftHorizontalScroll(); |
|
}, |
|
|
|
getRightHorizontalScroll: function () { |
|
return this.table.getRightHorizontalScroll(); |
|
}, |
|
|
|
attr: function () { |
|
BI.ResizableTable.superclass.attr.apply(this, arguments); |
|
this.table.attr.apply(this.table, arguments); |
|
}, |
|
|
|
restore: function () { |
|
this.table.restore(); |
|
}, |
|
|
|
populate: function (items, header) { |
|
if (items) { |
|
this.options.items = items; |
|
} |
|
if (header) { |
|
this.options.header = header; |
|
if (this.options.isNeedResize) { |
|
header = this._formatHeader(header); |
|
} |
|
} |
|
this.table.populate(items, header); |
|
this._populate(); |
|
} |
|
}); |
|
|
|
$.shortcut("bi.resizable_table", BI.ResizableTable);/** |
|
* 组件请求数据超时提示 |
|
* Created by Young's on 2017/2/4. |
|
*/ |
|
BI.TimeoutToast = BI.inherit(BI.Tip, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.TimeoutToast.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-timeout-toast" |
|
}); |
|
}, |
|
|
|
_init: function () { |
|
BI.TimeoutToast.superclass._init.apply(this, arguments); |
|
var self = this; |
|
this.requests = []; |
|
this.toast = BI.createWidget({ |
|
type: "bi.vertical_adapt", |
|
element: this, |
|
items: [{ |
|
type: "bi.label", |
|
text: BI.i18nText("BI-Request_Time_Out_Toast_Tip") |
|
}, { |
|
type: "bi.text_button", |
|
cls: "cancel-button", |
|
width: 60, |
|
height: 22, |
|
text: BI.i18nText("BI-Basic_Cancel"), |
|
title: BI.i18nText("BI-Basic_Cancel"), |
|
handler: function () { |
|
self.cancelAllRequests(); |
|
} |
|
}, { |
|
type: "bi.text_button", |
|
cls: "retry-button", |
|
width: 60, |
|
height: 22, |
|
text: BI.i18nText("BI-Basic_Retry"), |
|
title: BI.i18nText("BI-Basic_Retry"), |
|
handler: function () { |
|
self.toast.element.slideUp(500); |
|
self._retryAll(); |
|
} |
|
}, { |
|
type: "bi.icon_button", |
|
cls: "close-font", |
|
width: 20, |
|
height: 20, |
|
title: BI.i18nText("BI-Basic_Close"), |
|
handler: function () { |
|
self.toast.element.slideUp(500); |
|
} |
|
}], |
|
width: 520, |
|
height: 30, |
|
hgap: 2 |
|
}); |
|
|
|
BI.createWidget({ |
|
type: "bi.absolute", |
|
element: $("body"), |
|
items: [{ |
|
el: this.toast, |
|
left: "50%", |
|
top: 0 |
|
}] |
|
}); |
|
this.toast.element.css({"margin-left": -1 * this.toast.element.outerWidth() / 2}); |
|
this.toast.setVisible(false); |
|
}, |
|
|
|
_retryAll: function () { |
|
var self = this; |
|
var clonedRequests = BI.deepClone(this.requests); |
|
this.requests = []; |
|
BI.each(clonedRequests, function (i, options) { |
|
BI.isFunction(self.callback) && self.callback(options); |
|
}); |
|
}, |
|
|
|
cancelAllRequests: function () { |
|
this.toast.element.slideUp(500); |
|
BI.each(this.requests, function (i, reqArgs) { |
|
if (BI.isNotNull(reqArgs) && BI.isFunction(reqArgs.complete)) { |
|
reqArgs.complete(); |
|
} |
|
}); |
|
this.requests = []; |
|
}, |
|
|
|
setCallback: function (callback) { |
|
this.callback = callback; |
|
}, |
|
|
|
addReq: function (options) { |
|
var self = this; |
|
if (this.requests.length === 0) { |
|
setTimeout(function () { |
|
if (self.requests.contains(options)) { |
|
self.toast.element.slideDown(500); |
|
} |
|
}, 5 * 60 * 1000); //5 min |
|
} |
|
this.requests.push(options); |
|
}, |
|
|
|
removeReq: function (options) { |
|
BI.remove(this.requests, options); |
|
if (this.requests.length === 0) { |
|
this.toast.element.slideUp(500); |
|
} |
|
}, |
|
|
|
hasReq: function (options) { |
|
return this.requests.contains(options); |
|
} |
|
}); |
|
$.shortcut("bi.timeout_toast", BI.TimeoutToast);/** |
|
* |
|
* 自定义树 |
|
* |
|
* Created by GUY on 2015/9/7. |
|
* @class BI.CustomTree |
|
* @extends BI.Single |
|
*/ |
|
BI.CustomTree = BI.inherit(BI.Widget, { |
|
_defaultConfig: function () { |
|
return BI.extend(BI.CustomTree.superclass._defaultConfig.apply(this, arguments), { |
|
baseCls: "bi-custom-tree", |
|
expander: { |
|
el: {}, |
|
popup: { |
|
type: "bi.custom_tree" |
|
} |
|
}, |
|
|
|
items: [], |
|
itemsCreator: BI.emptyFn, |
|
|
|
el: { |
|
type: "bi.button_tree", |
|
chooseType: 0, |
|
layouts: [{ |
|
type: "bi.vertical" |
|
}] |
|
} |
|
}) |
|
}, |
|
|
|
_init: function () { |
|
BI.CustomTree.superclass._init.apply(this, arguments); |
|
this.initTree(this.options.items); |
|
}, |
|
|
|
setEnable: function (v) { |
|
BI.CustomTree.superclass.setEnable.apply(this, arguments); |
|
this.tree.setEnable(v) |
|
}, |
|
|
|
_formatItems: function (nodes) { |
|
var self = this, o = this.options; |
|
nodes = BI.Tree.transformToTreeFormat(nodes); |
|
|
|
var items = []; |
|
BI.each(nodes, function (i, node) { |
|
if (BI.isNotEmptyArray(node.children) || node.isParent === true) { |
|
var item = BI.extend({ |
|
type: "bi.expander", |
|
el: {}, |
|
popup: {type: "bi.custom_tree"} |
|
}, BI.deepClone(o.expander), { |
|
id: node.id, |
|
pId: node.pId, |
|
value: node.value |
|
}); |
|
var el = BI.stripEL(node); |
|
if (!BI.isWidget(el)) { |
|
el = BI.clone(el); |
|
delete el.children; |
|
BI.extend(item.el, el); |
|
} else { |
|
item.el = el; |
|
} |
|
item.popup.expander = BI.deepClone(o.expander); |
|
item.items = item.popup.items = node.children; |
|
item.itemsCreator = item.popup.itemsCreator = function (op) { |
|
if (BI.isNotNull(op.node)) {//从子节点传过来的itemsCreator直接向上传递 |
|
return o.itemsCreator.apply(self, arguments); |
|
} |
|
var args = Array.prototype.slice.call(arguments, 0); |
|
args[0].node = node; |
|
return o.itemsCreator.apply(self, args); |
|
}; |
|
BI.isNull(item.popup.el) && (item.popup.el = BI.deepClone(o.el)); |
|
items.push(item); |
|
} else { |
|
items.push(node); |
|
} |
|
}); |
|
return items; |
|
}, |
|
|
|
//构造树结构, |
|
initTree: function (nodes) { |
|
var self = this, o = this.options; |
|
this.tree = BI.createWidget(o.el, { |
|
element: this, |
|
items: this._formatItems(nodes), |
|
itemsCreator: function (op, callback) { |
|
o.itemsCreator.apply(this, [op, function (items) { |
|
var args = Array.prototype.slice.call(arguments, 0); |
|
args[0] = self._formatItems(items); |
|
callback.apply(null, args); |
|
}]); |
|
} |
|
}); |
|
this.tree.on(BI.Controller.EVENT_CHANGE, function (type, val, obj) { |
|
self.fireEvent(BI.Controller.EVENT_CHANGE, arguments); |
|
if (type === BI.Events.CLICK) { |
|
self.fireEvent(BI.CustomTree.EVENT_CHANGE, val, obj); |
|
} |
|
}) |
|
}, |
|
|
|
//生成树方法 |
|
stroke: function (nodes) { |
|
this.populate.apply(this, arguments); |
|
}, |
|
|
|
populate: function (nodes) { |
|
var args = Array.prototype.slice.call(arguments, 0); |
|
if (arguments.length > 0) { |
|
args[0] = this._formatItems(nodes); |
|
} |
|
this.tree.populate.apply(this.tree, args); |
|
}, |
|
|
|
doBehavior: function () { |
|
this.tree.doBehavior.apply(this.tree, arguments); |
|
}, |
|
|
|
setValue: function (v) { |
|
this.tree && this.tree.setValue(v); |
|
}, |
|
|
|
getValue: function () { |
|
return this.tree ? this.tree.getValue() : []; |
|
}, |
|
|
|
getAllButtons: function () { |
|
return this.tree ? this.tree.getAllButtons() : []; |
|
}, |
|
|
|
getAllLeaves: function () { |
|
return this.tree ? this.tree.getAllLeaves() : []; |
|
}, |
|
|
|
getNodeById: function (id) { |
|
return this.tree && this.tree.getNodeById(id); |
|
}, |
|
|
|
getNodeByValue: function (id) { |
|
return this.tree && this.tree.getNodeByValue(id); |
|
}, |
|
|
|
empty: function () { |
|
this.tree.empty(); |
|
} |
|
}); |
|
BI.CustomTree.EVENT_CHANGE = "EVENT_CHANGE"; |
|
|
|
$.shortcut("bi.custom_tree", BI.CustomTree); |