UNPKG

@scena/react-guides

Version:

A React Guides component that can draw ruler and manage guidelines.

813 lines (681 loc) 24.8 kB
/* Copyright (c) 2019 Daybrush name: @scena/react-guides license: MIT author: Daybrush repository: https://github.com/daybrush/guides/blob/master/packages/react-guides version: 0.28.2 */ import { createElement, createRef, PureComponent } from 'react'; import Ruler, { PROPERTIES as PROPERTIES$1 } from '@scena/react-ruler'; import { prefixNames, prefixCSS, ref, refs } from 'framework-utils'; import DragScroll from '@scena/dragscroll'; import Gesto from 'gesto'; import { styled } from 'react-css-styled'; import { hasClass, addClass, removeClass } from '@daybrush/utils'; import { getDistElementMatrix, calculateMatrixDist } from 'css-to-mat'; /****************************************************************************** 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 __assign = function () { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } function prefix() { var classNames = []; for (var _i = 0; _i < arguments.length; _i++) { classNames[_i] = arguments[_i]; } return prefixNames.apply(void 0, __spreadArray(["scena-guides-"], classNames, false)); } var ADDER = prefix("guide", "adder"); var GUIDES = prefix("guides"); var GUIDE = prefix("guide"); var DRAGGING = prefix("dragging"); var DISPLAY_DRAG = prefix("display-drag"); var GUIDES_CSS = prefixCSS("scena-guides-", "\n{\n position: relative;\n width: 100%;\n height: 100%;\n}\ncanvas {\n position: relative;\n}\n.guide-origin {\n position: absolute;\n width: 1px;\n height: 1px;\n top: 0;\n left: 0;\n opacity: 0;\n}\n.guides {\n position: absolute;\n bottom: 0;\n right: 0;\n will-change: transform;\n z-index: 2000;\n}\n.guide-pos {\n position: absolute;\n font-weight: bold;\n font-size: 12px;\n color: #f33;\n}\n.horizontal .guide-pos {\n bottom: 100%;\n left: 50%;\n transform: translate(-50%);\n}\n.vertical .guide-pos {\n left: calc(100% + 2px);\n top: 50%;\n transform: translateY(-50%);\n}\n.display-drag {\n position: absolute;\n will-change: transform;\n z-index: 2000;\n font-weight: bold;\n font-size: 12px;\n display: none;\n left: 20px;\n top: -20px;\n color: #f33;\n}\n:host.horizontal .guides {\n width: 100%;\n height: 0;\n}\n:host.vertical .guides {\n height: 100%;\n width: 0;\n}\n:host.horizontal canvas {\n cursor: ns-resize;\n}\n:host.vertical canvas {\n cursor: ew-resize;\n}\n.guide {\n position: absolute;\n background: #f33;\n z-index: 2;\n}\n.guide.dragging:before {\n position: absolute;\n content: \"\";\n width: 100%;\n height: 100%;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n:host.horizontal .guide {\n width: 100%;\n height: 1px;\n cursor: row-resize;\n}\n:host.vertical .guide {\n width: 1px;\n height: 100%;\n cursor: col-resize;\n}\n.mobile :host.horizontal .guide {\n transform: scale(1, 2);\n}\n.mobile :host.vertical .guide {\n transform: scale(2, 1);\n}\n:host.horizontal .guide:before {\n height: 20px;\n}\n:host.vertical .guide:before {\n width: 20px;\n}\n.adder {\n display: none;\n}\n.adder.dragging {\n display: block;\n}\n"); var PROPERTIES = __spreadArray(["className", "rulerStyle", 'snapThreshold', "snaps", "displayDragPos", "cspNonce", 'dragPosFormat', "defaultGuides", "showGuides", "scrollOptions", "guideStyle", "guidesOffset", "digit", "defaultGuidesPos", "dragGuideStyle", "displayGuidePos", "guidePosFormat", "guidePosStyle", "lockGuides", "guidesZoom"], PROPERTIES$1, true); var METHODS = ["getGuides", "loadGuides", "scroll", "scrollGuides", "resize", "getElement", "getRulerElement", "forceUpdate", "getRulerScrollPos", "getGuideScrollPos", "zoomTo", "drawRuler"]; var EVENTS = ["changeGuides", "requestScroll", "dragStart", "drag", "dragEnd", "clickRuler"]; var GuidesElement = styled("div", GUIDES_CSS); var Guides = /*#__PURE__*/ function (_super) { __extends(Guides, _super); function Guides(props) { var _this = _super.call(this, props) || this; _this.state = { guides: [] }; _this.scrollPos = 0; _this.managerRef = createRef(); _this.guideElements = []; _this._isFirstMove = false; _this._zoom = 1; _this._guidesZoom = 1; _this._observer = null; _this.onDragStart = function (e) { var datas = e.datas, inputEvent = e.inputEvent; _this._isFirstMove = true; _this.movePos(e); /** * When the drag starts, the dragStart event is called. * @memberof Guides * @event dragStart * @param {OnDragStart} - Parameters for the dragStart event */ _this.props.onDragStart(__assign(__assign({}, e), { dragElement: datas.target })); if (!_this.gesto.isFlag()) { return; } inputEvent.stopPropagation(); inputEvent.preventDefault(); _this._startDragScroll(e); }; _this._onDrag = function (e) { if (_this._isFirstMove) { _this._isFirstMove = false; addClass(e.datas.target, DRAGGING); } var nextPos = _this.movePos(e); /** * When dragging, the drag event is called. * @memberof Guides * @event drag * @param {OnDrag} - Parameters for the drag event */ _this.props.onDrag(__assign(__assign({}, e), { dragElement: e.datas.target })); if (!_this.gesto.isFlag()) { _this._endDragScroll(e); return; } _this._dragScroll(e); return nextPos; }; _this.onDragEnd = function (e) { var datas = e.datas, isDouble = e.isDouble, distX = e.distX, distY = e.distY; var pos = _this.movePos(e); var guides = _this.state.guides; var _a = _this.props, onChangeGuides = _a.onChangeGuides, displayDragPos = _a.displayDragPos, digit = _a.digit, lockGuides = _a.lockGuides, guidesOffset = _a.guidesOffset; var zoom = _this._guidesZoom; var guidePos = parseFloat((pos / zoom).toFixed(digit || 0)); var baseScrollPos = _this.scrollPos - (guidesOffset || 0); if (displayDragPos) { _this.displayElement.style.cssText += "display: none;"; } removeClass(datas.target, DRAGGING); /** * When the drag finishes, the dragEnd event is called. * @memberof Guides * @event dragEnd * @param {OnDragEnd} - Parameters for the dragEnd event */ _this.props.onDragEnd(__assign(__assign({}, e), { dragElement: datas.target })); _this._endDragScroll(e); if (datas.fromRuler) { if (_this._isFirstMove) { /** * When click the ruler, the click ruler is called. * @memberof Guides * @event clickRuler * @param {OnClickRuler} - Parameters for the clickRuler event */ _this.props.onClickRuler(__assign(__assign({}, e), { pos: 0 })); } if (guidePos >= baseScrollPos && guides.indexOf(guidePos) < 0) { _this.setState({ guides: __spreadArray(__spreadArray([], guides, true), [guidePos], false) }, function () { /** * The `changeGuides` event occurs when the guideline is added / removed / changed. * @memberof Guides * @event changeGuides * @param {OnChangeGuides} - Parameters for the changeGuides event */ onChangeGuides({ guides: _this.state.guides, distX: distX, distY: distY, index: guides.length, isAdd: true, isRemove: false, isChange: false }); }); } } else { var index_1 = parseFloat(datas.target.getAttribute("data-index")); var isRemove_1 = false; var isChange_1 = false; guides = __spreadArray([], guides, true); var guideIndex = guides.indexOf(guidePos); if (isDouble || guidePos < baseScrollPos || guideIndex > -1 && guideIndex !== index_1) { if (lockGuides && (lockGuides === true || lockGuides.indexOf("remove") > -1)) { return; } guides.splice(index_1, 1); isRemove_1 = true; } else if (guideIndex > -1) { return; } else { if (lockGuides && (lockGuides === true || lockGuides.indexOf("change") > -1)) { return; } guides[index_1] = guidePos; isChange_1 = true; } _this.setState({ guides: guides }, function () { var nextGuides = _this.state.guides; onChangeGuides({ distX: distX, distY: distY, guides: nextGuides, isAdd: false, index: index_1, isChange: isChange_1, isRemove: isRemove_1 }); }); } }; _this._onCheck = function () { _this.resize(); }; _this.state.guides = props.defaultGuides || []; _this.scrollPos = props.defaultGuidesPos || 0; return _this; } var __proto = Guides.prototype; __proto.render = function () { var _a = this.props, className = _a.className, type = _a.type, zoom = _a.zoom, guidesZoom = _a.guidesZoom, style = _a.style, rulerStyle = _a.rulerStyle, displayDragPos = _a.displayDragPos, cspNonce = _a.cspNonce, dragGuideStyle = _a.dragGuideStyle, _b = _a.guidePosStyle, guidePosStyle = _b === void 0 ? {} : _b; var props = this.props; var translateName = this.getTranslateName(); var rulerProps = {}; PROPERTIES$1.forEach(function (name) { if (name === "style" || name === "warpSelf" || name === "useResizeObserver") { return; } rulerProps[name] = props[name]; }); this._zoom = zoom; this._guidesZoom = guidesZoom || zoom; return createElement(GuidesElement, { ref: this.managerRef, cspNonce: cspNonce, className: "".concat(prefix("manager", type), " ").concat(className), style: style }, createElement("div", { className: prefix("guide-origin"), ref: ref(this, "originElement") }), createElement(Ruler, __assign({ ref: ref(this, "ruler"), style: rulerStyle }, rulerProps)), createElement("div", { className: GUIDES, ref: ref(this, "guidesElement"), style: { transform: "".concat(translateName, "(").concat(-this.scrollPos * this._guidesZoom, "px)") } }, displayDragPos && createElement("div", { className: DISPLAY_DRAG, ref: ref(this, "displayElement"), style: guidePosStyle || {} }), createElement("div", { className: ADDER, ref: ref(this, "adderElement"), style: dragGuideStyle }), this.renderGuides())); }; /** * Draw ruler */ __proto.drawRuler = function (options) { this.ruler.draw(options); }; __proto.renderGuides = function () { var _this = this; var props = this.props; var _a = props, type = _a.type, showGuides = _a.showGuides, guideStyle = _a.guideStyle, displayGuidePos = _a.displayGuidePos, _b = _a.guidePosStyle, guidePosStyle = _b === void 0 ? {} : _b, guidesOffset = _a.guidesOffset; var zoom = this._guidesZoom; var translateName = this.getTranslateName(); var guides = this.state.guides; var guidePosFormat = props.guidePosFormat || props.dragPosFormat || function (v) { return v; }; this.guideElements = []; if (showGuides) { return guides.map(function (pos, i) { var guidePos = pos + (guidesOffset || 0); return createElement("div", { className: prefix("guide", type), ref: refs(_this, "guideElements", i), key: i, "data-index": i, "data-pos": pos, style: __assign(__assign({}, guideStyle), { transform: "".concat(translateName, "(").concat(guidePos * zoom, "px) translateZ(0px)") }) }, displayGuidePos && createElement("div", { className: prefix("guide-pos"), style: guidePosStyle || {} }, guidePosFormat(pos))); }); } return; }; __proto.componentDidMount = function () { var _this = this; this.gesto = new Gesto(this.managerRef.current, { container: document.body }).on("dragStart", function (e) { var _a = _this.props, type = _a.type, lockGuides = _a.lockGuides; var zoom = _this._guidesZoom; if (lockGuides === true) { e.stop(); return; } var inputEvent = e.inputEvent; var target = inputEvent.target; var datas = e.datas; var canvasElement = _this.ruler.canvasElement; var guidesElement = _this.guidesElement; var isHorizontal = type === "horizontal"; var originRect = _this.originElement.getBoundingClientRect(); var matrix = getDistElementMatrix(_this.managerRef.current); var offsetPos = calculateMatrixDist(matrix, [e.clientX - originRect.left, e.clientY - originRect.top]); offsetPos[0] -= guidesElement.offsetLeft; offsetPos[1] -= guidesElement.offsetTop; offsetPos[isHorizontal ? 1 : 0] += _this.scrollPos * zoom; datas.offsetPos = offsetPos; datas.matrix = matrix; var isLockAdd = lockGuides && lockGuides.indexOf("add") > -1; var isLockRemove = lockGuides && lockGuides.indexOf("remove") > -1; var isLockChange = lockGuides && lockGuides.indexOf("change") > -1; if (target === canvasElement) { if (isLockAdd) { e.stop(); return; } datas.fromRuler = true; datas.target = _this.adderElement; // add } else if (hasClass(target, GUIDE)) { if (isLockRemove && isLockChange) { e.stop(); return; } datas.target = target; // change } else { e.stop(); return false; } _this.onDragStart(e); }).on("drag", this._onDrag).on("dragEnd", this.onDragEnd); if (this.props.useResizeObserver) { this._observer = new ResizeObserver(this._onCheck); this._observer.observe(this.guidesElement, { box: "border-box" }); this._observer.observe(this.getRulerElement(), { box: "border-box" }); } else { this._onCheck(); } }; __proto.componentWillUnmount = function () { var _a; this.gesto.unset(); (_a = this._observer) === null || _a === void 0 ? void 0 : _a.disconnect(); }; __proto.componentDidUpdate = function (prevProps) { var nextGuides = this.props.defaultGuides; if (prevProps.defaultGuides !== nextGuides) { // to dynamically update guides from code rather than dragging guidelines this.setState({ guides: nextGuides || [] }); } }; /** * Load the current guidelines. * @memberof Guides * @instance */ __proto.loadGuides = function (guides) { this.setState({ guides: guides }); }; /** * Get current guidelines. * @memberof Guides * @instance */ __proto.getGuides = function () { return this.state.guides; }; /** * Scroll the positions of the guidelines opposite the ruler. * @memberof Guides * @instance */ __proto.scrollGuides = function (pos, nextZoom) { if (nextZoom === void 0) { nextZoom = this._guidesZoom; } this._setZoom({ guidesZoom: nextZoom }); var translateName = this.getTranslateName(); var guidesElement = this.guidesElement; this.scrollPos = pos; guidesElement.style.transform = "".concat(translateName, "(").concat(-pos * nextZoom, "px)"); var guides = this.state.guides; var guidesOffset = this.props.guidesOffset || 0; this.guideElements.forEach(function (el, i) { if (!el) { return; } var guidePos = guides[i] + (guidesOffset || 0); el.style.transform = "".concat(translateName, "(").concat(guidePos * nextZoom, "px) translateZ(0px)"); el.style.display = -pos + guidePos < 0 ? "none" : "block"; }); }; /** * Set to the next zoom. * @memberof Guides * @since 0.22.0 * @param nextZoom - next zoom */ __proto.zoomTo = function (nextZoom, nextGuidesZoom) { if (nextGuidesZoom === void 0) { nextGuidesZoom = nextZoom; } this.scroll(this.getRulerScrollPos(), nextZoom); this.scrollGuides(this.getGuideScrollPos(), nextGuidesZoom); }; /** * Get Guides DOM Element * @memberof Guides * @instance */ __proto.getElement = function () { return this.managerRef.current; }; /** * Get Ruler DOM Element * @memberof Guides * @instance */ __proto.getRulerElement = function () { return this.ruler.canvasElement; }; /** * Scroll position of guides (horizontal: y, vertical: x) */ __proto.getGuideScrollPos = function () { return this.scrollPos; }; /** * Scroll position of the ruler (horizontal: x, vertical: y) */ __proto.getRulerScrollPos = function () { return this.ruler.getScrollPos(); }; /** * Scroll the position of the ruler. * @memberof Guides * @instance */ __proto.scroll = function (pos, nextZoom) { if (nextZoom === void 0) { nextZoom = this._zoom; } this._setZoom({ zoom: nextZoom }); this.ruler.scroll(pos, nextZoom); }; /** * Recalculate the size of the ruler. * @memberof Guides * @instance */ __proto.resize = function (nextZoom) { if (nextZoom === void 0) { nextZoom = this._zoom; } this._setZoom({ zoom: nextZoom }); this.ruler.resize(nextZoom); }; __proto.movePos = function (e) { var datas = e.datas, distX = e.distX, distY = e.distY; var props = this.props; var type = props.type, snaps = props.snaps, snapThreshold = props.snapThreshold, displayDragPos = props.displayDragPos, digit = props.digit; var guidesOffset = props.guidesOffset || 0; var zoom = this._guidesZoom; var dragPosFormat = props.dragPosFormat || function (v) { return v; }; var isHorizontal = type === "horizontal"; var matrixPos = calculateMatrixDist(datas.matrix, [distX, distY]); var offsetPos = datas.offsetPos; var offsetX = matrixPos[0] + offsetPos[0]; var offsetY = matrixPos[1] + offsetPos[1]; var guidesZoomOffset = guidesOffset * zoom; var nextPos = Math.round(isHorizontal ? offsetY : offsetX) - guidesOffset; var guidePos = parseFloat((nextPos / zoom).toFixed(digit || 0)); var guideSnaps = snaps.slice().sort(function (a, b) { return Math.abs(guidePos - a) - Math.abs(guidePos - b); }); if (guideSnaps.length && Math.abs(guideSnaps[0] * zoom - nextPos) < snapThreshold) { guidePos = guideSnaps[0]; nextPos = guidePos * zoom; } if (!datas.fromRuler || !this._isFirstMove) { if (displayDragPos) { var displayPos = type === "horizontal" ? [offsetX, nextPos + guidesZoomOffset] : [nextPos + guidesZoomOffset, offsetY]; this.displayElement.style.cssText += "display: block;" + "transform: translate(-50%, -50%) " + "translate(".concat(displayPos.map(function (v) { return "".concat(v, "px"); }).join(", "), ")"); this.displayElement.innerHTML = "".concat(dragPosFormat(guidePos)); } var target = datas.target; target.setAttribute("data-pos", guidePos); target.style.transform = "".concat(this.getTranslateName(), "(").concat(nextPos + guidesOffset * zoom, "px)"); } return nextPos; }; __proto.getTranslateName = function () { return this.props.type === "horizontal" ? "translateY" : "translateX"; }; __proto._startDragScroll = function (e) { var _this = this; var scrollOptions = this.props.scrollOptions; if (!scrollOptions) { return; } var datas = e.datas; var dragScroll = new DragScroll(); datas.dragScroll = dragScroll; dragScroll.on("scroll", function (_a) { var _b, _c; var container = _a.container, direction = _a.direction; /** * If scroll can be triggered through drag, the `requestScroll` event is fired. * @memberof Guides * @event requestScroll * @param {OnRequestScroll} - Parameters for the `requestScroll` event */ (_c = (_b = _this.props).onRequestScroll) === null || _c === void 0 ? void 0 : _c.call(_b, { container: container, direction: direction }); }).on("move", function (_a) { var offsetX = _a.offsetX, offsetY = _a.offsetY, inputEvent = _a.inputEvent; _this.gesto.scrollBy(offsetX, offsetY, inputEvent.inputEvent, true); }); dragScroll.dragStart(e, { container: scrollOptions.container }); }; __proto._dragScroll = function (e) { var scrollOptions = this.props.scrollOptions; if (!scrollOptions) { return; } var dragScroll = e.datas.dragScroll; dragScroll.drag(e, scrollOptions); }; __proto._endDragScroll = function (e) { var _a; (_a = e.datas.dragScroll) === null || _a === void 0 ? void 0 : _a.dragEnd(); e.datas.dragScroll = null; }; __proto._setZoom = function (zooms) { var nextZoom = zooms.zoom, nextGuidesZoom = zooms.guidesZoom; var hasZoom = !!this.props.zoom; var hasGuidesZoom = !!this.props.guidesZoom; if (hasGuidesZoom) { if (nextGuidesZoom) { this._guidesZoom = nextGuidesZoom; } } else { if (nextGuidesZoom) { this._zoom = nextGuidesZoom; this._guidesZoom = nextGuidesZoom; } if (nextZoom) { this._guidesZoom = nextZoom; } } if (nextZoom) { this._zoom = nextZoom; } }; Guides.defaultProps = { className: "", type: "horizontal", zoom: 1, guidesZoom: 0, style: {}, snapThreshold: 5, snaps: [], digit: 0, onClickRuler: function () {}, onChangeGuides: function () {}, onRequestScroll: function () {}, onDragStart: function () {}, onDrag: function () {}, onDragEnd: function () {}, displayDragPos: false, dragPosFormat: function (v) { return v; }, defaultGuides: [], lockGuides: false, showGuides: true, guideStyle: {}, dragGuideStyle: {}, guidePosStyle: {}, defaultGuidesPos: 0 }; return Guides; }(PureComponent); export default Guides; export { EVENTS, METHODS, PROPERTIES }; //# sourceMappingURL=guides.esm.js.map