@staski/vue-img-mapper
Version:
Vue Component to highlight interactive zones in images
522 lines (521 loc) • 15.9 kB
JavaScript
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 };