@scena/react-ruler
Version:
A React Ruler component that can draw grids and scroll infinitely.
589 lines (480 loc) • 17.3 kB
JavaScript
/*
Copyright (c) 2019 Daybrush
name: @scena/react-ruler
license: MIT
author: Daybrush
repository: https://github.com/daybrush/ruler/blob/master/packages/react-ruler
version: 0.19.0
*/
'use strict';
var React = require('react');
var frameworkUtils = require('framework-utils');
var utils = require('@daybrush/utils');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf || {
__proto__: []
} instanceof Array && function (d, b) {
d.__proto__ = b;
} || function (d, b) {
for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p];
};
return extendStatics(d, b);
};
function __extends(d, b) {
if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() {
this.constructor = d;
}
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var Ruler =
/*#__PURE__*/
function (_super) {
__extends(Ruler, _super);
function Ruler() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.state = {
scrollPos: 0
};
_this.width = 0;
_this.height = 0;
_this._zoom = 0;
_this._rulerScale = 0;
_this._observer = null;
_this._checkResize = function () {
_this.resize();
};
return _this;
}
var __proto = Ruler.prototype;
__proto.render = function () {
var props = this.props;
this._zoom = props.zoom;
return React.createElement("canvas", {
ref: frameworkUtils.ref(this, "canvasElement"),
style: this.props.style
});
};
__proto.componentDidMount = function () {
var props = this.props;
this.state.scrollPos = props.defaultScrollPos || 0;
var canvas = this.canvasElement;
var context = canvas.getContext("2d", {
alpha: true
});
this.canvasContext = context;
if (props.useResizeObserver) {
this._observer = new ResizeObserver(this._checkResize);
this._observer.observe(canvas, {
box: "border-box"
});
} else {
this.resize();
}
};
__proto.componentDidUpdate = function () {
this.resize();
};
__proto.componentWillUnmount = function () {
var _a;
this.state.scrollPos = 0;
(_a = this._observer) === null || _a === void 0 ? void 0 : _a.disconnect();
};
/**
* Gets the scroll position of the ruler.
*/
__proto.getScrollPos = function () {
return this.state.scrollPos;
};
/**
* @method Ruler#scroll
* @param scrollPos
*/
__proto.scroll = function (scrollPos, zoom) {
this.draw({
scrollPos: scrollPos,
zoom: zoom
});
};
/**
* @method Ruler#resize
*/
__proto.resize = function (nextZoom) {
var canvas = this.canvasElement;
var _a = this.props,
width = _a.width,
height = _a.height,
scrollPos = _a.scrollPos;
var rulerScale = this._getRulerScale();
this.width = width || canvas.offsetWidth;
this.height = height || canvas.offsetHeight;
canvas.width = this.width * rulerScale;
canvas.height = this.height * rulerScale;
this.draw({
scrollPos: scrollPos,
zoom: nextZoom
});
};
/**
* draw a ruler
* @param options - It is drawn with an external value, not the existing state.
*/
__proto.draw = function (options) {
if (options === void 0) {
options = {};
}
var props = this.props;
var _a = options.zoom,
nextZoom = _a === void 0 ? this._zoom : _a,
_b = options.scrollPos,
scrollPos = _b === void 0 ? this.state.scrollPos : _b,
_c = options.marks,
marks = _c === void 0 ? props.marks : _c,
_d = options.selectedRanges,
selectedRanges = _d === void 0 ? props.selectedRanges : _d,
_e = options.segment,
segment = _e === void 0 ? props.segment || 10 : _e,
_f = options.unit,
unit = _f === void 0 ? props.unit : _f;
this._zoom = nextZoom;
var _g = props,
type = _g.type,
backgroundColor = _g.backgroundColor,
lineColor = _g.lineColor,
textColor = _g.textColor,
textBackgroundColor = _g.textBackgroundColor,
direction = _g.direction,
_h = _g.negativeRuler,
negativeRuler = _h === void 0 ? true : _h,
textFormat = _g.textFormat,
_j = _g.range,
range = _j === void 0 ? [-Infinity, Infinity] : _j,
rangeBackgroundColor = _g.rangeBackgroundColor,
selectedBackgroundColor = _g.selectedBackgroundColor,
_k = _g.lineWidth,
lineWidth = _k === void 0 ? 1 : _k,
selectedRangesText = _g.selectedRangesText,
_l = _g.selectedRangesTextColor,
selectedRangesTextColor = _l === void 0 ? "#44aaff" : _l,
_m = _g.selectedRangesTextOffset,
selectedRangesTextOffset = _m === void 0 ? [0, 0] : _m,
_o = _g.markColor,
markColor = _o === void 0 ? "#ff5" : _o;
var rulerScale = this._getRulerScale();
var width = this.width;
var height = this.height;
var state = this.state;
state.scrollPos = scrollPos;
var context = this.canvasContext;
var isHorizontal = type === "horizontal";
var isNegative = negativeRuler !== false;
var font = props.font || "10px sans-serif";
var textAlign = props.textAlign || "left";
var textOffset = props.textOffset || [0, 0];
var containerSize = isHorizontal ? height : width;
var mainLineSize = utils.convertUnitSize("".concat(props.mainLineSize || "100%"), containerSize);
var longLineSize = utils.convertUnitSize("".concat(props.longLineSize || 10), containerSize);
var shortLineSize = utils.convertUnitSize("".concat(props.shortLineSize || 7), containerSize);
var lineOffset = props.lineOffset || [0, 0];
if (backgroundColor === "transparent") {
// Clear existing paths & text
context.clearRect(0, 0, width * rulerScale, height * rulerScale);
} else {
// Draw the background
context.rect(0, 0, width * rulerScale, height * rulerScale);
context.fillStyle = backgroundColor;
context.fill();
}
context.save();
context.scale(rulerScale, rulerScale);
context.strokeStyle = lineColor;
context.lineWidth = lineWidth;
context.font = font;
context.fillStyle = textColor;
context.textAlign = textAlign;
switch (direction) {
case "start":
context.textBaseline = "top";
break;
case "center":
context.textBaseline = "middle";
break;
case "end":
context.textBaseline = "bottom";
break;
}
context.translate(0.5, 0);
context.beginPath();
var size = isHorizontal ? width : height;
var zoomUnit = nextZoom * unit;
var minRange = Math.floor(scrollPos * nextZoom / zoomUnit);
var maxRange = Math.ceil((scrollPos * nextZoom + size) / zoomUnit);
var length = maxRange - minRange;
var alignOffset = Math.max(["left", "center", "right"].indexOf(textAlign) - 1, -1);
var barSize = isHorizontal ? height : width;
var values = [];
for (var i = 0; i <= length; ++i) {
var value = (i + minRange) * unit;
var text = "".concat(value);
if (textFormat) {
text = textFormat(value);
}
var textSize = context.measureText(text).width;
values.push({
color: textColor,
offset: textOffset,
backgroundColor: textBackgroundColor,
value: value,
text: text,
textSize: textSize
});
} // Draw Selected Range Background
if (selectedBackgroundColor !== "transparent" && (selectedRanges === null || selectedRanges === void 0 ? void 0 : selectedRanges.length)) {
selectedRanges.forEach(function (selectedRange) {
var rangeStart = Math.max(selectedRange[0], range[0], negativeRuler ? -Infinity : 0);
var rangeEnd = Math.min(selectedRange[1], range[1]);
var rangeX = (rangeStart - scrollPos) * nextZoom;
var rangeWidth = (rangeEnd - rangeStart) * nextZoom;
if (selectedRangesText) {
selectedRange.forEach(function (value) {
var text = "".concat(value);
if (textFormat) {
text = textFormat(value);
}
var textSize = context.measureText(text).width;
var startPos = value * nextZoom;
var endPos = startPos + textSize;
utils.findLast(values, function (_a, index) {
var prevValue = _a.value,
prevTextSize = _a.textSize;
var prevStartPos = prevValue * nextZoom;
var prevEndPos = prevStartPos + prevTextSize;
if (prevStartPos <= endPos && startPos <= prevEndPos) {
values.splice(index, 1);
}
});
values.push({
value: value,
color: selectedRangesTextColor,
offset: selectedRangesTextOffset,
text: text,
textSize: textSize
});
});
}
if (rangeWidth <= 0) {
return;
}
context.save();
context.fillStyle = selectedBackgroundColor;
if (isHorizontal) {
context.fillRect(rangeX, 0, rangeWidth, barSize);
} else {
context.fillRect(0, rangeX, barSize, rangeWidth);
}
context.restore();
});
} // Draw Range Background
if (rangeBackgroundColor !== "transparent" && range[0] !== -Infinity && range[1] !== Infinity) {
var rangeStart = (range[0] - scrollPos) * nextZoom;
var rangeEnd = (range[1] - range[0]) * nextZoom;
context.save();
context.fillStyle = rangeBackgroundColor;
if (isHorizontal) {
context.fillRect(rangeStart, 0, rangeEnd, barSize);
} else {
context.fillRect(0, rangeStart, barSize, rangeEnd);
}
context.restore();
} // Render Segments First
for (var i = 0; i <= length; ++i) {
var value = i + minRange;
if (!isNegative && value < 0) {
continue;
}
var startValue = value * unit;
var startPos = (startValue - scrollPos) * nextZoom;
for (var j = 0; j < segment; ++j) {
var pos = startPos + j / segment * zoomUnit;
var value_1 = startValue + j / segment * unit;
if (pos < 0 || pos >= size || value_1 < range[0] || value_1 > range[1]) {
continue;
}
var lineSize = j === 0 ? mainLineSize : j % 2 === 0 ? longLineSize : shortLineSize;
var origin = 0;
switch (direction) {
case "start":
origin = 0;
break;
case "center":
origin = barSize / 2 - lineSize / 2;
break;
case "end":
origin = barSize - lineSize;
break;
}
var _p = isHorizontal ? [pos + lineOffset[0], origin + lineOffset[1]] : [origin + lineOffset[0], pos + lineOffset[1]],
x1 = _p[0],
y1 = _p[1];
var _q = isHorizontal ? [x1, y1 + lineSize] : [x1 + lineSize, y1],
x2 = _q[0],
y2 = _q[1];
context.moveTo(x1 + lineOffset[0], y1 + lineOffset[1]);
context.lineTo(x2 + lineOffset[0], y2 + lineOffset[1]);
}
}
context.stroke();
context.beginPath(); // Render marks
context.strokeStyle = markColor;
context.lineWidth = 1;
(marks || []).forEach(function (value) {
var pos = (-scrollPos + value) * nextZoom;
if (pos < 0 || pos >= size || value < range[0] || value > range[1]) {
return;
}
var _a = isHorizontal ? [pos + lineOffset[0], lineOffset[1]] : [lineOffset[0], pos + lineOffset[1]],
x1 = _a[0],
y1 = _a[1];
var _b = isHorizontal ? [x1, y1 + containerSize] : [x1 + containerSize, y1],
x2 = _b[0],
y2 = _b[1];
context.moveTo(x1 + lineOffset[0], y1 + lineOffset[1]);
context.lineTo(x2 + lineOffset[0], y2 + lineOffset[1]);
});
context.stroke(); // Render Labels
values.forEach(function (_a) {
var value = _a.value,
offset = _a.offset,
backgroundColor = _a.backgroundColor,
color = _a.color,
text = _a.text,
textSize = _a.textSize;
if (!isNegative && value < 0) {
return;
}
var startPos = (value - scrollPos) * nextZoom;
if (startPos < -zoomUnit || startPos >= size + unit * nextZoom || value < range[0] || value > range[1]) {
return;
}
var origin = 0;
switch (direction) {
case "start":
origin = 17;
break;
case "center":
origin = barSize / 2;
break;
case "end":
origin = barSize - 17;
break;
}
var _b = isHorizontal ? [startPos + alignOffset * -3, origin] : [origin, startPos + alignOffset * 3],
startX = _b[0],
startY = _b[1];
if (backgroundColor) {
var backgroundOffset = 0;
switch (textAlign) {
case "left":
backgroundOffset = 0;
break;
case "center":
backgroundOffset = -textSize / 2;
break;
case "right":
backgroundOffset = -textSize;
break;
}
context.save();
context.fillStyle = backgroundColor;
if (isHorizontal) {
context.fillRect(startX + offset[0] + backgroundOffset, 0, textSize, mainLineSize);
} else {
context.translate(0, startY + offset[1]);
context.rotate(-Math.PI / 2);
context.fillRect(backgroundOffset, 0, textSize, mainLineSize);
}
context.restore();
}
context.save();
context.fillStyle = color;
if (isHorizontal) {
context.fillText(text, startX + offset[0], startY + offset[1]);
} else {
context.translate(startX + offset[0], startY + offset[1]);
context.rotate(-Math.PI / 2);
context.fillText(text, 0, 0);
}
context.restore();
});
context.restore();
};
__proto._getRulerScale = function () {
var defaultPixelScale = this.props.defaultPixelScale || 2;
if (!this._rulerScale) {
var isHighDensity = window.devicePixelRatio > 1;
if (!isHighDensity && window.matchMedia) {
var mq = window.matchMedia('only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)');
isHighDensity = mq && mq.matches;
}
this._rulerScale = isHighDensity ? 3 : defaultPixelScale;
}
return this._rulerScale;
};
Ruler.defaultProps = {
type: "horizontal",
zoom: 1,
width: 0,
height: 0,
unit: 50,
negativeRuler: true,
mainLineSize: "100%",
longLineSize: 10,
shortLineSize: 7,
segment: 10,
direction: "end",
style: {
width: "100%",
height: "100%"
},
backgroundColor: "#333333",
font: "10px sans-serif",
textColor: "#ffffff",
textBackgroundColor: 'transparent',
lineColor: "#777777",
range: [-Infinity, Infinity],
rangeBackgroundColor: 'transparent',
lineWidth: 1,
selectedBackgroundColor: "#555555",
defaultScrollPos: 0,
markColor: "#f55",
marks: []
};
return Ruler;
}(React.PureComponent);
var PROPERTIES = ["type", "width", "height", "unit", "zoom", "direction", "textAlign", "font", "segment", "mainLineSize", "longLineSize", "shortLineSize", "lineOffset", "textOffset", "negativeRuler", "range", "scrollPos", "defaultScrollPos", "style", "backgroundColor", "rangeBackgroundColor", "lineColor", "textColor", "textBackgroundColor", "textFormat", "warpSelf", "selectedBackgroundColor", "selectedRanges", "defaultPixelScale", "useResizeObserver", "selectedRangesText", "selectedRangesTextColor", "selectedRangesTextOffset", "marks", "markColor"];
var METHODS = ["scroll", "resize", "getScrollPos", "draw"];
var others = {
__proto__: null,
'default': Ruler,
PROPERTIES: PROPERTIES,
METHODS: METHODS
};
for (var name in others) {
Ruler[name] = others[name];
}
module.exports = Ruler;
//# sourceMappingURL=ruler.cjs.js.map