interactive_image.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. export default {
  2. template: `
  3. <div style="position:relative">
  4. <img
  5. ref="img"
  6. :src="computed_src"
  7. style="width:100%; height:100%;"
  8. @load="onImageLoaded"
  9. v-on="onCrossEvents"
  10. v-on="onUserEvents"
  11. draggable="false"
  12. />
  13. <svg style="position:absolute;top:0;left:0;pointer-events:none" :viewBox="viewBox">
  14. <g v-if="cross" :style="{ display: cssDisplay }">
  15. <line :x1="x" y1="0" :x2="x" y2="100%" stroke="black" />
  16. <line x1="0" :y1="y" x2="100%" :y2="y" stroke="black" />
  17. </g>
  18. <g v-html="content"></g>
  19. </svg>
  20. <slot></slot>
  21. </div>
  22. `,
  23. data() {
  24. return {
  25. viewBox: "0 0 0 0",
  26. x: 100,
  27. y: 100,
  28. cssDisplay: "none",
  29. computed_src: undefined,
  30. waiting_source: undefined,
  31. loading: false,
  32. };
  33. },
  34. mounted() {
  35. setTimeout(() => this.compute_src(), 0); // NOTE: wait for window.path_prefix to be set in app.mounted()
  36. const handle_completion = () => {
  37. if (this.waiting_source) {
  38. this.computed_src = this.waiting_source;
  39. this.waiting_source = undefined;
  40. } else {
  41. this.loading = false;
  42. }
  43. };
  44. this.$refs.img.addEventListener("load", handle_completion);
  45. this.$refs.img.addEventListener("error", handle_completion);
  46. },
  47. updated() {
  48. this.compute_src();
  49. },
  50. methods: {
  51. compute_src() {
  52. const new_src = (this.src.startsWith("/") ? window.path_prefix : "") + this.src;
  53. if (new_src == this.computed_src) {
  54. return;
  55. }
  56. if (this.loading) {
  57. this.waiting_source = new_src;
  58. } else {
  59. this.computed_src = new_src;
  60. this.loading = true;
  61. }
  62. },
  63. updateCrossHair(e) {
  64. this.x = (e.offsetX * e.target.naturalWidth) / e.target.clientWidth;
  65. this.y = (e.offsetY * e.target.naturalHeight) / e.target.clientHeight;
  66. },
  67. onImageLoaded(e) {
  68. this.viewBox = `0 0 ${e.target.naturalWidth} ${e.target.naturalHeight}`;
  69. },
  70. onMouseEvent(type, e) {
  71. this.$emit("mouse", {
  72. mouse_event_type: type,
  73. image_x: (e.offsetX * e.target.naturalWidth) / e.target.clientWidth,
  74. image_y: (e.offsetY * e.target.naturalHeight) / e.target.clientHeight,
  75. button: e.button,
  76. buttons: e.buttons,
  77. altKey: e.altKey,
  78. ctrlKey: e.ctrlKey,
  79. metaKey: e.metaKey,
  80. shiftKey: e.shiftKey,
  81. });
  82. },
  83. },
  84. computed: {
  85. onCrossEvents() {
  86. if (!this.cross) return {};
  87. return {
  88. mouseenter: () => (this.cssDisplay = "block"),
  89. mouseleave: () => (this.cssDisplay = "none"),
  90. mousemove: (event) => this.updateCrossHair(event),
  91. };
  92. },
  93. onUserEvents() {
  94. const events = {};
  95. for (const type of this.events) {
  96. events[type] = (event) => this.onMouseEvent(type, event);
  97. }
  98. return events;
  99. },
  100. },
  101. props: {
  102. src: String,
  103. content: String,
  104. events: Array,
  105. cross: Boolean,
  106. },
  107. };