UNPKG

@staski/vue-img-mapper

Version:

Vue Component to highlight interactive zones in images

522 lines (521 loc) 15.9 kB
import { openBlock, createElementBlock, createCommentVNode, defineComponent, resolveComponent, normalizeStyle, createElementVNode, Fragment, renderList, createBlock, mergeProps } from "vue"; const imageMouseMove = (event, props) => { if (props.$attrs.onImageMouseMove) { event.preventDefault(); event.stopImmediatePropagation(); props.$emit("imageMouseMove", event); } }; const imageClick = (event, props) => { if (props.$attrs.onImageClick) { event.preventDefault(); event.stopImmediatePropagation(); props.$emit("imageClick", event); } }; const mouseMove = (area, index, event, props) => { if (props.$attrs.onMousemove) { event.preventDefault(); event.stopImmediatePropagation(); props.$emit("mousemove", area, index, event); } }; const mouseDown = (area, index, event, props) => { if (props.$attrs.onMousedown) { event.preventDefault(); event.stopImmediatePropagation(); props.$emit("mousedown", area, index, event); } }; const mouseUp = (area, index, event, props) => { if (props.$attrs.onMouseup) { event.preventDefault(); event.stopImmediatePropagation(); props.$emit("mouseup", area, index, event); } }; const touchStart = (area, index, event, props) => { if (props.$attrs.onTouchstart) { event.preventDefault(); event.stopImmediatePropagation(); props.$emit("touchstart", area, index, event); } }; const touchEnd = (area, index, event, props) => { if (props.$attrs.onTouchend) { event.preventDefault(); event.stopImmediatePropagation(); props.$emit("touchend", area, index, event); } }; var _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _sfc_main$1 = { inheritAttrs: false, props: { area: { type: Object, required: true }, index: { type: Number, required: true }, extendedArea: { type: Function, required: true }, hoverOff: { type: Function, required: true }, hoverOn: { type: Function, required: true }, click: { type: Function, required: true } }, methods: { mouseMove, mouseDown, mouseUp, touchStart, touchEnd } }; const _hoisted_1$1 = ["id", "coords", "shape", "href"]; function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) { return !$props.area.disabled ? (openBlock(), createElementBlock("area", { key: 0, id: $props.area.id, coords: $props.extendedArea($props.area).scaledCoords.join(","), shape: $props.area.shape, href: $props.area.href, alt: "map", target: "_blank", onMouseenter: _cache[0] || (_cache[0] = ($event) => $props.hoverOn($props.extendedArea($props.area), $props.index, $event)), onMouseleave: _cache[1] || (_cache[1] = ($event) => $props.hoverOff($props.extendedArea($props.area), $props.index, $event)), onMousemove: _cache[2] || (_cache[2] = ($event) => $options.mouseMove($props.extendedArea($props.area), $props.index, $event, this)), onMousedown: _cache[3] || (_cache[3] = ($event) => $options.mouseDown($props.extendedArea($props.area), $props.index, $event, this)), onMouseup: _cache[4] || (_cache[4] = ($event) => $options.mouseUp($props.extendedArea($props.area), $props.index, $event, this)), onTocuhstart: _cache[5] || (_cache[5] = ($event) => $options.touchStart($props.extendedArea($props.area), $props.index, $event, this)), onTouchend: _cache[6] || (_cache[6] = ($event) => $options.touchEnd($props.extendedArea($props.area), $props.index, $event, this)), onClick: _cache[7] || (_cache[7] = ($event) => $props.click($props.extendedArea($props.area), $props.index, $event)) }, null, 40, _hoisted_1$1)) : createCommentVNode("", true); } var RenderAreas = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render$1]]); const absPos = { position: "absolute", top: 0, left: 0 }; const imgNonResponsive = { ...absPos, zIndex: 1, userSelect: "none" }; const imgResponsive = { ...imgNonResponsive, width: "100%", height: "auto" }; const container = { position: "relative" }; const canvas = { ...absPos, pointerEvents: "none", zIndex: 2 }; const img = (responsive) => responsive ? imgResponsive : imgNonResponsive; const map = (onClick) => onClick && { cursor: "pointer" } || void 0; const ImageMapperDefaultProps = { src: { type: String, required: true }, map: { type: Object, required: true }, areaKeyName: { type: String, default: "id" }, active: { type: Boolean, default: true }, disabled: { type: Boolean, default: false }, fillColor: { type: String, default: "rgba(255, 255, 255, 0.5)" }, strokeColor: { type: String, default: "rgba(0, 0, 0, 0.5)" }, lineWidth: { type: Number, default: 1 }, imgWidth: { type: Number, default: 0 }, width: { type: Number, default: 0 }, height: { type: Number, default: 0 }, natural: { type: Boolean, default: false }, stayHighlighted: { type: Boolean, default: false }, stayMultiHighlighted: { type: Boolean, default: false }, toggleHighlighted: { type: Boolean, default: false }, responsive: { type: Boolean, default: false }, parentWidth: { type: Number, default: 0 } }; const drawRect = (coords, fillColor, lineWidth, strokeColor, ctx) => { const [left, top, right, bot] = coords; ctx.fillStyle = fillColor; ctx.lineWidth = lineWidth; ctx.strokeStyle = strokeColor; ctx.strokeRect(left, top, right - left, bot - top); ctx.fillRect(left, top, right - left, bot - top); }; const drawCircle = (coords, fillColor, lineWidth, strokeColor, ctx) => { ctx.fillStyle = fillColor; ctx.beginPath(); ctx.lineWidth = lineWidth; ctx.strokeStyle = strokeColor; ctx.arc(coords[0], coords[1], coords[2], 0, 2 * Math.PI); ctx.closePath(); ctx.stroke(); ctx.fill(); }; const drawPoly = (coords, fillColor, lineWidth, strokeColor, ctx) => { const newCoords = coords.reduce((a, v, i, s) => i % 2 ? a : [...a, s.slice(i, i + 2)], []); ctx.fillStyle = fillColor; ctx.beginPath(); ctx.lineWidth = lineWidth; ctx.strokeStyle = strokeColor; newCoords.forEach((c) => ctx.lineTo(c[0], c[1])); ctx.closePath(); ctx.stroke(); ctx.fill(); }; const callingFn = (shape, coords, fillColor, lineWidth, strokeColor, isAreaActive, ctx) => { console.log(); if (shape === "rect" && isAreaActive) { return drawRect(coords, fillColor, lineWidth, strokeColor, ctx); } if (shape === "circle" && isAreaActive) { return drawCircle(coords, fillColor, lineWidth, strokeColor, ctx); } if (shape === "poly" && isAreaActive) { return drawPoly(coords, fillColor, lineWidth, strokeColor, ctx); } return false; }; const _sfc_main = defineComponent({ name: "ImageMapper", components: { RenderAreas }, props: ImageMapperDefaultProps, data() { return { mapState: this.map, storedMap: this.mapState, isRendered: false, imgRef: null, ctx: null, onClick: null }; }, computed: { containerStyle: () => container, canvasStyle: () => canvas, mapStyle() { return map(this.onClick); }, imgStyle() { return img(this.responsive); } }, watch: { parentWidth() { if (this.responsive) this.initCanvas(); }, map(newMap) { this.setContextAndCache(newMap); this.updateCanvasOnMapChange(); } }, mounted() { this.setContextAndCache(this.map); this.updateCanvasOnMapChange(); }, methods: { imageMouseMove, imageClick, setContextAndCache(map2) { if (!this.isRendered && map2.areas.length) { this.ctx = this.$refs.canvas.getContext("2d"); this.updateCacheMap(); this.isRendered = true; } }, updateCanvasOnMapChange() { if (JSON.stringify(this.mapState) === JSON.stringify(this.map)) { this.updateCacheMap(); this.initCanvas(); this.updateCanvas(); } }, clearHighlightedArea() { this.mapState = this.storedMap; this.initCanvas(); }, extendedArea(area) { const scaledCoords = this.scaleCoords(area.coords); const center = this.computeCenter(area); return { ...area, scaledCoords, center }; }, updateCacheMap() { this.mapState = this.map; this.storedMap = this.map; }, getDimensions(type) { if (typeof this[type] === "function") { return this[type](this.$refs.img); } return this[type]; }, getValues(type, measure, name = "area") { const { naturalWidth, naturalHeight, clientWidth, clientHeight } = this.$refs.img; if (type === "width") { if (this.responsive) return this.parentWidth; if (this.natural) return naturalWidth; if (this.width || name === "image") return measure; return clientWidth; } if (type === "height") { if (this.responsive) return clientHeight; if (this.natural) return naturalHeight; if (this.height || name === "image") return measure; return clientHeight; } return 0; }, initCanvas(firstLoad = false) { const imgWidth = this.getDimensions("width"); const imgHeight = this.getDimensions("height"); const imageWidth = this.getValues("width", imgWidth); const imageHeight = this.getValues("height", imgHeight); if (this.width || this.responsive) { this.$refs.img.width = this.getValues("width", imgWidth, "image"); } if (this.height || this.responsive) { this.$refs.img.height = this.getValues("height", imgHeight, "image"); } this.$refs.canvas.width = imageWidth; this.$refs.canvas.height = imageHeight; this.$refs.container.style.width = `${imageWidth}px`; this.$refs.container.style.height = `${imageHeight}px`; this.ctx = this.$refs.canvas.getContext("2d"); this.ctx.fillStyle = this.fillColor; if (this.$attrs.onLoad && firstLoad) { this.$emit("load", this.$refs.img, { width: imageWidth, height: imageHeight }); } this.imgRef = this.$refs.img; this.renderPrefilledAreas(); }, hoverOn(area, index, event) { const { shape, scaledCoords, fillColor, lineWidth, strokeColor, active: isAreaActive } = area; if (this.active) { callingFn( shape, scaledCoords, fillColor || this.fillColor, lineWidth || this.lineWidth, strokeColor || this.strokeColor, isAreaActive != null ? isAreaActive : true, this.ctx ); } if (this.$attrs.onMouseenter) { event.preventDefault(); event.stopImmediatePropagation(); this.$emit("mouseenter", area, index, event); } }, hoverOff(area, index, event) { if (this.active) { this.ctx.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height); this.renderPrefilledAreas(); } if (this.$attrs.onMouseleave) { event.preventDefault(); event.stopImmediatePropagation(); this.$emit("mouseleave", area, index, event); } }, click(area, index, event) { var _a; const isAreaActive = (_a = area.active) != null ? _a : true; if (isAreaActive && (this.stayHighlighted || this.stayMultiHighlighted || this.toggleHighlighted)) { const newArea = { ...area }; const chosenArea = this.stayMultiHighlighted ? this.mapState : this.storedMap; if (this.toggleHighlighted && newArea.preFillColor) { delete newArea.preFillColor; } else if (this.stayHighlighted || this.stayMultiHighlighted) { newArea.preFillColor = area.fillColor || this.fillColor; } const updatedAreas = chosenArea.areas.map( (cur) => cur[this.areaKeyName] === area[this.areaKeyName] ? newArea : cur ); this.mapState = { ...this.mapState, areas: updatedAreas }; } if (this.$attrs.onClick) { event.preventDefault(); event.stopImmediatePropagation(); this.$emit("click", area, index, event); } }, scaleCoords(coords) { const scale = this.width && this.imgWidth && this.imgWidth > 0 ? this.width / this.imgWidth : 1; if (this.responsive && this.parentWidth) { return coords.map((coord) => coord / (this.imgRef.naturalWidth / this.parentWidth)); } return coords.map((coord) => coord * scale); }, renderPrefilledAreas(mapObj = this.mapState) { mapObj.areas.map((area) => { if (!area.preFillColor) return false; callingFn( area.shape, this.scaleCoords(area.coords), area.preFillColor, area.lineWidth || this.lineWidth, area.strokeColor || this.strokeColor, true, this.ctx ); return true; }); }, computeCenter(area) { if (!area) return [0, 0]; const scaledCoords = this.scaleCoords(area.coords); switch (area.shape) { case "circle": return [scaledCoords[0], scaledCoords[1]]; case "poly": case "rect": default: { const n = scaledCoords.length / 2; const { y: scaleY, x: scaleX } = scaledCoords.reduce( ({ y, x }, val, idx) => !(idx % 2) ? { y, x: x + val / n } : { y: y + val / n, x }, { y: 0, x: 0 } ); return [scaleX, scaleY]; } } }, updateCanvas() { this.ctx.clearRect(0, 0, this.$refs.canvas.width, this.$refs.canvas.height); this.renderPrefilledAreas(this.map); } } }); const _hoisted_1 = ["src", "useMap"]; const _hoisted_2 = ["name"]; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { const _component_RenderAreas = resolveComponent("RenderAreas"); return openBlock(), createElementBlock("div", { id: "img-mapper", ref: "container", style: normalizeStyle(_ctx.containerStyle) }, [ createElementVNode("img", { ref: "img", role: "presentation", class: "img-mapper-img", style: normalizeStyle(_ctx.imgStyle), src: _ctx.src, useMap: `#${_ctx.mapState.name}`, alt: "map", onLoad: _cache[0] || (_cache[0] = ($event) => _ctx.initCanvas(true)), onClick: _cache[1] || (_cache[1] = ($event) => _ctx.imageClick($event, this)), onMousemove: _cache[2] || (_cache[2] = ($event) => _ctx.imageMouseMove($event, this)) }, null, 44, _hoisted_1), createElementVNode("canvas", { ref: "canvas", class: "img-mapper-canvas", style: normalizeStyle(_ctx.canvasStyle) }, null, 4), _ctx.isRendered && !_ctx.disabled ? (openBlock(), createElementBlock("map", { key: 0, class: "img-mapper-map", name: _ctx.mapState.name, style: normalizeStyle(_ctx.mapStyle) }, [ (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.mapState.areas, (area, index) => { return openBlock(), createBlock(_component_RenderAreas, mergeProps({ key: area[_ctx.areaKeyName] || index.toString(), area, index, "extended-area": _ctx.extendedArea, "hover-on": _ctx.hoverOn, "hover-off": _ctx.hoverOff, click: _ctx.click }, _ctx.$attrs), null, 16, ["area", "index", "extended-area", "hover-on", "hover-off", "click"]); }), 128)) ], 12, _hoisted_2)) : createCommentVNode("", true) ], 4); } var ImageMapper = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]); export { ImageMapper as default };