annotate-image 2.0.0-beta.2 → 2.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js CHANGED
@@ -1,3 +1,30 @@
1
+ // src/positioning.ts
2
+ function clampNote(note, naturalWidth, naturalHeight) {
3
+ const width = Math.min(note.width, naturalWidth);
4
+ const height = Math.min(note.height, naturalHeight);
5
+ const left = Math.max(0, Math.min(note.left, naturalWidth - width));
6
+ const top = Math.max(0, Math.min(note.top, naturalHeight - height));
7
+ return { top, left, width, height };
8
+ }
9
+ function clampNotes(notes, naturalWidth, naturalHeight) {
10
+ for (const note of notes) {
11
+ Object.assign(note, clampNote(note, naturalWidth, naturalHeight));
12
+ }
13
+ }
14
+ function computeNoteLeft(noteWidth, areaLeftInViewport, areaWidth, viewportWidth) {
15
+ let left = (areaWidth - noteWidth) / 2;
16
+ const noteLeftInViewport = areaLeftInViewport + left;
17
+ const noteRightInViewport = noteLeftInViewport + noteWidth;
18
+ if (noteRightInViewport > viewportWidth) {
19
+ left -= noteRightInViewport - viewportWidth;
20
+ }
21
+ const adjustedNoteLeft = areaLeftInViewport + left;
22
+ if (adjustedNoteLeft < 0) {
23
+ left -= adjustedNoteLeft;
24
+ }
25
+ return left;
26
+ }
27
+
1
28
  // src/annotate-edit.ts
2
29
  var DEFAULT_NOTE_TOP = 30;
3
30
  var DEFAULT_NOTE_LEFT = 30;
@@ -47,6 +74,8 @@ var AnnotateEdit = class {
47
74
  formEl.appendChild(this.textarea);
48
75
  this.form.appendChild(formEl);
49
76
  this.area.appendChild(this.form);
77
+ this.positionForm();
78
+ this.textarea.focus();
50
79
  this.form.addEventListener("pointerdown", (e) => e.stopPropagation());
51
80
  const area = this.area;
52
81
  const applyRect = (rect) => {
@@ -58,7 +87,10 @@ var AnnotateEdit = class {
58
87
  this.handlers.makeResizable(area, {
59
88
  containment: image.canvas,
60
89
  onResize: applyRect,
61
- onStop: applyRect
90
+ onStop: (rect) => {
91
+ applyRect(rect);
92
+ this.positionForm();
93
+ }
62
94
  });
63
95
  this.handlers.makeDraggable(area, {
64
96
  containment: image.canvas,
@@ -69,9 +101,9 @@ var AnnotateEdit = class {
69
101
  onStop: (pos) => {
70
102
  area.style.left = pos.left + "px";
71
103
  area.style.top = pos.top + "px";
104
+ this.positionForm();
72
105
  }
73
106
  });
74
- this.textarea.focus();
75
107
  this.form.addEventListener("keydown", (e) => {
76
108
  if (e.key === "Escape") {
77
109
  const cancelBtn = this.form.querySelector(".image-annotate-edit-close");
@@ -87,6 +119,13 @@ var AnnotateEdit = class {
87
119
  }
88
120
  this.addCancelButton(buttonRow);
89
121
  }
122
+ /** Recompute the form's horizontal position relative to the area. */
123
+ positionForm() {
124
+ const formRect = this.form.getBoundingClientRect();
125
+ const areaRect = this.area.getBoundingClientRect();
126
+ const left = computeNoteLeft(formRect.width, areaRect.left, areaRect.width, window.innerWidth);
127
+ this.form.style.left = left + "px";
128
+ }
90
129
  /** Tear down the edit form and interaction handlers. */
91
130
  destroy() {
92
131
  this.image.activeEdit = null;
@@ -113,6 +152,7 @@ var AnnotateEdit = class {
113
152
  } else {
114
153
  this.note.editable = true;
115
154
  const view = new AnnotateView(this.image, this.note);
155
+ this.note.view = view;
116
156
  view.resetPosition(this, text);
117
157
  this.image.notes.push(this.note);
118
158
  }
@@ -270,7 +310,13 @@ var AnnotateView = class {
270
310
  }
271
311
  /** Show the tooltip and apply hover styling. */
272
312
  show() {
313
+ this.tooltip.style.visibility = "hidden";
273
314
  this.tooltip.style.display = "block";
315
+ const noteRect = this.tooltip.getBoundingClientRect();
316
+ const areaRect = this.area.getBoundingClientRect();
317
+ const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
318
+ this.tooltip.style.left = left + "px";
319
+ this.tooltip.style.visibility = "";
274
320
  if (!this.editable) {
275
321
  this.area.classList.add("image-annotate-area-hover");
276
322
  } else {
@@ -368,17 +414,19 @@ function destroyDraggable(el) {
368
414
  var MIN_SIZE = 10;
369
415
  var CORNERS = ["nw", "ne", "sw", "se"];
370
416
  function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
371
- let left = startLeft, top = startTop, width = startWidth, height = startHeight;
417
+ let left, top, width, height;
372
418
  if (corner === "nw" || corner === "sw") {
373
419
  left = startLeft + dx;
374
420
  width = startWidth - dx;
375
421
  } else {
422
+ left = startLeft;
376
423
  width = startWidth + dx;
377
424
  }
378
425
  if (corner === "nw" || corner === "ne") {
379
426
  top = startTop + dy;
380
427
  height = startHeight - dy;
381
428
  } else {
429
+ top = startTop;
382
430
  height = startHeight + dy;
383
431
  }
384
432
  if (width < MIN_SIZE) {
@@ -554,6 +602,9 @@ var AnnotateImage = class {
554
602
  this.originalNextSibling = img.nextSibling;
555
603
  this.canvas = document.createElement("div");
556
604
  this.canvas.className = "image-annotate-canvas";
605
+ if (options.theme) {
606
+ this.canvas.dataset.theme = options.theme;
607
+ }
557
608
  this.viewOverlay = document.createElement("div");
558
609
  this.viewOverlay.className = "image-annotate-view";
559
610
  this.editOverlay = document.createElement("div");
@@ -654,6 +705,7 @@ var AnnotateImage = class {
654
705
  for (const note of this.notes) {
655
706
  note.view?.destroy();
656
707
  }
708
+ this.viewOverlay.replaceChildren();
657
709
  }
658
710
  createViews() {
659
711
  for (const note of this.notes) {
@@ -663,6 +715,7 @@ var AnnotateImage = class {
663
715
  /** Rebuild annotation views from the current notes array. */
664
716
  load() {
665
717
  this.destroyViews();
718
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
666
719
  this.createViews();
667
720
  this.notifyLoad();
668
721
  }
@@ -771,6 +824,7 @@ var AnnotateImage = class {
771
824
  this.api.load().then((notes) => {
772
825
  this.destroyViews();
773
826
  this.notes = notes;
827
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
774
828
  this.createViews();
775
829
  this.notifyLoad();
776
830
  }).catch((err) => {
package/dist/core.min.js CHANGED
@@ -1 +1 @@
1
- "use strict";var AnnotateImage=(()=>{var P=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Y=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var V=(e,t)=>{for(var n in t)P(e,n,{get:t[n],enumerable:!0})},W=(e,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Y(t))!_.call(e,o)&&o!==n&&P(e,o,{get:()=>t[o],enumerable:!(i=X(t,o))||i.enumerable});return e};var U=e=>W(P({},"__esModule",{value:!0}),e);var at={};V(at,{AnnotateImage:()=>R,DEFAULT_LABELS:()=>H,annotate:()=>ot});var $=30,j=30,K=30,q=30,D=class{constructor(t,n,i){this.busy=!1;this.image=t,this.handlers=t.handlers,n?this.note=n:this.note={id:"new",top:$,left:j,width:K,height:q,text:"",editable:!0},this.area=t.editOverlay.querySelector(".image-annotate-edit-area");let o=t.toRendered(this.note);this.area.style.height=o.height+"px",this.area.style.width=o.width+"px",this.area.style.left=o.left+"px",this.area.style.top=o.top+"px",this.form=document.createElement("div"),this.form.className="image-annotate-edit-form";let d=document.createElement("form");this.textarea=document.createElement("textarea"),this.textarea.name="text",this.textarea.rows=3,this.textarea.cols=30,this.textarea.value=this.note.text;let l=this.image.options.labels?.placeholder??"";l&&(this.textarea.placeholder=l),d.appendChild(this.textarea),this.form.appendChild(d),this.area.appendChild(this.form),this.form.addEventListener("pointerdown",a=>a.stopPropagation());let s=this.area,c=a=>{s.style.left=a.left+"px",s.style.top=a.top+"px",s.style.width=a.width+"px",s.style.height=a.height+"px"};this.handlers.makeResizable(s,{containment:t.canvas,onResize:c,onStop:c}),this.handlers.makeDraggable(s,{containment:t.canvas,onDrag:a=>{s.style.left=a.left+"px",s.style.top=a.top+"px"},onStop:a=>{s.style.left=a.left+"px",s.style.top=a.top+"px"}}),this.textarea.focus(),this.form.addEventListener("keydown",a=>{if(a.key==="Escape"){let b=this.form.querySelector(".image-annotate-edit-close");b&&b.click()}});let r=document.createElement("div");r.className="image-annotate-edit-buttons",this.form.appendChild(r),this.addSaveButton(r,i),i&&this.addDeleteButton(r,i),this.addCancelButton(r)}destroy(){this.image.activeEdit=null,this.handlers.destroyResizable(this.area),this.handlers.destroyDraggable(this.area),this.area.style.height="",this.area.style.width="",this.area.style.left="",this.area.style.top="",this.form.remove()}addSaveButton(t,n){let i=document.createElement("button");i.className="image-annotate-edit-ok",i.textContent=this.image.options.labels?.save??"OK",i.type="button",i.addEventListener("click",()=>{if(this.busy)return;let o=this.textarea.value,d=()=>{this.image.setMode("view"),n?n.resetPosition(this,o):(this.note.editable=!0,new T(this.image,this.note).resetPosition(this,o),this.image.notes.push(this.note)),this.image.notifySave(C(this.note)),this.destroy(),this.image.flushPendingRescale()},l=O(this.area),s=S(this.area),c=this.image.toNatural({top:l.top,left:l.left,width:s.width,height:s.height});this.note.top=c.top,this.note.left=c.left,this.note.width=c.width,this.note.height=c.height,this.note.text=o,this.image.api.save?(this.busy=!0,this.image.api.save(C(this.note)).then(r=>{r.annotation_id!=null&&(this.note.id=r.annotation_id),d()}).catch(r=>{this.busy=!1;let a=r instanceof Error?r:new Error(String(r));this.image.reportError({type:"save",error:a,note:this.note})})):d()}),t.appendChild(i)}addDeleteButton(t,n){let i=document.createElement("button");i.className="image-annotate-edit-delete",i.textContent=this.image.options.labels?.delete??"Delete",i.type="button",i.addEventListener("click",()=>{if(this.busy)return;let o=()=>{this.image.setMode("view"),this.destroy(),n.destroy();let d=this.image.notes.indexOf(this.note);d!==-1&&this.image.notes.splice(d,1),this.image.notifyDelete(C(this.note)),this.image.flushPendingRescale()};this.image.api.delete?(this.busy=!0,this.image.api.delete(C(this.note)).then(()=>{o()}).catch(d=>{this.busy=!1;let l=d instanceof Error?d:new Error(String(d));this.image.reportError({type:"delete",error:l,note:this.note})})):o()}),t.appendChild(i)}addCancelButton(t){let n=document.createElement("button");n.className="image-annotate-edit-close",n.textContent=this.image.options.labels?.cancel??"Cancel",n.type="button",n.addEventListener("click",()=>{this.image.cancelEdit()}),t.appendChild(n)}};function O(e){return{left:parseInt(e.style.left)||0,top:parseInt(e.style.top)||0}}function S(e){return{width:parseInt(e.style.width)||e.offsetWidth,height:parseInt(e.style.height)||e.offsetHeight}}var T=class{constructor(t,n){this.image=t,this.note=n,this.editable=!!(n.editable&&t.options.editable),this.area=document.createElement("div"),this.area.className="image-annotate-area"+(this.editable?" image-annotate-area-editable":"");let i=document.createElement("div");this.area.appendChild(i),t.viewOverlay.insertBefore(this.area,t.viewOverlay.firstChild),this.tooltip=document.createElement("div"),this.tooltip.className="image-annotate-note",this.tooltip.textContent=n.text,this.tooltip.style.display="none",this.area.appendChild(this.tooltip),this.setPosition(),this.area.addEventListener("mouseenter",()=>this.show()),this.area.addEventListener("mouseleave",()=>this.hide()),this.editable&&(this.area.setAttribute("tabindex","0"),this.area.setAttribute("role","button"),this.area.addEventListener("click",()=>this.edit()),this.area.addEventListener("keydown",o=>{(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),this.edit())}))}setPosition(){let t=this.image.toRendered(this.note),n=this.area.firstElementChild;n.style.height=t.height+"px",n.style.width=t.width+"px",this.area.style.left=t.left+"px",this.area.style.top=t.top+"px"}resetPosition(t,n){this.tooltip.textContent=n,this.tooltip.style.display="none";let i=this.image.toRendered(t.note),o=this.area.firstElementChild;o.style.height=i.height+"px",o.style.width=i.width+"px",this.area.style.left=i.left+"px",this.area.style.top=i.top+"px",this.note.top=t.note.top,this.note.left=t.note.left,this.note.height=t.note.height,this.note.width=t.note.width,this.note.text=n,this.note.id=t.note.id,this.editable=!0}show(){this.tooltip.style.display="block",this.editable?this.area.classList.add("image-annotate-area-editable-hover"):this.area.classList.add("image-annotate-area-hover")}hide(){this.tooltip.style.display="none",this.area.classList.remove("image-annotate-area-hover"),this.area.classList.remove("image-annotate-area-editable-hover")}destroy(){this.area.remove(),this.tooltip.remove()}edit(){this.image.mode==="view"&&(this.image.setMode("edit"),this.image.activeEdit=new D(this.image,this.note,this))}};var I=new WeakMap,M=new WeakMap;function J(e,t){k(e);function n(i){if(i.button!==0)return;i.preventDefault(),e.setPointerCapture&&e.setPointerCapture(i.pointerId);let o=i.clientX,d=i.clientY,l=parseFloat(e.style.left)||0,s=parseFloat(e.style.top)||0,c=parseFloat(e.style.width)||e.offsetWidth,r=parseFloat(e.style.height)||e.offsetHeight,a=-1/0,b=-1/0,N=1/0,w=1/0;if(t.containment){let m=t.containment.getBoundingClientRect(),v=e.getBoundingClientRect(),g=l-(v.left-m.left),h=s-(v.top-m.top);a=g,b=h,N=m.width-c+g,w=m.height-r+h}function E(m,v,g){return Math.max(v,Math.min(g,m))}function L(m){let v=m.clientX-o,g=m.clientY-d,h=E(l+v,a,N),p=E(s+g,b,w);t.onDrag&&t.onDrag({left:h,top:p})}function x(m){e.releasePointerCapture&&e.releasePointerCapture(m.pointerId),e.removeEventListener("pointermove",L),e.removeEventListener("pointerup",x);let v=m.clientX-o,g=m.clientY-d,h=E(l+v,a,N),p=E(s+g,b,w);t.onStop&&t.onStop({left:h,top:p})}e.addEventListener("pointermove",L),e.addEventListener("pointerup",x)}e.addEventListener("pointerdown",n),I.set(e,()=>{e.removeEventListener("pointerdown",n)})}function k(e){let t=I.get(e);t&&(t(),I.delete(e))}var y=10,G=["nw","ne","sw","se"];function z(e,t,n,i,o,d,l){let s=t,c=n,r=i,a=o;return e==="nw"||e==="sw"?(s=t+d,r=i-d):r=i+d,e==="nw"||e==="ne"?(c=n+l,a=o-l):a=o+l,r<y&&((e==="nw"||e==="sw")&&(s=t+i-y),r=y),a<y&&((e==="nw"||e==="ne")&&(c=n+o-y),a=y),{left:s,top:c,width:r,height:a}}function Z(e,t){B(e);let n=[];for(let i of G){let o=document.createElement("div");o.className=`image-annotate-resize-handle image-annotate-resize-handle-${i}`,e.appendChild(o),n.push(o),o.addEventListener("pointerdown",function(l){if(l.button!==0)return;l.preventDefault(),l.stopPropagation(),o.setPointerCapture&&o.setPointerCapture(l.pointerId);let s=l.clientX,c=l.clientY,r=parseFloat(e.style.left)||0,a=parseFloat(e.style.top)||0,b=parseFloat(e.style.width)||0,N=parseFloat(e.style.height)||0,w=1/0,E=1/0,L=-1/0,x=-1/0;if(t.containment){let h=t.containment.getBoundingClientRect(),p=e.getBoundingClientRect(),f=r-(p.left-h.left),u=a-(p.top-h.top);L=f,x=u,w=h.width+f,E=h.height+u}function m(h){let{left:p,top:f,width:u,height:A}=h;return p<L&&(u-=L-p,p=L),f<x&&(A-=x-f,f=x),p+u>w&&(u=w-p),f+A>E&&(A=E-f),u<y&&(u=y),A<y&&(A=y),{left:p,top:f,width:u,height:A}}function v(h){let p=h.clientX-s,f=h.clientY-c,u=m(z(i,r,a,b,N,p,f));t.onResize?.(u)}function g(h){o.releasePointerCapture&&o.releasePointerCapture(h.pointerId),o.removeEventListener("pointermove",v),o.removeEventListener("pointerup",g);let p=h.clientX-s,f=h.clientY-c,u=m(z(i,r,a,b,N,p,f));t.onStop&&t.onStop(u)}o.addEventListener("pointermove",v),o.addEventListener("pointerup",g)})}M.set(e,()=>{for(let i of n)i.remove()})}function B(e){let t=M.get(e);t&&(t(),M.delete(e))}function F(){return{makeDraggable:J,makeResizable:Z,destroyDraggable:k,destroyResizable:B}}function C(e){let{view:t,editable:n,...i}=e;return i}function Q(e){return{load:typeof e.load=="string"?tt(e.load):e.load,save:typeof e.save=="string"?et(e.save):e.save,delete:typeof e.delete=="string"?nt(e.delete):e.delete}}function tt(e){return()=>fetch(e).then(t=>{if(!t.ok)throw new Error(`Load failed (HTTP ${t.status})`);return t.json()})}function et(e){return t=>fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).then(n=>{if(!n.ok)throw new Error(`Save failed (HTTP ${n.status})`);return n.json()})}function nt(e){return t=>fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).then(n=>{if(!n.ok)throw new Error(`Delete failed (HTTP ${n.status})`)})}var R=class{constructor(t,n){this._mode="view";this.activeEdit=null;this.destroyed=!1;this.pendingRescale=!1;this.originalParent=null;this.originalNextSibling=null;this.options=n,this.handlers=F(),this.img=t,this.naturalWidth=t.naturalWidth||t.width,this.naturalHeight=t.naturalHeight||t.height;let i=t.getBoundingClientRect(),o=i.width||t.width,d=i.height||t.height;if(this.naturalWidth===0||this.naturalHeight===0)throw new Error("image-annotate: image must have non-zero dimensions (is the image loaded?)");this.scaleX=o/this.naturalWidth,this.scaleY=d/this.naturalHeight,this.notes=n.notes.map(s=>({...s})),this.originalParent=t.parentNode,this.originalNextSibling=t.nextSibling,this.canvas=document.createElement("div"),this.canvas.className="image-annotate-canvas",this.viewOverlay=document.createElement("div"),this.viewOverlay.className="image-annotate-view",this.editOverlay=document.createElement("div"),this.editOverlay.className="image-annotate-edit",this.editOverlay.style.display="none";let l=document.createElement("div");if(l.className="image-annotate-edit-area",this.editOverlay.appendChild(l),!t.parentNode)throw new Error("image-annotate: image must be in the DOM before initialization");t.parentNode.insertBefore(this.canvas,t),this.canvas.appendChild(t),this.canvas.appendChild(this.viewOverlay),this.canvas.appendChild(this.editOverlay),this.api=this.options.api?Q(this.options.api):{},this.api.load?this.loadFromApi():this.load(),this.options.editable&&this.createButton(),n.autoResize!==!1&&typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(s=>{let c=s[0];if(!c)return;let{width:r,height:a}=c.contentRect;r===0||a===0||this.rescale(r,a)}),this.resizeObserver.observe(this.canvas))}toRendered(t){return{top:t.top*this.scaleY,left:t.left*this.scaleX,width:t.width*this.scaleX,height:t.height*this.scaleY}}toNatural(t){let n={top:t.top/this.scaleY,left:t.left/this.scaleX,width:t.width/this.scaleX,height:t.height/this.scaleY};if(!isFinite(n.top)||!isFinite(n.left)||!isFinite(n.width)||!isFinite(n.height))throw new Error("image-annotate: scale conversion produced non-finite coordinates");return n}get mode(){return this._mode}setMode(t){this._mode=t,t==="edit"?(this.canvas.classList.add("image-annotate-editing"),this.editOverlay.style.display="block"):(this.canvas.classList.remove("image-annotate-editing"),this.editOverlay.style.display="none")}getNotes(){return this.notes.map(C)}notifyChange(){this.options.onChange?.(this.getNotes())}notifySave(t){this.options.onSave?.(t),this.notifyChange()}notifyDelete(t){this.options.onDelete?.(t),this.notifyChange()}notifyLoad(){this.options.onLoad?.(this.getNotes()),this.notifyChange()}destroyViews(){this.cancelEdit();for(let t of this.notes)t.view?.destroy()}createViews(){for(let t of this.notes)t.view=new T(this,t)}load(){this.destroyViews(),this.createViews(),this.notifyLoad()}clear(){this.destroyViews(),this.notes=[],this.notifyChange()}destroy(){if(!this.destroyed){if(this.destroyed=!0,this.destroyViews(),this.notes=[],this.button&&this.button.remove(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=void 0),this.originalParent&&this.originalParent.isConnected){let t=this.originalNextSibling?.parentNode===this.originalParent?this.originalNextSibling:null;this.originalParent.insertBefore(this.img,t)}this.canvas.remove()}}cancelEdit(){this.activeEdit&&(this.activeEdit.destroy(),this.setMode("view")),this.flushPendingRescale()}rescale(t,n){if(this.mode==="edit"){this.pendingRescale=!0;return}this.applyRescale(t,n)}applyRescale(t,n){let i=t/this.naturalWidth,o=n/this.naturalHeight;i===this.scaleX&&o===this.scaleY||(this.scaleX=i,this.scaleY=o,this.destroyViews(),this.createViews())}flushPendingRescale(){if(!this.pendingRescale)return;this.pendingRescale=!1;let t=this.canvas.getBoundingClientRect();t.width>0&&t.height>0&&this.applyRescale(t.width,t.height)}setNotes(t){this.destroyed||(this.destroyViews(),this.notes=t.map(n=>({...n})),this.createViews())}setEditable(t){this.destroyed||this.options.editable!==t&&(this.options.editable=t,t&&!this.button?this.createButton():!t&&this.button&&(this.button.remove(),this.button=void 0),this.destroyViews(),this.createViews())}createButton(){this.button=document.createElement("button"),this.button.className="image-annotate-add",this.button.title=this.options.labels?.addNote??"Add Note",this.button.type="button",this.button.addEventListener("click",()=>{this.add()}),this.canvas.appendChild(this.button)}reportError(t){this.options.onError?this.options.onError(t):console.error(`image-annotate: ${t.type} failed`,t.error)}loadFromApi(){this.api.load&&this.api.load().then(t=>{this.destroyViews(),this.notes=t,this.createViews(),this.notifyLoad()}).catch(t=>{let n=t instanceof Error?t:new Error(String(t));this.reportError({type:"load",error:n})})}add(){return this.mode==="view"?(this.setMode("edit"),this.activeEdit=new D(this),!0):!1}};var H={addNote:"Add Note",save:"OK",delete:"Delete",cancel:"Cancel",placeholder:""};var it={editable:!0,notes:[],autoResize:!0,labels:{...H}};function ot(e,t){let n={...it,...t,labels:{...H,...t?.labels}};return new R(e,n)}return U(at);})();
1
+ "use strict";var AnnotateImage=(()=>{var M=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var W=Object.prototype.hasOwnProperty;var U=(e,t)=>{for(var n in t)M(e,n,{get:t[n],enumerable:!0})},j=(e,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of _(t))!W.call(e,o)&&o!==n&&M(e,o,{get:()=>t[o],enumerable:!(i=Y(t,o))||i.enumerable});return e};var $=e=>j(M({},"__esModule",{value:!0}),e);var lt={};U(lt,{AnnotateImage:()=>T,DEFAULT_LABELS:()=>P,annotate:()=>rt});function K(e,t,n){let i=Math.min(e.width,t),o=Math.min(e.height,n),h=Math.max(0,Math.min(e.left,t-i));return{top:Math.max(0,Math.min(e.top,n-o)),left:h,width:i,height:o}}function I(e,t,n){for(let i of e)Object.assign(i,K(i,t,n))}function H(e,t,n,i){let o=(n-e)/2,l=t+o+e;l>i&&(o-=l-i);let s=t+o;return s<0&&(o-=s),o}var q=30,J=30,G=30,Z=30,R=class{constructor(t,n,i){this.busy=!1;this.image=t,this.handlers=t.handlers,n?this.note=n:this.note={id:"new",top:q,left:J,width:G,height:Z,text:"",editable:!0},this.area=t.editOverlay.querySelector(".image-annotate-edit-area");let o=t.toRendered(this.note);this.area.style.height=o.height+"px",this.area.style.width=o.width+"px",this.area.style.left=o.left+"px",this.area.style.top=o.top+"px",this.form=document.createElement("div"),this.form.className="image-annotate-edit-form";let h=document.createElement("form");this.textarea=document.createElement("textarea"),this.textarea.name="text",this.textarea.rows=3,this.textarea.cols=30,this.textarea.value=this.note.text;let l=this.image.options.labels?.placeholder??"";l&&(this.textarea.placeholder=l),h.appendChild(this.textarea),this.form.appendChild(h),this.area.appendChild(this.form),this.positionForm(),this.textarea.focus(),this.form.addEventListener("pointerdown",a=>a.stopPropagation());let s=this.area,d=a=>{s.style.left=a.left+"px",s.style.top=a.top+"px",s.style.width=a.width+"px",s.style.height=a.height+"px"};this.handlers.makeResizable(s,{containment:t.canvas,onResize:d,onStop:a=>{d(a),this.positionForm()}}),this.handlers.makeDraggable(s,{containment:t.canvas,onDrag:a=>{s.style.left=a.left+"px",s.style.top=a.top+"px"},onStop:a=>{s.style.left=a.left+"px",s.style.top=a.top+"px",this.positionForm()}}),this.form.addEventListener("keydown",a=>{if(a.key==="Escape"){let y=this.form.querySelector(".image-annotate-edit-close");y&&y.click()}});let r=document.createElement("div");r.className="image-annotate-edit-buttons",this.form.appendChild(r),this.addSaveButton(r,i),i&&this.addDeleteButton(r,i),this.addCancelButton(r)}positionForm(){let t=this.form.getBoundingClientRect(),n=this.area.getBoundingClientRect(),i=H(t.width,n.left,n.width,window.innerWidth);this.form.style.left=i+"px"}destroy(){this.image.activeEdit=null,this.handlers.destroyResizable(this.area),this.handlers.destroyDraggable(this.area),this.area.style.height="",this.area.style.width="",this.area.style.left="",this.area.style.top="",this.form.remove()}addSaveButton(t,n){let i=document.createElement("button");i.className="image-annotate-edit-ok",i.textContent=this.image.options.labels?.save??"OK",i.type="button",i.addEventListener("click",()=>{if(this.busy)return;let o=this.textarea.value,h=()=>{if(this.image.setMode("view"),n)n.resetPosition(this,o);else{this.note.editable=!0;let r=new D(this.image,this.note);this.note.view=r,r.resetPosition(this,o),this.image.notes.push(this.note)}this.image.notifySave(C(this.note)),this.destroy(),this.image.flushPendingRescale()},l=z(this.area),s=k(this.area),d=this.image.toNatural({top:l.top,left:l.left,width:s.width,height:s.height});this.note.top=d.top,this.note.left=d.left,this.note.width=d.width,this.note.height=d.height,this.note.text=o,this.image.api.save?(this.busy=!0,this.image.api.save(C(this.note)).then(r=>{r.annotation_id!=null&&(this.note.id=r.annotation_id),h()}).catch(r=>{this.busy=!1;let a=r instanceof Error?r:new Error(String(r));this.image.reportError({type:"save",error:a,note:this.note})})):h()}),t.appendChild(i)}addDeleteButton(t,n){let i=document.createElement("button");i.className="image-annotate-edit-delete",i.textContent=this.image.options.labels?.delete??"Delete",i.type="button",i.addEventListener("click",()=>{if(this.busy)return;let o=()=>{this.image.setMode("view"),this.destroy(),n.destroy();let h=this.image.notes.indexOf(this.note);h!==-1&&this.image.notes.splice(h,1),this.image.notifyDelete(C(this.note)),this.image.flushPendingRescale()};this.image.api.delete?(this.busy=!0,this.image.api.delete(C(this.note)).then(()=>{o()}).catch(h=>{this.busy=!1;let l=h instanceof Error?h:new Error(String(h));this.image.reportError({type:"delete",error:l,note:this.note})})):o()}),t.appendChild(i)}addCancelButton(t){let n=document.createElement("button");n.className="image-annotate-edit-close",n.textContent=this.image.options.labels?.cancel??"Cancel",n.type="button",n.addEventListener("click",()=>{this.image.cancelEdit()}),t.appendChild(n)}};function z(e){return{left:parseInt(e.style.left)||0,top:parseInt(e.style.top)||0}}function k(e){return{width:parseInt(e.style.width)||e.offsetWidth,height:parseInt(e.style.height)||e.offsetHeight}}var D=class{constructor(t,n){this.image=t,this.note=n,this.editable=!!(n.editable&&t.options.editable),this.area=document.createElement("div"),this.area.className="image-annotate-area"+(this.editable?" image-annotate-area-editable":"");let i=document.createElement("div");this.area.appendChild(i),t.viewOverlay.insertBefore(this.area,t.viewOverlay.firstChild),this.tooltip=document.createElement("div"),this.tooltip.className="image-annotate-note",this.tooltip.textContent=n.text,this.tooltip.style.display="none",this.area.appendChild(this.tooltip),this.setPosition(),this.area.addEventListener("mouseenter",()=>this.show()),this.area.addEventListener("mouseleave",()=>this.hide()),this.editable&&(this.area.setAttribute("tabindex","0"),this.area.setAttribute("role","button"),this.area.addEventListener("click",()=>this.edit()),this.area.addEventListener("keydown",o=>{(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),this.edit())}))}setPosition(){let t=this.image.toRendered(this.note),n=this.area.firstElementChild;n.style.height=t.height+"px",n.style.width=t.width+"px",this.area.style.left=t.left+"px",this.area.style.top=t.top+"px"}resetPosition(t,n){this.tooltip.textContent=n,this.tooltip.style.display="none";let i=this.image.toRendered(t.note),o=this.area.firstElementChild;o.style.height=i.height+"px",o.style.width=i.width+"px",this.area.style.left=i.left+"px",this.area.style.top=i.top+"px",this.note.top=t.note.top,this.note.left=t.note.left,this.note.height=t.note.height,this.note.width=t.note.width,this.note.text=n,this.note.id=t.note.id,this.editable=!0}show(){this.tooltip.style.visibility="hidden",this.tooltip.style.display="block";let t=this.tooltip.getBoundingClientRect(),n=this.area.getBoundingClientRect(),i=H(t.width,n.left,n.width,window.innerWidth);this.tooltip.style.left=i+"px",this.tooltip.style.visibility="",this.editable?this.area.classList.add("image-annotate-area-editable-hover"):this.area.classList.add("image-annotate-area-hover")}hide(){this.tooltip.style.display="none",this.area.classList.remove("image-annotate-area-hover"),this.area.classList.remove("image-annotate-area-editable-hover")}destroy(){this.area.remove(),this.tooltip.remove()}edit(){this.image.mode==="view"&&(this.image.setMode("edit"),this.image.activeEdit=new R(this.image,this.note,this))}};var O=new WeakMap,S=new WeakMap;function Q(e,t){F(e);function n(i){if(i.button!==0)return;i.preventDefault(),e.setPointerCapture&&e.setPointerCapture(i.pointerId);let o=i.clientX,h=i.clientY,l=parseFloat(e.style.left)||0,s=parseFloat(e.style.top)||0,d=parseFloat(e.style.width)||e.offsetWidth,r=parseFloat(e.style.height)||e.offsetHeight,a=-1/0,y=-1/0,N=1/0,E=1/0;if(t.containment){let p=t.containment.getBoundingClientRect(),v=e.getBoundingClientRect(),g=l-(v.left-p.left),c=s-(v.top-p.top);a=g,y=c,N=p.width-d+g,E=p.height-r+c}function w(p,v,g){return Math.max(v,Math.min(g,p))}function x(p){let v=p.clientX-o,g=p.clientY-h,c=w(l+v,a,N),m=w(s+g,y,E);t.onDrag&&t.onDrag({left:c,top:m})}function L(p){e.releasePointerCapture&&e.releasePointerCapture(p.pointerId),e.removeEventListener("pointermove",x),e.removeEventListener("pointerup",L);let v=p.clientX-o,g=p.clientY-h,c=w(l+v,a,N),m=w(s+g,y,E);t.onStop&&t.onStop({left:c,top:m})}e.addEventListener("pointermove",x),e.addEventListener("pointerup",L)}e.addEventListener("pointerdown",n),O.set(e,()=>{e.removeEventListener("pointerdown",n)})}function F(e){let t=O.get(e);t&&(t(),O.delete(e))}var b=10,tt=["nw","ne","sw","se"];function B(e,t,n,i,o,h,l){let s,d,r,a;return e==="nw"||e==="sw"?(s=t+h,r=i-h):(s=t,r=i+h),e==="nw"||e==="ne"?(d=n+l,a=o-l):(d=n,a=o+l),r<b&&((e==="nw"||e==="sw")&&(s=t+i-b),r=b),a<b&&((e==="nw"||e==="ne")&&(d=n+o-b),a=b),{left:s,top:d,width:r,height:a}}function et(e,t){V(e);let n=[];for(let i of tt){let o=document.createElement("div");o.className=`image-annotate-resize-handle image-annotate-resize-handle-${i}`,e.appendChild(o),n.push(o),o.addEventListener("pointerdown",function(l){if(l.button!==0)return;l.preventDefault(),l.stopPropagation(),o.setPointerCapture&&o.setPointerCapture(l.pointerId);let s=l.clientX,d=l.clientY,r=parseFloat(e.style.left)||0,a=parseFloat(e.style.top)||0,y=parseFloat(e.style.width)||0,N=parseFloat(e.style.height)||0,E=1/0,w=1/0,x=-1/0,L=-1/0;if(t.containment){let c=t.containment.getBoundingClientRect(),m=e.getBoundingClientRect(),u=r-(m.left-c.left),f=a-(m.top-c.top);x=u,L=f,E=c.width+u,w=c.height+f}function p(c){let{left:m,top:u,width:f,height:A}=c;return m<x&&(f-=x-m,m=x),u<L&&(A-=L-u,u=L),m+f>E&&(f=E-m),u+A>w&&(A=w-u),f<b&&(f=b),A<b&&(A=b),{left:m,top:u,width:f,height:A}}function v(c){let m=c.clientX-s,u=c.clientY-d,f=p(B(i,r,a,y,N,m,u));t.onResize?.(f)}function g(c){o.releasePointerCapture&&o.releasePointerCapture(c.pointerId),o.removeEventListener("pointermove",v),o.removeEventListener("pointerup",g);let m=c.clientX-s,u=c.clientY-d,f=p(B(i,r,a,y,N,m,u));t.onStop&&t.onStop(f)}o.addEventListener("pointermove",v),o.addEventListener("pointerup",g)})}S.set(e,()=>{for(let i of n)i.remove()})}function V(e){let t=S.get(e);t&&(t(),S.delete(e))}function X(){return{makeDraggable:Q,makeResizable:et,destroyDraggable:F,destroyResizable:V}}function C(e){let{view:t,editable:n,...i}=e;return i}function nt(e){return{load:typeof e.load=="string"?it(e.load):e.load,save:typeof e.save=="string"?ot(e.save):e.save,delete:typeof e.delete=="string"?at(e.delete):e.delete}}function it(e){return()=>fetch(e).then(t=>{if(!t.ok)throw new Error(`Load failed (HTTP ${t.status})`);return t.json()})}function ot(e){return t=>fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).then(n=>{if(!n.ok)throw new Error(`Save failed (HTTP ${n.status})`);return n.json()})}function at(e){return t=>fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).then(n=>{if(!n.ok)throw new Error(`Delete failed (HTTP ${n.status})`)})}var T=class{constructor(t,n){this._mode="view";this.activeEdit=null;this.destroyed=!1;this.pendingRescale=!1;this.originalParent=null;this.originalNextSibling=null;this.options=n,this.handlers=X(),this.img=t,this.naturalWidth=t.naturalWidth||t.width,this.naturalHeight=t.naturalHeight||t.height;let i=t.getBoundingClientRect(),o=i.width||t.width,h=i.height||t.height;if(this.naturalWidth===0||this.naturalHeight===0)throw new Error("image-annotate: image must have non-zero dimensions (is the image loaded?)");this.scaleX=o/this.naturalWidth,this.scaleY=h/this.naturalHeight,this.notes=n.notes.map(s=>({...s})),this.originalParent=t.parentNode,this.originalNextSibling=t.nextSibling,this.canvas=document.createElement("div"),this.canvas.className="image-annotate-canvas",n.theme&&(this.canvas.dataset.theme=n.theme),this.viewOverlay=document.createElement("div"),this.viewOverlay.className="image-annotate-view",this.editOverlay=document.createElement("div"),this.editOverlay.className="image-annotate-edit",this.editOverlay.style.display="none";let l=document.createElement("div");if(l.className="image-annotate-edit-area",this.editOverlay.appendChild(l),!t.parentNode)throw new Error("image-annotate: image must be in the DOM before initialization");t.parentNode.insertBefore(this.canvas,t),this.canvas.appendChild(t),this.canvas.appendChild(this.viewOverlay),this.canvas.appendChild(this.editOverlay),this.api=this.options.api?nt(this.options.api):{},this.api.load?this.loadFromApi():this.load(),this.options.editable&&this.createButton(),n.autoResize!==!1&&typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(s=>{let d=s[0];if(!d)return;let{width:r,height:a}=d.contentRect;r===0||a===0||this.rescale(r,a)}),this.resizeObserver.observe(this.canvas))}toRendered(t){return{top:t.top*this.scaleY,left:t.left*this.scaleX,width:t.width*this.scaleX,height:t.height*this.scaleY}}toNatural(t){let n={top:t.top/this.scaleY,left:t.left/this.scaleX,width:t.width/this.scaleX,height:t.height/this.scaleY};if(!isFinite(n.top)||!isFinite(n.left)||!isFinite(n.width)||!isFinite(n.height))throw new Error("image-annotate: scale conversion produced non-finite coordinates");return n}get mode(){return this._mode}setMode(t){this._mode=t,t==="edit"?(this.canvas.classList.add("image-annotate-editing"),this.editOverlay.style.display="block"):(this.canvas.classList.remove("image-annotate-editing"),this.editOverlay.style.display="none")}getNotes(){return this.notes.map(C)}notifyChange(){this.options.onChange?.(this.getNotes())}notifySave(t){this.options.onSave?.(t),this.notifyChange()}notifyDelete(t){this.options.onDelete?.(t),this.notifyChange()}notifyLoad(){this.options.onLoad?.(this.getNotes()),this.notifyChange()}destroyViews(){this.cancelEdit();for(let t of this.notes)t.view?.destroy();this.viewOverlay.replaceChildren()}createViews(){for(let t of this.notes)t.view=new D(this,t)}load(){this.destroyViews(),I(this.notes,this.naturalWidth,this.naturalHeight),this.createViews(),this.notifyLoad()}clear(){this.destroyViews(),this.notes=[],this.notifyChange()}destroy(){if(!this.destroyed){if(this.destroyed=!0,this.destroyViews(),this.notes=[],this.button&&this.button.remove(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=void 0),this.originalParent&&this.originalParent.isConnected){let t=this.originalNextSibling?.parentNode===this.originalParent?this.originalNextSibling:null;this.originalParent.insertBefore(this.img,t)}this.canvas.remove()}}cancelEdit(){this.activeEdit&&(this.activeEdit.destroy(),this.setMode("view")),this.flushPendingRescale()}rescale(t,n){if(this.mode==="edit"){this.pendingRescale=!0;return}this.applyRescale(t,n)}applyRescale(t,n){let i=t/this.naturalWidth,o=n/this.naturalHeight;i===this.scaleX&&o===this.scaleY||(this.scaleX=i,this.scaleY=o,this.destroyViews(),this.createViews())}flushPendingRescale(){if(!this.pendingRescale)return;this.pendingRescale=!1;let t=this.canvas.getBoundingClientRect();t.width>0&&t.height>0&&this.applyRescale(t.width,t.height)}setNotes(t){this.destroyed||(this.destroyViews(),this.notes=t.map(n=>({...n})),this.createViews())}setEditable(t){this.destroyed||this.options.editable!==t&&(this.options.editable=t,t&&!this.button?this.createButton():!t&&this.button&&(this.button.remove(),this.button=void 0),this.destroyViews(),this.createViews())}createButton(){this.button=document.createElement("button"),this.button.className="image-annotate-add",this.button.title=this.options.labels?.addNote??"Add Note",this.button.type="button",this.button.addEventListener("click",()=>{this.add()}),this.canvas.appendChild(this.button)}reportError(t){this.options.onError?this.options.onError(t):console.error(`image-annotate: ${t.type} failed`,t.error)}loadFromApi(){this.api.load&&this.api.load().then(t=>{this.destroyViews(),this.notes=t,I(this.notes,this.naturalWidth,this.naturalHeight),this.createViews(),this.notifyLoad()}).catch(t=>{let n=t instanceof Error?t:new Error(String(t));this.reportError({type:"load",error:n})})}add(){return this.mode==="view"?(this.setMode("edit"),this.activeEdit=new R(this),!0):!1}};var P={addNote:"Add Note",save:"OK",delete:"Delete",cancel:"Cancel",placeholder:""};var st={editable:!0,notes:[],autoResize:!0,labels:{...P}};function rt(e,t){let n={...st,...t,labels:{...P,...t?.labels}};return new T(e,n)}return $(lt);})();
@@ -1 +1 @@
1
- .image-annotate-add{--image-annotate-icon-add: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23fff'%3E%3Cpath d='M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z'/%3E%3C/svg%3E");appearance:none;background-color:#0006;background-image:var(--image-annotate-icon-add);background-repeat:no-repeat;background-position:center;background-size:20px 20px;border:1px solid rgba(255,255,255,.5);border-radius:4px;bottom:8px;color:#fff;cursor:pointer;height:32px;opacity:0;padding:0;position:absolute;right:8px;transition:opacity .2s,background-color .2s;width:32px;z-index:1}.image-annotate-add:hover{background-color:#0009}.image-annotate-add:focus{opacity:1;outline:2px solid var(--image-annotate-hover-editable-color);outline-offset:2px}.image-annotate-canvas{--image-annotate-font-family: Verdana, sans-serif;--image-annotate-font-size: 12px;--image-annotate-area-border: #000;--image-annotate-area-inner-border: #fff;--image-annotate-hover-color: yellow;--image-annotate-hover-editable-color: #00ad00;--image-annotate-note-bg: #e7ffe7;--image-annotate-note-border: #397f39;--image-annotate-note-text: #000;--image-annotate-edit-bg: #fffee3;--image-annotate-edit-border: #000;--image-annotate-button-bg: #fff;--image-annotate-button-bg-hover: #eee;--image-annotate-button-border: #ccc;--image-annotate-button-text: #000;--image-annotate-icon-save: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23333'%3E%3Cpath d='M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z'/%3E%3C/svg%3E");--image-annotate-icon-cancel: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23333'%3E%3Cpath d='M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z'/%3E%3C/svg%3E");--image-annotate-icon-delete: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23333'%3E%3Cpath d='M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z'/%3E%3C/svg%3E");border:solid 1px var(--image-annotate-button-border);display:inline-block;margin:0;max-width:100%;position:relative}.image-annotate-canvas>img{display:block;height:auto;max-width:100%}.image-annotate-view{display:none;inset:0;position:absolute}@media(hover:hover){.image-annotate-canvas:hover:not(.image-annotate-editing) .image-annotate-view{display:block}}@media(hover:none){.image-annotate-canvas:not(.image-annotate-editing) .image-annotate-view{display:block}}@media(hover:hover){.image-annotate-canvas:hover:not(.image-annotate-editing) .image-annotate-add{opacity:1}}@media(hover:none){.image-annotate-canvas:not(.image-annotate-editing) .image-annotate-add{opacity:1}}.image-annotate-editing .image-annotate-add{display:none}.image-annotate-area{border:1px solid var(--image-annotate-area-border);position:absolute}.image-annotate-area div{border:1px solid var(--image-annotate-area-inner-border);box-sizing:border-box;display:block}.image-annotate-area-hover div{border-color:var(--image-annotate-hover-color)}.image-annotate-area-editable{cursor:pointer}.image-annotate-area-editable-hover div{border-color:var(--image-annotate-hover-editable-color)}.image-annotate-note{background-color:var(--image-annotate-note-bg);border:solid 1px var(--image-annotate-note-border);color:var(--image-annotate-note-text);display:none;font-family:var(--image-annotate-font-family);font-size:var(--image-annotate-font-size);left:-1px;max-width:200px;padding:3px 7px;position:absolute;top:calc(100% + 7px)}.image-annotate-note .actions{display:block;font-size:80%}.image-annotate-edit{display:none;inset:0;position:absolute}.image-annotate-edit-form{background-color:var(--image-annotate-edit-bg);border:1px solid var(--image-annotate-edit-border);box-sizing:border-box;cursor:default;display:flex;flex-direction:column;gap:7px;left:-1px;min-width:250px;padding:7px;position:absolute;top:calc(100% + 7px);width:max-content}.image-annotate-edit-form form{margin:0;padding:0}.image-annotate-edit-form textarea{box-sizing:border-box;font-family:var(--image-annotate-font-family);font-size:var(--image-annotate-font-size);height:50px;width:100%}.image-annotate-edit-buttons{display:flex;flex-wrap:nowrap;gap:6px}.image-annotate-edit-form button{appearance:none;background-color:var(--image-annotate-button-bg);background-repeat:no-repeat;background-position:3px center;background-size:16px 16px;border:solid 1px var(--image-annotate-button-border);color:var(--image-annotate-button-text);cursor:pointer;font-family:var(--image-annotate-font-family);font-size:var(--image-annotate-font-size);line-height:18px;min-height:22px;padding:2px 6px 2px 24px;white-space:nowrap}.image-annotate-edit-form button:empty{background-position:center;min-width:24px;padding:2px 4px}.image-annotate-edit-form button:hover{background-color:var(--image-annotate-button-bg-hover)}.image-annotate-edit-area{border:1px solid var(--image-annotate-area-border);cursor:move;display:block;height:60px;left:10px;margin:0;padding:0;position:absolute;top:10px;width:60px}.image-annotate-resize-handle{background-color:var(--image-annotate-area-border);border:1px solid var(--image-annotate-area-inner-border);box-sizing:border-box;height:8px;position:absolute;width:8px}.image-annotate-resize-handle-nw{cursor:nw-resize;left:-4px;top:-4px}.image-annotate-resize-handle-ne{cursor:ne-resize;right:-4px;top:-4px}.image-annotate-resize-handle-sw{cursor:sw-resize;left:-4px;bottom:-4px}.image-annotate-resize-handle-se{cursor:se-resize;right:-4px;bottom:-4px}.image-annotate-edit-ok{background-image:var(--image-annotate-icon-save)}.image-annotate-edit-delete{background-image:var(--image-annotate-icon-delete)}.image-annotate-edit-close{background-image:var(--image-annotate-icon-cancel)}.image-annotate-area-editable:focus{outline:2px solid var(--image-annotate-hover-editable-color);outline-offset:2px}.image-annotate-edit-form button:focus{outline:2px solid var(--image-annotate-hover-editable-color);outline-offset:1px}
1
+ .image-annotate-add{--image-annotate-icon-add: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23fff'%3E%3Cpath d='M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z'/%3E%3C/svg%3E");appearance:none;background-color:var(--image-annotate-add-bg);background-image:var(--image-annotate-icon-add);background-repeat:no-repeat;background-position:center;background-size:20px 20px;border:var(--image-annotate-add-border);border-radius:var(--image-annotate-add-radius);bottom:8px;color:#fff;cursor:pointer;height:32px;opacity:0;padding:0;position:absolute;right:8px;transition:opacity .2s,background-color .2s;width:32px;z-index:1}.image-annotate-add:hover{background-color:var(--image-annotate-add-bg-hover)}.image-annotate-add:focus{opacity:1;outline:2px solid var(--image-annotate-hover-editable-color);outline-offset:2px}.image-annotate-canvas{--image-annotate-font-family: Verdana, sans-serif;--image-annotate-font-size: 12px;--image-annotate-area-border: #000;--image-annotate-area-inner-border: #fff;--image-annotate-hover-color: yellow;--image-annotate-hover-editable-color: #00ad00;--image-annotate-note-bg: #e7ffe7;--image-annotate-note-border: #397f39;--image-annotate-note-text: #000;--image-annotate-edit-bg: #fffee3;--image-annotate-edit-border: #000;--image-annotate-button-bg: #fff;--image-annotate-button-bg-hover: #eee;--image-annotate-button-border: #ccc;--image-annotate-button-text: #000;--image-annotate-icon-save: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23fff'%3E%3Cpath d='M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z'/%3E%3C/svg%3E");--image-annotate-icon-cancel: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23fff'%3E%3Cpath d='M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z'/%3E%3C/svg%3E");--image-annotate-icon-delete: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' fill='%23fff'%3E%3Cpath d='M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z'/%3E%3C/svg%3E");--image-annotate-canvas-border: none;--image-annotate-area-bg: transparent;--image-annotate-area-border-width: 1px;--image-annotate-area-border-style: solid;--image-annotate-area-radius: 0;--image-annotate-hover-bg: transparent;--image-annotate-note-radius: 0;--image-annotate-note-shadow: none;--image-annotate-edit-radius: 0;--image-annotate-edit-shadow: none;--image-annotate-note-max-width: 300px;--image-annotate-edit-max-width: 300px;--image-annotate-add-bg: rgba(0, 0, 0, .4);--image-annotate-add-bg-hover: rgba(0, 0, 0, .6);--image-annotate-add-border: 1px solid rgba(255, 255, 255, .5);--image-annotate-add-radius: 4px;border:var(--image-annotate-canvas-border);display:inline-block;margin:0;max-width:100%;position:relative}.image-annotate-canvas>img{display:block;height:auto;max-width:100%}.image-annotate-view{display:none;inset:0;position:absolute}@media(hover:hover){.image-annotate-canvas:hover:not(.image-annotate-editing) .image-annotate-view{display:block}}@media(hover:none){.image-annotate-canvas:not(.image-annotate-editing) .image-annotate-view{display:block}}@media(hover:hover){.image-annotate-canvas:hover:not(.image-annotate-editing) .image-annotate-add{opacity:1}}@media(hover:none){.image-annotate-canvas:not(.image-annotate-editing) .image-annotate-add{opacity:1}}.image-annotate-editing .image-annotate-add{display:none}.image-annotate-area{background-color:var(--image-annotate-area-bg);border:var(--image-annotate-area-border-width) var(--image-annotate-area-border-style) var(--image-annotate-area-border);border-radius:var(--image-annotate-area-radius);position:absolute}.image-annotate-area div{border:1px solid var(--image-annotate-area-inner-border);border-radius:var(--image-annotate-area-radius);box-sizing:border-box;display:block}.image-annotate-area-hover div{border-color:var(--image-annotate-hover-color)}.image-annotate-area-hover{background-color:var(--image-annotate-hover-bg);z-index:1}.image-annotate-area-editable{cursor:pointer}.image-annotate-area-editable-hover{z-index:1}.image-annotate-area-editable-hover div{border-color:var(--image-annotate-hover-editable-color)}.image-annotate-note{background-color:var(--image-annotate-note-bg);border:solid 1px var(--image-annotate-note-border);border-radius:var(--image-annotate-note-radius);box-shadow:var(--image-annotate-note-shadow);color:var(--image-annotate-note-text);display:none;font-family:var(--image-annotate-font-family);font-size:var(--image-annotate-font-size);left:0;max-width:var(--image-annotate-note-max-width);padding:3px 7px;position:absolute;top:calc(100% + 7px);width:max-content}.image-annotate-note .actions{display:block;font-size:80%}.image-annotate-edit{display:none;inset:0;position:absolute}.image-annotate-edit-form{background-color:var(--image-annotate-edit-bg);border:1px solid var(--image-annotate-edit-border);border-radius:var(--image-annotate-edit-radius);box-shadow:var(--image-annotate-edit-shadow);box-sizing:border-box;cursor:default;display:flex;flex-direction:column;gap:7px;left:0;max-width:var(--image-annotate-edit-max-width);min-width:250px;padding:7px;position:absolute;top:calc(100% + 7px);width:max-content}.image-annotate-edit-form form{margin:0;padding:0}.image-annotate-edit-form textarea{box-sizing:border-box;font-family:var(--image-annotate-font-family);font-size:var(--image-annotate-font-size);height:50px;width:100%}.image-annotate-edit-buttons{display:flex;flex-wrap:nowrap;gap:6px}.image-annotate-edit-form button{appearance:none;background-color:var(--image-annotate-button-bg);border:solid 1px var(--image-annotate-button-border);color:var(--image-annotate-button-text);cursor:pointer;font-family:var(--image-annotate-font-family);font-size:var(--image-annotate-font-size);line-height:18px;min-height:22px;padding:2px 6px 2px 24px;position:relative;white-space:nowrap}.image-annotate-edit-form button:before{background-color:var(--image-annotate-button-text);content:"";height:16px;left:3px;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:16px 16px;mask-size:16px 16px;position:absolute;top:50%;transform:translateY(-50%);width:16px}.image-annotate-edit-form button:empty{min-width:24px;padding:2px 4px}.image-annotate-edit-form button:empty:before{left:50%;transform:translate(-50%,-50%)}.image-annotate-edit-form button:hover{background-color:var(--image-annotate-button-bg-hover)}.image-annotate-edit-area{border:1px solid var(--image-annotate-area-border);cursor:move;display:block;height:60px;left:10px;margin:0;padding:0;position:absolute;top:10px;width:60px}.image-annotate-resize-handle{background-color:var(--image-annotate-area-border);border:1px solid var(--image-annotate-area-inner-border);box-sizing:border-box;height:8px;position:absolute;width:8px}.image-annotate-resize-handle-nw{cursor:nw-resize;left:-4px;top:-4px}.image-annotate-resize-handle-ne{cursor:ne-resize;right:-4px;top:-4px}.image-annotate-resize-handle-sw{cursor:sw-resize;left:-4px;bottom:-4px}.image-annotate-resize-handle-se{cursor:se-resize;right:-4px;bottom:-4px}.image-annotate-edit-ok:before{-webkit-mask-image:var(--image-annotate-icon-save);mask-image:var(--image-annotate-icon-save)}.image-annotate-edit-delete:before{-webkit-mask-image:var(--image-annotate-icon-delete);mask-image:var(--image-annotate-icon-delete)}.image-annotate-edit-close:before{-webkit-mask-image:var(--image-annotate-icon-cancel);mask-image:var(--image-annotate-icon-cancel)}.image-annotate-area-editable:focus{outline:2px solid var(--image-annotate-hover-editable-color);outline-offset:2px}.image-annotate-edit-form button:focus{outline:2px solid var(--image-annotate-hover-editable-color);outline-offset:1px}
package/dist/jquery.js CHANGED
@@ -1,3 +1,30 @@
1
+ // src/positioning.ts
2
+ function clampNote(note, naturalWidth, naturalHeight) {
3
+ const width = Math.min(note.width, naturalWidth);
4
+ const height = Math.min(note.height, naturalHeight);
5
+ const left = Math.max(0, Math.min(note.left, naturalWidth - width));
6
+ const top = Math.max(0, Math.min(note.top, naturalHeight - height));
7
+ return { top, left, width, height };
8
+ }
9
+ function clampNotes(notes, naturalWidth, naturalHeight) {
10
+ for (const note of notes) {
11
+ Object.assign(note, clampNote(note, naturalWidth, naturalHeight));
12
+ }
13
+ }
14
+ function computeNoteLeft(noteWidth, areaLeftInViewport, areaWidth, viewportWidth) {
15
+ let left = (areaWidth - noteWidth) / 2;
16
+ const noteLeftInViewport = areaLeftInViewport + left;
17
+ const noteRightInViewport = noteLeftInViewport + noteWidth;
18
+ if (noteRightInViewport > viewportWidth) {
19
+ left -= noteRightInViewport - viewportWidth;
20
+ }
21
+ const adjustedNoteLeft = areaLeftInViewport + left;
22
+ if (adjustedNoteLeft < 0) {
23
+ left -= adjustedNoteLeft;
24
+ }
25
+ return left;
26
+ }
27
+
1
28
  // src/annotate-edit.ts
2
29
  var DEFAULT_NOTE_TOP = 30;
3
30
  var DEFAULT_NOTE_LEFT = 30;
@@ -47,6 +74,8 @@ var AnnotateEdit = class {
47
74
  formEl.appendChild(this.textarea);
48
75
  this.form.appendChild(formEl);
49
76
  this.area.appendChild(this.form);
77
+ this.positionForm();
78
+ this.textarea.focus();
50
79
  this.form.addEventListener("pointerdown", (e) => e.stopPropagation());
51
80
  const area = this.area;
52
81
  const applyRect = (rect) => {
@@ -58,7 +87,10 @@ var AnnotateEdit = class {
58
87
  this.handlers.makeResizable(area, {
59
88
  containment: image.canvas,
60
89
  onResize: applyRect,
61
- onStop: applyRect
90
+ onStop: (rect) => {
91
+ applyRect(rect);
92
+ this.positionForm();
93
+ }
62
94
  });
63
95
  this.handlers.makeDraggable(area, {
64
96
  containment: image.canvas,
@@ -69,9 +101,9 @@ var AnnotateEdit = class {
69
101
  onStop: (pos) => {
70
102
  area.style.left = pos.left + "px";
71
103
  area.style.top = pos.top + "px";
104
+ this.positionForm();
72
105
  }
73
106
  });
74
- this.textarea.focus();
75
107
  this.form.addEventListener("keydown", (e) => {
76
108
  if (e.key === "Escape") {
77
109
  const cancelBtn = this.form.querySelector(".image-annotate-edit-close");
@@ -87,6 +119,13 @@ var AnnotateEdit = class {
87
119
  }
88
120
  this.addCancelButton(buttonRow);
89
121
  }
122
+ /** Recompute the form's horizontal position relative to the area. */
123
+ positionForm() {
124
+ const formRect = this.form.getBoundingClientRect();
125
+ const areaRect = this.area.getBoundingClientRect();
126
+ const left = computeNoteLeft(formRect.width, areaRect.left, areaRect.width, window.innerWidth);
127
+ this.form.style.left = left + "px";
128
+ }
90
129
  /** Tear down the edit form and interaction handlers. */
91
130
  destroy() {
92
131
  this.image.activeEdit = null;
@@ -113,6 +152,7 @@ var AnnotateEdit = class {
113
152
  } else {
114
153
  this.note.editable = true;
115
154
  const view = new AnnotateView(this.image, this.note);
155
+ this.note.view = view;
116
156
  view.resetPosition(this, text);
117
157
  this.image.notes.push(this.note);
118
158
  }
@@ -270,7 +310,13 @@ var AnnotateView = class {
270
310
  }
271
311
  /** Show the tooltip and apply hover styling. */
272
312
  show() {
313
+ this.tooltip.style.visibility = "hidden";
273
314
  this.tooltip.style.display = "block";
315
+ const noteRect = this.tooltip.getBoundingClientRect();
316
+ const areaRect = this.area.getBoundingClientRect();
317
+ const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
318
+ this.tooltip.style.left = left + "px";
319
+ this.tooltip.style.visibility = "";
274
320
  if (!this.editable) {
275
321
  this.area.classList.add("image-annotate-area-hover");
276
322
  } else {
@@ -368,17 +414,19 @@ function destroyDraggable(el) {
368
414
  var MIN_SIZE = 10;
369
415
  var CORNERS = ["nw", "ne", "sw", "se"];
370
416
  function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
371
- let left = startLeft, top = startTop, width = startWidth, height = startHeight;
417
+ let left, top, width, height;
372
418
  if (corner === "nw" || corner === "sw") {
373
419
  left = startLeft + dx;
374
420
  width = startWidth - dx;
375
421
  } else {
422
+ left = startLeft;
376
423
  width = startWidth + dx;
377
424
  }
378
425
  if (corner === "nw" || corner === "ne") {
379
426
  top = startTop + dy;
380
427
  height = startHeight - dy;
381
428
  } else {
429
+ top = startTop;
382
430
  height = startHeight + dy;
383
431
  }
384
432
  if (width < MIN_SIZE) {
@@ -554,6 +602,9 @@ var AnnotateImage = class {
554
602
  this.originalNextSibling = img.nextSibling;
555
603
  this.canvas = document.createElement("div");
556
604
  this.canvas.className = "image-annotate-canvas";
605
+ if (options.theme) {
606
+ this.canvas.dataset.theme = options.theme;
607
+ }
557
608
  this.viewOverlay = document.createElement("div");
558
609
  this.viewOverlay.className = "image-annotate-view";
559
610
  this.editOverlay = document.createElement("div");
@@ -654,6 +705,7 @@ var AnnotateImage = class {
654
705
  for (const note of this.notes) {
655
706
  note.view?.destroy();
656
707
  }
708
+ this.viewOverlay.replaceChildren();
657
709
  }
658
710
  createViews() {
659
711
  for (const note of this.notes) {
@@ -663,6 +715,7 @@ var AnnotateImage = class {
663
715
  /** Rebuild annotation views from the current notes array. */
664
716
  load() {
665
717
  this.destroyViews();
718
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
666
719
  this.createViews();
667
720
  this.notifyLoad();
668
721
  }
@@ -771,6 +824,7 @@ var AnnotateImage = class {
771
824
  this.api.load().then((notes) => {
772
825
  this.destroyViews();
773
826
  this.notes = notes;
827
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
774
828
  this.createViews();
775
829
  this.notifyLoad();
776
830
  }).catch((err) => {
@@ -1 +1 @@
1
- "use strict";(()=>{var F=30,X=30,Y=30,_=30,D=class{constructor(t,n,i){this.busy=!1;this.image=t,this.handlers=t.handlers,n?this.note=n:this.note={id:"new",top:F,left:X,width:Y,height:_,text:"",editable:!0},this.area=t.editOverlay.querySelector(".image-annotate-edit-area");let o=t.toRendered(this.note);this.area.style.height=o.height+"px",this.area.style.width=o.width+"px",this.area.style.left=o.left+"px",this.area.style.top=o.top+"px",this.form=document.createElement("div"),this.form.className="image-annotate-edit-form";let l=document.createElement("form");this.textarea=document.createElement("textarea"),this.textarea.name="text",this.textarea.rows=3,this.textarea.cols=30,this.textarea.value=this.note.text;let d=this.image.options.labels?.placeholder??"";d&&(this.textarea.placeholder=d),l.appendChild(this.textarea),this.form.appendChild(l),this.area.appendChild(this.form),this.form.addEventListener("pointerdown",a=>a.stopPropagation());let s=this.area,c=a=>{s.style.left=a.left+"px",s.style.top=a.top+"px",s.style.width=a.width+"px",s.style.height=a.height+"px"};this.handlers.makeResizable(s,{containment:t.canvas,onResize:c,onStop:c}),this.handlers.makeDraggable(s,{containment:t.canvas,onDrag:a=>{s.style.left=a.left+"px",s.style.top=a.top+"px"},onStop:a=>{s.style.left=a.left+"px",s.style.top=a.top+"px"}}),this.textarea.focus(),this.form.addEventListener("keydown",a=>{if(a.key==="Escape"){let b=this.form.querySelector(".image-annotate-edit-close");b&&b.click()}});let r=document.createElement("div");r.className="image-annotate-edit-buttons",this.form.appendChild(r),this.addSaveButton(r,i),i&&this.addDeleteButton(r,i),this.addCancelButton(r)}destroy(){this.image.activeEdit=null,this.handlers.destroyResizable(this.area),this.handlers.destroyDraggable(this.area),this.area.style.height="",this.area.style.width="",this.area.style.left="",this.area.style.top="",this.form.remove()}addSaveButton(t,n){let i=document.createElement("button");i.className="image-annotate-edit-ok",i.textContent=this.image.options.labels?.save??"OK",i.type="button",i.addEventListener("click",()=>{if(this.busy)return;let o=this.textarea.value,l=()=>{this.image.setMode("view"),n?n.resetPosition(this,o):(this.note.editable=!0,new T(this.image,this.note).resetPosition(this,o),this.image.notes.push(this.note)),this.image.notifySave(C(this.note)),this.destroy(),this.image.flushPendingRescale()},d=O(this.area),s=M(this.area),c=this.image.toNatural({top:d.top,left:d.left,width:s.width,height:s.height});this.note.top=c.top,this.note.left=c.left,this.note.width=c.width,this.note.height=c.height,this.note.text=o,this.image.api.save?(this.busy=!0,this.image.api.save(C(this.note)).then(r=>{r.annotation_id!=null&&(this.note.id=r.annotation_id),l()}).catch(r=>{this.busy=!1;let a=r instanceof Error?r:new Error(String(r));this.image.reportError({type:"save",error:a,note:this.note})})):l()}),t.appendChild(i)}addDeleteButton(t,n){let i=document.createElement("button");i.className="image-annotate-edit-delete",i.textContent=this.image.options.labels?.delete??"Delete",i.type="button",i.addEventListener("click",()=>{if(this.busy)return;let o=()=>{this.image.setMode("view"),this.destroy(),n.destroy();let l=this.image.notes.indexOf(this.note);l!==-1&&this.image.notes.splice(l,1),this.image.notifyDelete(C(this.note)),this.image.flushPendingRescale()};this.image.api.delete?(this.busy=!0,this.image.api.delete(C(this.note)).then(()=>{o()}).catch(l=>{this.busy=!1;let d=l instanceof Error?l:new Error(String(l));this.image.reportError({type:"delete",error:d,note:this.note})})):o()}),t.appendChild(i)}addCancelButton(t){let n=document.createElement("button");n.className="image-annotate-edit-close",n.textContent=this.image.options.labels?.cancel??"Cancel",n.type="button",n.addEventListener("click",()=>{this.image.cancelEdit()}),t.appendChild(n)}};function O(e){return{left:parseInt(e.style.left)||0,top:parseInt(e.style.top)||0}}function M(e){return{width:parseInt(e.style.width)||e.offsetWidth,height:parseInt(e.style.height)||e.offsetHeight}}var T=class{constructor(t,n){this.image=t,this.note=n,this.editable=!!(n.editable&&t.options.editable),this.area=document.createElement("div"),this.area.className="image-annotate-area"+(this.editable?" image-annotate-area-editable":"");let i=document.createElement("div");this.area.appendChild(i),t.viewOverlay.insertBefore(this.area,t.viewOverlay.firstChild),this.tooltip=document.createElement("div"),this.tooltip.className="image-annotate-note",this.tooltip.textContent=n.text,this.tooltip.style.display="none",this.area.appendChild(this.tooltip),this.setPosition(),this.area.addEventListener("mouseenter",()=>this.show()),this.area.addEventListener("mouseleave",()=>this.hide()),this.editable&&(this.area.setAttribute("tabindex","0"),this.area.setAttribute("role","button"),this.area.addEventListener("click",()=>this.edit()),this.area.addEventListener("keydown",o=>{(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),this.edit())}))}setPosition(){let t=this.image.toRendered(this.note),n=this.area.firstElementChild;n.style.height=t.height+"px",n.style.width=t.width+"px",this.area.style.left=t.left+"px",this.area.style.top=t.top+"px"}resetPosition(t,n){this.tooltip.textContent=n,this.tooltip.style.display="none";let i=this.image.toRendered(t.note),o=this.area.firstElementChild;o.style.height=i.height+"px",o.style.width=i.width+"px",this.area.style.left=i.left+"px",this.area.style.top=i.top+"px",this.note.top=t.note.top,this.note.left=t.note.left,this.note.height=t.note.height,this.note.width=t.note.width,this.note.text=n,this.note.id=t.note.id,this.editable=!0}show(){this.tooltip.style.display="block",this.editable?this.area.classList.add("image-annotate-area-editable-hover"):this.area.classList.add("image-annotate-area-hover")}hide(){this.tooltip.style.display="none",this.area.classList.remove("image-annotate-area-hover"),this.area.classList.remove("image-annotate-area-editable-hover")}destroy(){this.area.remove(),this.tooltip.remove()}edit(){this.image.mode==="view"&&(this.image.setMode("edit"),this.image.activeEdit=new D(this.image,this.note,this))}};var I=new WeakMap,P=new WeakMap;function V(e,t){z(e);function n(i){if(i.button!==0)return;i.preventDefault(),e.setPointerCapture&&e.setPointerCapture(i.pointerId);let o=i.clientX,l=i.clientY,d=parseFloat(e.style.left)||0,s=parseFloat(e.style.top)||0,c=parseFloat(e.style.width)||e.offsetWidth,r=parseFloat(e.style.height)||e.offsetHeight,a=-1/0,b=-1/0,N=1/0,E=1/0;if(t.containment){let m=t.containment.getBoundingClientRect(),v=e.getBoundingClientRect(),g=d-(v.left-m.left),h=s-(v.top-m.top);a=g,b=h,N=m.width-c+g,E=m.height-r+h}function w(m,v,g){return Math.max(v,Math.min(g,m))}function L(m){let v=m.clientX-o,g=m.clientY-l,h=w(d+v,a,N),p=w(s+g,b,E);t.onDrag&&t.onDrag({left:h,top:p})}function x(m){e.releasePointerCapture&&e.releasePointerCapture(m.pointerId),e.removeEventListener("pointermove",L),e.removeEventListener("pointerup",x);let v=m.clientX-o,g=m.clientY-l,h=w(d+v,a,N),p=w(s+g,b,E);t.onStop&&t.onStop({left:h,top:p})}e.addEventListener("pointermove",L),e.addEventListener("pointerup",x)}e.addEventListener("pointerdown",n),I.set(e,()=>{e.removeEventListener("pointerdown",n)})}function z(e){let t=I.get(e);t&&(t(),I.delete(e))}var y=10,W=["nw","ne","sw","se"];function S(e,t,n,i,o,l,d){let s=t,c=n,r=i,a=o;return e==="nw"||e==="sw"?(s=t+l,r=i-l):r=i+l,e==="nw"||e==="ne"?(c=n+d,a=o-d):a=o+d,r<y&&((e==="nw"||e==="sw")&&(s=t+i-y),r=y),a<y&&((e==="nw"||e==="ne")&&(c=n+o-y),a=y),{left:s,top:c,width:r,height:a}}function U(e,t){k(e);let n=[];for(let i of W){let o=document.createElement("div");o.className=`image-annotate-resize-handle image-annotate-resize-handle-${i}`,e.appendChild(o),n.push(o),o.addEventListener("pointerdown",function(d){if(d.button!==0)return;d.preventDefault(),d.stopPropagation(),o.setPointerCapture&&o.setPointerCapture(d.pointerId);let s=d.clientX,c=d.clientY,r=parseFloat(e.style.left)||0,a=parseFloat(e.style.top)||0,b=parseFloat(e.style.width)||0,N=parseFloat(e.style.height)||0,E=1/0,w=1/0,L=-1/0,x=-1/0;if(t.containment){let h=t.containment.getBoundingClientRect(),p=e.getBoundingClientRect(),f=r-(p.left-h.left),u=a-(p.top-h.top);L=f,x=u,E=h.width+f,w=h.height+u}function m(h){let{left:p,top:f,width:u,height:A}=h;return p<L&&(u-=L-p,p=L),f<x&&(A-=x-f,f=x),p+u>E&&(u=E-p),f+A>w&&(A=w-f),u<y&&(u=y),A<y&&(A=y),{left:p,top:f,width:u,height:A}}function v(h){let p=h.clientX-s,f=h.clientY-c,u=m(S(i,r,a,b,N,p,f));t.onResize?.(u)}function g(h){o.releasePointerCapture&&o.releasePointerCapture(h.pointerId),o.removeEventListener("pointermove",v),o.removeEventListener("pointerup",g);let p=h.clientX-s,f=h.clientY-c,u=m(S(i,r,a,b,N,p,f));t.onStop&&t.onStop(u)}o.addEventListener("pointermove",v),o.addEventListener("pointerup",g)})}P.set(e,()=>{for(let i of n)i.remove()})}function k(e){let t=P.get(e);t&&(t(),P.delete(e))}function B(){return{makeDraggable:V,makeResizable:U,destroyDraggable:z,destroyResizable:k}}function C(e){let{view:t,editable:n,...i}=e;return i}function j(e){return{load:typeof e.load=="string"?J(e.load):e.load,save:typeof e.save=="string"?K(e.save):e.save,delete:typeof e.delete=="string"?q(e.delete):e.delete}}function J(e){return()=>fetch(e).then(t=>{if(!t.ok)throw new Error(`Load failed (HTTP ${t.status})`);return t.json()})}function K(e){return t=>fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).then(n=>{if(!n.ok)throw new Error(`Save failed (HTTP ${n.status})`);return n.json()})}function q(e){return t=>fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).then(n=>{if(!n.ok)throw new Error(`Delete failed (HTTP ${n.status})`)})}var H=class{constructor(t,n){this._mode="view";this.activeEdit=null;this.destroyed=!1;this.pendingRescale=!1;this.originalParent=null;this.originalNextSibling=null;this.options=n,this.handlers=B(),this.img=t,this.naturalWidth=t.naturalWidth||t.width,this.naturalHeight=t.naturalHeight||t.height;let i=t.getBoundingClientRect(),o=i.width||t.width,l=i.height||t.height;if(this.naturalWidth===0||this.naturalHeight===0)throw new Error("image-annotate: image must have non-zero dimensions (is the image loaded?)");this.scaleX=o/this.naturalWidth,this.scaleY=l/this.naturalHeight,this.notes=n.notes.map(s=>({...s})),this.originalParent=t.parentNode,this.originalNextSibling=t.nextSibling,this.canvas=document.createElement("div"),this.canvas.className="image-annotate-canvas",this.viewOverlay=document.createElement("div"),this.viewOverlay.className="image-annotate-view",this.editOverlay=document.createElement("div"),this.editOverlay.className="image-annotate-edit",this.editOverlay.style.display="none";let d=document.createElement("div");if(d.className="image-annotate-edit-area",this.editOverlay.appendChild(d),!t.parentNode)throw new Error("image-annotate: image must be in the DOM before initialization");t.parentNode.insertBefore(this.canvas,t),this.canvas.appendChild(t),this.canvas.appendChild(this.viewOverlay),this.canvas.appendChild(this.editOverlay),this.api=this.options.api?j(this.options.api):{},this.api.load?this.loadFromApi():this.load(),this.options.editable&&this.createButton(),n.autoResize!==!1&&typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(s=>{let c=s[0];if(!c)return;let{width:r,height:a}=c.contentRect;r===0||a===0||this.rescale(r,a)}),this.resizeObserver.observe(this.canvas))}toRendered(t){return{top:t.top*this.scaleY,left:t.left*this.scaleX,width:t.width*this.scaleX,height:t.height*this.scaleY}}toNatural(t){let n={top:t.top/this.scaleY,left:t.left/this.scaleX,width:t.width/this.scaleX,height:t.height/this.scaleY};if(!isFinite(n.top)||!isFinite(n.left)||!isFinite(n.width)||!isFinite(n.height))throw new Error("image-annotate: scale conversion produced non-finite coordinates");return n}get mode(){return this._mode}setMode(t){this._mode=t,t==="edit"?(this.canvas.classList.add("image-annotate-editing"),this.editOverlay.style.display="block"):(this.canvas.classList.remove("image-annotate-editing"),this.editOverlay.style.display="none")}getNotes(){return this.notes.map(C)}notifyChange(){this.options.onChange?.(this.getNotes())}notifySave(t){this.options.onSave?.(t),this.notifyChange()}notifyDelete(t){this.options.onDelete?.(t),this.notifyChange()}notifyLoad(){this.options.onLoad?.(this.getNotes()),this.notifyChange()}destroyViews(){this.cancelEdit();for(let t of this.notes)t.view?.destroy()}createViews(){for(let t of this.notes)t.view=new T(this,t)}load(){this.destroyViews(),this.createViews(),this.notifyLoad()}clear(){this.destroyViews(),this.notes=[],this.notifyChange()}destroy(){if(!this.destroyed){if(this.destroyed=!0,this.destroyViews(),this.notes=[],this.button&&this.button.remove(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=void 0),this.originalParent&&this.originalParent.isConnected){let t=this.originalNextSibling?.parentNode===this.originalParent?this.originalNextSibling:null;this.originalParent.insertBefore(this.img,t)}this.canvas.remove()}}cancelEdit(){this.activeEdit&&(this.activeEdit.destroy(),this.setMode("view")),this.flushPendingRescale()}rescale(t,n){if(this.mode==="edit"){this.pendingRescale=!0;return}this.applyRescale(t,n)}applyRescale(t,n){let i=t/this.naturalWidth,o=n/this.naturalHeight;i===this.scaleX&&o===this.scaleY||(this.scaleX=i,this.scaleY=o,this.destroyViews(),this.createViews())}flushPendingRescale(){if(!this.pendingRescale)return;this.pendingRescale=!1;let t=this.canvas.getBoundingClientRect();t.width>0&&t.height>0&&this.applyRescale(t.width,t.height)}setNotes(t){this.destroyed||(this.destroyViews(),this.notes=t.map(n=>({...n})),this.createViews())}setEditable(t){this.destroyed||this.options.editable!==t&&(this.options.editable=t,t&&!this.button?this.createButton():!t&&this.button&&(this.button.remove(),this.button=void 0),this.destroyViews(),this.createViews())}createButton(){this.button=document.createElement("button"),this.button.className="image-annotate-add",this.button.title=this.options.labels?.addNote??"Add Note",this.button.type="button",this.button.addEventListener("click",()=>{this.add()}),this.canvas.appendChild(this.button)}reportError(t){this.options.onError?this.options.onError(t):console.error(`image-annotate: ${t.type} failed`,t.error)}loadFromApi(){this.api.load&&this.api.load().then(t=>{this.destroyViews(),this.notes=t,this.createViews(),this.notifyLoad()}).catch(t=>{let n=t instanceof Error?t:new Error(String(t));this.reportError({type:"load",error:n})})}add(){return this.mode==="view"?(this.setMode("edit"),this.activeEdit=new D(this),!0):!1}};var R={addNote:"Add Note",save:"OK",delete:"Delete",cancel:"Cancel",placeholder:""};var Q={editable:!0,notes:[],labels:{...R}};$.fn.annotateImage=function(e){if(e==="destroy"){let l=this.data("annotateImage");return l&&(l.destroy(),this.removeData("annotateImage")),this}let t=e,n={...Q,...t,labels:{...R,...t?.labels}},i=this[0],o=new H(i,n);return this.data("annotateImage",o),this};})();
1
+ "use strict";(()=>{function X(e,t,i){let n=Math.min(e.width,t),o=Math.min(e.height,i),h=Math.max(0,Math.min(e.left,t-n));return{top:Math.max(0,Math.min(e.top,i-o)),left:h,width:n,height:o}}function I(e,t,i){for(let n of e)Object.assign(n,X(n,t,i))}function T(e,t,i,n){let o=(i-e)/2,l=t+o+e;l>n&&(o-=l-n);let s=t+o;return s<0&&(o-=s),o}var Y=30,_=30,W=30,U=30,R=class{constructor(t,i,n){this.busy=!1;this.image=t,this.handlers=t.handlers,i?this.note=i:this.note={id:"new",top:Y,left:_,width:W,height:U,text:"",editable:!0},this.area=t.editOverlay.querySelector(".image-annotate-edit-area");let o=t.toRendered(this.note);this.area.style.height=o.height+"px",this.area.style.width=o.width+"px",this.area.style.left=o.left+"px",this.area.style.top=o.top+"px",this.form=document.createElement("div"),this.form.className="image-annotate-edit-form";let h=document.createElement("form");this.textarea=document.createElement("textarea"),this.textarea.name="text",this.textarea.rows=3,this.textarea.cols=30,this.textarea.value=this.note.text;let l=this.image.options.labels?.placeholder??"";l&&(this.textarea.placeholder=l),h.appendChild(this.textarea),this.form.appendChild(h),this.area.appendChild(this.form),this.positionForm(),this.textarea.focus(),this.form.addEventListener("pointerdown",a=>a.stopPropagation());let s=this.area,d=a=>{s.style.left=a.left+"px",s.style.top=a.top+"px",s.style.width=a.width+"px",s.style.height=a.height+"px"};this.handlers.makeResizable(s,{containment:t.canvas,onResize:d,onStop:a=>{d(a),this.positionForm()}}),this.handlers.makeDraggable(s,{containment:t.canvas,onDrag:a=>{s.style.left=a.left+"px",s.style.top=a.top+"px"},onStop:a=>{s.style.left=a.left+"px",s.style.top=a.top+"px",this.positionForm()}}),this.form.addEventListener("keydown",a=>{if(a.key==="Escape"){let y=this.form.querySelector(".image-annotate-edit-close");y&&y.click()}});let r=document.createElement("div");r.className="image-annotate-edit-buttons",this.form.appendChild(r),this.addSaveButton(r,n),n&&this.addDeleteButton(r,n),this.addCancelButton(r)}positionForm(){let t=this.form.getBoundingClientRect(),i=this.area.getBoundingClientRect(),n=T(t.width,i.left,i.width,window.innerWidth);this.form.style.left=n+"px"}destroy(){this.image.activeEdit=null,this.handlers.destroyResizable(this.area),this.handlers.destroyDraggable(this.area),this.area.style.height="",this.area.style.width="",this.area.style.left="",this.area.style.top="",this.form.remove()}addSaveButton(t,i){let n=document.createElement("button");n.className="image-annotate-edit-ok",n.textContent=this.image.options.labels?.save??"OK",n.type="button",n.addEventListener("click",()=>{if(this.busy)return;let o=this.textarea.value,h=()=>{if(this.image.setMode("view"),i)i.resetPosition(this,o);else{this.note.editable=!0;let r=new D(this.image,this.note);this.note.view=r,r.resetPosition(this,o),this.image.notes.push(this.note)}this.image.notifySave(C(this.note)),this.destroy(),this.image.flushPendingRescale()},l=S(this.area),s=z(this.area),d=this.image.toNatural({top:l.top,left:l.left,width:s.width,height:s.height});this.note.top=d.top,this.note.left=d.left,this.note.width=d.width,this.note.height=d.height,this.note.text=o,this.image.api.save?(this.busy=!0,this.image.api.save(C(this.note)).then(r=>{r.annotation_id!=null&&(this.note.id=r.annotation_id),h()}).catch(r=>{this.busy=!1;let a=r instanceof Error?r:new Error(String(r));this.image.reportError({type:"save",error:a,note:this.note})})):h()}),t.appendChild(n)}addDeleteButton(t,i){let n=document.createElement("button");n.className="image-annotate-edit-delete",n.textContent=this.image.options.labels?.delete??"Delete",n.type="button",n.addEventListener("click",()=>{if(this.busy)return;let o=()=>{this.image.setMode("view"),this.destroy(),i.destroy();let h=this.image.notes.indexOf(this.note);h!==-1&&this.image.notes.splice(h,1),this.image.notifyDelete(C(this.note)),this.image.flushPendingRescale()};this.image.api.delete?(this.busy=!0,this.image.api.delete(C(this.note)).then(()=>{o()}).catch(h=>{this.busy=!1;let l=h instanceof Error?h:new Error(String(h));this.image.reportError({type:"delete",error:l,note:this.note})})):o()}),t.appendChild(n)}addCancelButton(t){let i=document.createElement("button");i.className="image-annotate-edit-close",i.textContent=this.image.options.labels?.cancel??"Cancel",i.type="button",i.addEventListener("click",()=>{this.image.cancelEdit()}),t.appendChild(i)}};function S(e){return{left:parseInt(e.style.left)||0,top:parseInt(e.style.top)||0}}function z(e){return{width:parseInt(e.style.width)||e.offsetWidth,height:parseInt(e.style.height)||e.offsetHeight}}var D=class{constructor(t,i){this.image=t,this.note=i,this.editable=!!(i.editable&&t.options.editable),this.area=document.createElement("div"),this.area.className="image-annotate-area"+(this.editable?" image-annotate-area-editable":"");let n=document.createElement("div");this.area.appendChild(n),t.viewOverlay.insertBefore(this.area,t.viewOverlay.firstChild),this.tooltip=document.createElement("div"),this.tooltip.className="image-annotate-note",this.tooltip.textContent=i.text,this.tooltip.style.display="none",this.area.appendChild(this.tooltip),this.setPosition(),this.area.addEventListener("mouseenter",()=>this.show()),this.area.addEventListener("mouseleave",()=>this.hide()),this.editable&&(this.area.setAttribute("tabindex","0"),this.area.setAttribute("role","button"),this.area.addEventListener("click",()=>this.edit()),this.area.addEventListener("keydown",o=>{(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),this.edit())}))}setPosition(){let t=this.image.toRendered(this.note),i=this.area.firstElementChild;i.style.height=t.height+"px",i.style.width=t.width+"px",this.area.style.left=t.left+"px",this.area.style.top=t.top+"px"}resetPosition(t,i){this.tooltip.textContent=i,this.tooltip.style.display="none";let n=this.image.toRendered(t.note),o=this.area.firstElementChild;o.style.height=n.height+"px",o.style.width=n.width+"px",this.area.style.left=n.left+"px",this.area.style.top=n.top+"px",this.note.top=t.note.top,this.note.left=t.note.left,this.note.height=t.note.height,this.note.width=t.note.width,this.note.text=i,this.note.id=t.note.id,this.editable=!0}show(){this.tooltip.style.visibility="hidden",this.tooltip.style.display="block";let t=this.tooltip.getBoundingClientRect(),i=this.area.getBoundingClientRect(),n=T(t.width,i.left,i.width,window.innerWidth);this.tooltip.style.left=n+"px",this.tooltip.style.visibility="",this.editable?this.area.classList.add("image-annotate-area-editable-hover"):this.area.classList.add("image-annotate-area-hover")}hide(){this.tooltip.style.display="none",this.area.classList.remove("image-annotate-area-hover"),this.area.classList.remove("image-annotate-area-editable-hover")}destroy(){this.area.remove(),this.tooltip.remove()}edit(){this.image.mode==="view"&&(this.image.setMode("edit"),this.image.activeEdit=new R(this.image,this.note,this))}};var P=new WeakMap,M=new WeakMap;function j(e,t){B(e);function i(n){if(n.button!==0)return;n.preventDefault(),e.setPointerCapture&&e.setPointerCapture(n.pointerId);let o=n.clientX,h=n.clientY,l=parseFloat(e.style.left)||0,s=parseFloat(e.style.top)||0,d=parseFloat(e.style.width)||e.offsetWidth,r=parseFloat(e.style.height)||e.offsetHeight,a=-1/0,y=-1/0,N=1/0,E=1/0;if(t.containment){let p=t.containment.getBoundingClientRect(),v=e.getBoundingClientRect(),g=l-(v.left-p.left),c=s-(v.top-p.top);a=g,y=c,N=p.width-d+g,E=p.height-r+c}function w(p,v,g){return Math.max(v,Math.min(g,p))}function L(p){let v=p.clientX-o,g=p.clientY-h,c=w(l+v,a,N),m=w(s+g,y,E);t.onDrag&&t.onDrag({left:c,top:m})}function x(p){e.releasePointerCapture&&e.releasePointerCapture(p.pointerId),e.removeEventListener("pointermove",L),e.removeEventListener("pointerup",x);let v=p.clientX-o,g=p.clientY-h,c=w(l+v,a,N),m=w(s+g,y,E);t.onStop&&t.onStop({left:c,top:m})}e.addEventListener("pointermove",L),e.addEventListener("pointerup",x)}e.addEventListener("pointerdown",i),P.set(e,()=>{e.removeEventListener("pointerdown",i)})}function B(e){let t=P.get(e);t&&(t(),P.delete(e))}var b=10,q=["nw","ne","sw","se"];function k(e,t,i,n,o,h,l){let s,d,r,a;return e==="nw"||e==="sw"?(s=t+h,r=n-h):(s=t,r=n+h),e==="nw"||e==="ne"?(d=i+l,a=o-l):(d=i,a=o+l),r<b&&((e==="nw"||e==="sw")&&(s=t+n-b),r=b),a<b&&((e==="nw"||e==="ne")&&(d=i+o-b),a=b),{left:s,top:d,width:r,height:a}}function K(e,t){F(e);let i=[];for(let n of q){let o=document.createElement("div");o.className=`image-annotate-resize-handle image-annotate-resize-handle-${n}`,e.appendChild(o),i.push(o),o.addEventListener("pointerdown",function(l){if(l.button!==0)return;l.preventDefault(),l.stopPropagation(),o.setPointerCapture&&o.setPointerCapture(l.pointerId);let s=l.clientX,d=l.clientY,r=parseFloat(e.style.left)||0,a=parseFloat(e.style.top)||0,y=parseFloat(e.style.width)||0,N=parseFloat(e.style.height)||0,E=1/0,w=1/0,L=-1/0,x=-1/0;if(t.containment){let c=t.containment.getBoundingClientRect(),m=e.getBoundingClientRect(),u=r-(m.left-c.left),f=a-(m.top-c.top);L=u,x=f,E=c.width+u,w=c.height+f}function p(c){let{left:m,top:u,width:f,height:A}=c;return m<L&&(f-=L-m,m=L),u<x&&(A-=x-u,u=x),m+f>E&&(f=E-m),u+A>w&&(A=w-u),f<b&&(f=b),A<b&&(A=b),{left:m,top:u,width:f,height:A}}function v(c){let m=c.clientX-s,u=c.clientY-d,f=p(k(n,r,a,y,N,m,u));t.onResize?.(f)}function g(c){o.releasePointerCapture&&o.releasePointerCapture(c.pointerId),o.removeEventListener("pointermove",v),o.removeEventListener("pointerup",g);let m=c.clientX-s,u=c.clientY-d,f=p(k(n,r,a,y,N,m,u));t.onStop&&t.onStop(f)}o.addEventListener("pointermove",v),o.addEventListener("pointerup",g)})}M.set(e,()=>{for(let n of i)n.remove()})}function F(e){let t=M.get(e);t&&(t(),M.delete(e))}function V(){return{makeDraggable:j,makeResizable:K,destroyDraggable:B,destroyResizable:F}}function C(e){let{view:t,editable:i,...n}=e;return n}function J(e){return{load:typeof e.load=="string"?G(e.load):e.load,save:typeof e.save=="string"?Q(e.save):e.save,delete:typeof e.delete=="string"?Z(e.delete):e.delete}}function G(e){return()=>fetch(e).then(t=>{if(!t.ok)throw new Error(`Load failed (HTTP ${t.status})`);return t.json()})}function Q(e){return t=>fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).then(i=>{if(!i.ok)throw new Error(`Save failed (HTTP ${i.status})`);return i.json()})}function Z(e){return t=>fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).then(i=>{if(!i.ok)throw new Error(`Delete failed (HTTP ${i.status})`)})}var H=class{constructor(t,i){this._mode="view";this.activeEdit=null;this.destroyed=!1;this.pendingRescale=!1;this.originalParent=null;this.originalNextSibling=null;this.options=i,this.handlers=V(),this.img=t,this.naturalWidth=t.naturalWidth||t.width,this.naturalHeight=t.naturalHeight||t.height;let n=t.getBoundingClientRect(),o=n.width||t.width,h=n.height||t.height;if(this.naturalWidth===0||this.naturalHeight===0)throw new Error("image-annotate: image must have non-zero dimensions (is the image loaded?)");this.scaleX=o/this.naturalWidth,this.scaleY=h/this.naturalHeight,this.notes=i.notes.map(s=>({...s})),this.originalParent=t.parentNode,this.originalNextSibling=t.nextSibling,this.canvas=document.createElement("div"),this.canvas.className="image-annotate-canvas",i.theme&&(this.canvas.dataset.theme=i.theme),this.viewOverlay=document.createElement("div"),this.viewOverlay.className="image-annotate-view",this.editOverlay=document.createElement("div"),this.editOverlay.className="image-annotate-edit",this.editOverlay.style.display="none";let l=document.createElement("div");if(l.className="image-annotate-edit-area",this.editOverlay.appendChild(l),!t.parentNode)throw new Error("image-annotate: image must be in the DOM before initialization");t.parentNode.insertBefore(this.canvas,t),this.canvas.appendChild(t),this.canvas.appendChild(this.viewOverlay),this.canvas.appendChild(this.editOverlay),this.api=this.options.api?J(this.options.api):{},this.api.load?this.loadFromApi():this.load(),this.options.editable&&this.createButton(),i.autoResize!==!1&&typeof ResizeObserver<"u"&&(this.resizeObserver=new ResizeObserver(s=>{let d=s[0];if(!d)return;let{width:r,height:a}=d.contentRect;r===0||a===0||this.rescale(r,a)}),this.resizeObserver.observe(this.canvas))}toRendered(t){return{top:t.top*this.scaleY,left:t.left*this.scaleX,width:t.width*this.scaleX,height:t.height*this.scaleY}}toNatural(t){let i={top:t.top/this.scaleY,left:t.left/this.scaleX,width:t.width/this.scaleX,height:t.height/this.scaleY};if(!isFinite(i.top)||!isFinite(i.left)||!isFinite(i.width)||!isFinite(i.height))throw new Error("image-annotate: scale conversion produced non-finite coordinates");return i}get mode(){return this._mode}setMode(t){this._mode=t,t==="edit"?(this.canvas.classList.add("image-annotate-editing"),this.editOverlay.style.display="block"):(this.canvas.classList.remove("image-annotate-editing"),this.editOverlay.style.display="none")}getNotes(){return this.notes.map(C)}notifyChange(){this.options.onChange?.(this.getNotes())}notifySave(t){this.options.onSave?.(t),this.notifyChange()}notifyDelete(t){this.options.onDelete?.(t),this.notifyChange()}notifyLoad(){this.options.onLoad?.(this.getNotes()),this.notifyChange()}destroyViews(){this.cancelEdit();for(let t of this.notes)t.view?.destroy();this.viewOverlay.replaceChildren()}createViews(){for(let t of this.notes)t.view=new D(this,t)}load(){this.destroyViews(),I(this.notes,this.naturalWidth,this.naturalHeight),this.createViews(),this.notifyLoad()}clear(){this.destroyViews(),this.notes=[],this.notifyChange()}destroy(){if(!this.destroyed){if(this.destroyed=!0,this.destroyViews(),this.notes=[],this.button&&this.button.remove(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=void 0),this.originalParent&&this.originalParent.isConnected){let t=this.originalNextSibling?.parentNode===this.originalParent?this.originalNextSibling:null;this.originalParent.insertBefore(this.img,t)}this.canvas.remove()}}cancelEdit(){this.activeEdit&&(this.activeEdit.destroy(),this.setMode("view")),this.flushPendingRescale()}rescale(t,i){if(this.mode==="edit"){this.pendingRescale=!0;return}this.applyRescale(t,i)}applyRescale(t,i){let n=t/this.naturalWidth,o=i/this.naturalHeight;n===this.scaleX&&o===this.scaleY||(this.scaleX=n,this.scaleY=o,this.destroyViews(),this.createViews())}flushPendingRescale(){if(!this.pendingRescale)return;this.pendingRescale=!1;let t=this.canvas.getBoundingClientRect();t.width>0&&t.height>0&&this.applyRescale(t.width,t.height)}setNotes(t){this.destroyed||(this.destroyViews(),this.notes=t.map(i=>({...i})),this.createViews())}setEditable(t){this.destroyed||this.options.editable!==t&&(this.options.editable=t,t&&!this.button?this.createButton():!t&&this.button&&(this.button.remove(),this.button=void 0),this.destroyViews(),this.createViews())}createButton(){this.button=document.createElement("button"),this.button.className="image-annotate-add",this.button.title=this.options.labels?.addNote??"Add Note",this.button.type="button",this.button.addEventListener("click",()=>{this.add()}),this.canvas.appendChild(this.button)}reportError(t){this.options.onError?this.options.onError(t):console.error(`image-annotate: ${t.type} failed`,t.error)}loadFromApi(){this.api.load&&this.api.load().then(t=>{this.destroyViews(),this.notes=t,I(this.notes,this.naturalWidth,this.naturalHeight),this.createViews(),this.notifyLoad()}).catch(t=>{let i=t instanceof Error?t:new Error(String(t));this.reportError({type:"load",error:i})})}add(){return this.mode==="view"?(this.setMode("edit"),this.activeEdit=new R(this),!0):!1}};var O={addNote:"Add Note",save:"OK",delete:"Delete",cancel:"Cancel",placeholder:""};var tt={editable:!0,notes:[],labels:{...O}};$.fn.annotateImage=function(e){if(e==="destroy"){let h=this.data("annotateImage");return h&&(h.destroy(),this.removeData("annotateImage")),this}let t=e,i={...tt,...t,labels:{...O,...t?.labels}},n=this[0],o=new H(n,i);return this.data("annotateImage",o),this};})();
package/dist/react.js CHANGED
@@ -7,6 +7,33 @@ import {
7
7
  useRef
8
8
  } from "react";
9
9
 
10
+ // src/positioning.ts
11
+ function clampNote(note, naturalWidth, naturalHeight) {
12
+ const width = Math.min(note.width, naturalWidth);
13
+ const height = Math.min(note.height, naturalHeight);
14
+ const left = Math.max(0, Math.min(note.left, naturalWidth - width));
15
+ const top = Math.max(0, Math.min(note.top, naturalHeight - height));
16
+ return { top, left, width, height };
17
+ }
18
+ function clampNotes(notes, naturalWidth, naturalHeight) {
19
+ for (const note of notes) {
20
+ Object.assign(note, clampNote(note, naturalWidth, naturalHeight));
21
+ }
22
+ }
23
+ function computeNoteLeft(noteWidth, areaLeftInViewport, areaWidth, viewportWidth) {
24
+ let left = (areaWidth - noteWidth) / 2;
25
+ const noteLeftInViewport = areaLeftInViewport + left;
26
+ const noteRightInViewport = noteLeftInViewport + noteWidth;
27
+ if (noteRightInViewport > viewportWidth) {
28
+ left -= noteRightInViewport - viewportWidth;
29
+ }
30
+ const adjustedNoteLeft = areaLeftInViewport + left;
31
+ if (adjustedNoteLeft < 0) {
32
+ left -= adjustedNoteLeft;
33
+ }
34
+ return left;
35
+ }
36
+
10
37
  // src/annotate-edit.ts
11
38
  var DEFAULT_NOTE_TOP = 30;
12
39
  var DEFAULT_NOTE_LEFT = 30;
@@ -56,6 +83,8 @@ var AnnotateEdit = class {
56
83
  formEl.appendChild(this.textarea);
57
84
  this.form.appendChild(formEl);
58
85
  this.area.appendChild(this.form);
86
+ this.positionForm();
87
+ this.textarea.focus();
59
88
  this.form.addEventListener("pointerdown", (e) => e.stopPropagation());
60
89
  const area = this.area;
61
90
  const applyRect = (rect) => {
@@ -67,7 +96,10 @@ var AnnotateEdit = class {
67
96
  this.handlers.makeResizable(area, {
68
97
  containment: image.canvas,
69
98
  onResize: applyRect,
70
- onStop: applyRect
99
+ onStop: (rect) => {
100
+ applyRect(rect);
101
+ this.positionForm();
102
+ }
71
103
  });
72
104
  this.handlers.makeDraggable(area, {
73
105
  containment: image.canvas,
@@ -78,9 +110,9 @@ var AnnotateEdit = class {
78
110
  onStop: (pos) => {
79
111
  area.style.left = pos.left + "px";
80
112
  area.style.top = pos.top + "px";
113
+ this.positionForm();
81
114
  }
82
115
  });
83
- this.textarea.focus();
84
116
  this.form.addEventListener("keydown", (e) => {
85
117
  if (e.key === "Escape") {
86
118
  const cancelBtn = this.form.querySelector(".image-annotate-edit-close");
@@ -96,6 +128,13 @@ var AnnotateEdit = class {
96
128
  }
97
129
  this.addCancelButton(buttonRow);
98
130
  }
131
+ /** Recompute the form's horizontal position relative to the area. */
132
+ positionForm() {
133
+ const formRect = this.form.getBoundingClientRect();
134
+ const areaRect = this.area.getBoundingClientRect();
135
+ const left = computeNoteLeft(formRect.width, areaRect.left, areaRect.width, window.innerWidth);
136
+ this.form.style.left = left + "px";
137
+ }
99
138
  /** Tear down the edit form and interaction handlers. */
100
139
  destroy() {
101
140
  this.image.activeEdit = null;
@@ -122,6 +161,7 @@ var AnnotateEdit = class {
122
161
  } else {
123
162
  this.note.editable = true;
124
163
  const view = new AnnotateView(this.image, this.note);
164
+ this.note.view = view;
125
165
  view.resetPosition(this, text);
126
166
  this.image.notes.push(this.note);
127
167
  }
@@ -279,7 +319,13 @@ var AnnotateView = class {
279
319
  }
280
320
  /** Show the tooltip and apply hover styling. */
281
321
  show() {
322
+ this.tooltip.style.visibility = "hidden";
282
323
  this.tooltip.style.display = "block";
324
+ const noteRect = this.tooltip.getBoundingClientRect();
325
+ const areaRect = this.area.getBoundingClientRect();
326
+ const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
327
+ this.tooltip.style.left = left + "px";
328
+ this.tooltip.style.visibility = "";
283
329
  if (!this.editable) {
284
330
  this.area.classList.add("image-annotate-area-hover");
285
331
  } else {
@@ -377,17 +423,19 @@ function destroyDraggable(el) {
377
423
  var MIN_SIZE = 10;
378
424
  var CORNERS = ["nw", "ne", "sw", "se"];
379
425
  function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
380
- let left = startLeft, top = startTop, width = startWidth, height = startHeight;
426
+ let left, top, width, height;
381
427
  if (corner === "nw" || corner === "sw") {
382
428
  left = startLeft + dx;
383
429
  width = startWidth - dx;
384
430
  } else {
431
+ left = startLeft;
385
432
  width = startWidth + dx;
386
433
  }
387
434
  if (corner === "nw" || corner === "ne") {
388
435
  top = startTop + dy;
389
436
  height = startHeight - dy;
390
437
  } else {
438
+ top = startTop;
391
439
  height = startHeight + dy;
392
440
  }
393
441
  if (width < MIN_SIZE) {
@@ -563,6 +611,9 @@ var AnnotateImage = class {
563
611
  this.originalNextSibling = img.nextSibling;
564
612
  this.canvas = document.createElement("div");
565
613
  this.canvas.className = "image-annotate-canvas";
614
+ if (options.theme) {
615
+ this.canvas.dataset.theme = options.theme;
616
+ }
566
617
  this.viewOverlay = document.createElement("div");
567
618
  this.viewOverlay.className = "image-annotate-view";
568
619
  this.editOverlay = document.createElement("div");
@@ -663,6 +714,7 @@ var AnnotateImage = class {
663
714
  for (const note of this.notes) {
664
715
  note.view?.destroy();
665
716
  }
717
+ this.viewOverlay.replaceChildren();
666
718
  }
667
719
  createViews() {
668
720
  for (const note of this.notes) {
@@ -672,6 +724,7 @@ var AnnotateImage = class {
672
724
  /** Rebuild annotation views from the current notes array. */
673
725
  load() {
674
726
  this.destroyViews();
727
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
675
728
  this.createViews();
676
729
  this.notifyLoad();
677
730
  }
@@ -780,6 +833,7 @@ var AnnotateImage = class {
780
833
  this.api.load().then((notes) => {
781
834
  this.destroyViews();
782
835
  this.notes = notes;
836
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
783
837
  this.createViews();
784
838
  this.notifyLoad();
785
839
  }).catch((err) => {
@@ -21,6 +21,8 @@ export declare class AnnotateEdit {
21
21
  * @param existingView - The view being edited (for updates); omit for new annotations.
22
22
  */
23
23
  constructor(image: AnnotateImage, note?: AnnotationNote, existingView?: AnnotateView);
24
+ /** Recompute the form's horizontal position relative to the area. */
25
+ private positionForm;
24
26
  /** Tear down the edit form and interaction handlers. */
25
27
  destroy(): void;
26
28
  private addSaveButton;
@@ -0,0 +1,31 @@
1
+ /** Clamp a note's size and position to fit within the given natural image dimensions. */
2
+ export declare function clampNote(note: {
3
+ top: number;
4
+ left: number;
5
+ width: number;
6
+ height: number;
7
+ }, naturalWidth: number, naturalHeight: number): {
8
+ top: number;
9
+ left: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ /** Clamp all notes in the array in place, constraining geometry to image bounds. */
14
+ export declare function clampNotes(notes: {
15
+ top: number;
16
+ left: number;
17
+ width: number;
18
+ height: number;
19
+ }[], naturalWidth: number, naturalHeight: number): void;
20
+ /**
21
+ * Compute the horizontal `left` value for a note/form element
22
+ * relative to its parent annotation area, centering it under the area
23
+ * and clamping to viewport edges.
24
+ *
25
+ * @param noteWidth - Measured width of the note/form element
26
+ * @param areaLeftInViewport - The area's left edge in viewport coordinates
27
+ * @param areaWidth - Width of the annotation area
28
+ * @param viewportWidth - Browser viewport width (window.innerWidth)
29
+ * @returns CSS `left` value in pixels, relative to the area element
30
+ */
31
+ export declare function computeNoteLeft(noteWidth: number, areaLeftInViewport: number, areaWidth: number, viewportWidth: number): number;
@@ -86,6 +86,8 @@ export interface AnnotateImageOptions {
86
86
  labels?: Labels;
87
87
  /** Attach a ResizeObserver to rescale annotations when the image resizes. Default: true. */
88
88
  autoResize?: boolean;
89
+ /** CSS theme name. When set, adds `data-theme="<value>"` to the canvas element for CSS variable scoping. */
90
+ theme?: string;
89
91
  }
90
92
  export interface DragCallbacks {
91
93
  containment?: HTMLElement;
package/dist/vue.js CHANGED
@@ -1,6 +1,33 @@
1
1
  // src/vue.ts
2
2
  import { defineComponent, ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, h } from "vue";
3
3
 
4
+ // src/positioning.ts
5
+ function clampNote(note, naturalWidth, naturalHeight) {
6
+ const width = Math.min(note.width, naturalWidth);
7
+ const height = Math.min(note.height, naturalHeight);
8
+ const left = Math.max(0, Math.min(note.left, naturalWidth - width));
9
+ const top = Math.max(0, Math.min(note.top, naturalHeight - height));
10
+ return { top, left, width, height };
11
+ }
12
+ function clampNotes(notes, naturalWidth, naturalHeight) {
13
+ for (const note of notes) {
14
+ Object.assign(note, clampNote(note, naturalWidth, naturalHeight));
15
+ }
16
+ }
17
+ function computeNoteLeft(noteWidth, areaLeftInViewport, areaWidth, viewportWidth) {
18
+ let left = (areaWidth - noteWidth) / 2;
19
+ const noteLeftInViewport = areaLeftInViewport + left;
20
+ const noteRightInViewport = noteLeftInViewport + noteWidth;
21
+ if (noteRightInViewport > viewportWidth) {
22
+ left -= noteRightInViewport - viewportWidth;
23
+ }
24
+ const adjustedNoteLeft = areaLeftInViewport + left;
25
+ if (adjustedNoteLeft < 0) {
26
+ left -= adjustedNoteLeft;
27
+ }
28
+ return left;
29
+ }
30
+
4
31
  // src/annotate-edit.ts
5
32
  var DEFAULT_NOTE_TOP = 30;
6
33
  var DEFAULT_NOTE_LEFT = 30;
@@ -50,6 +77,8 @@ var AnnotateEdit = class {
50
77
  formEl.appendChild(this.textarea);
51
78
  this.form.appendChild(formEl);
52
79
  this.area.appendChild(this.form);
80
+ this.positionForm();
81
+ this.textarea.focus();
53
82
  this.form.addEventListener("pointerdown", (e) => e.stopPropagation());
54
83
  const area = this.area;
55
84
  const applyRect = (rect) => {
@@ -61,7 +90,10 @@ var AnnotateEdit = class {
61
90
  this.handlers.makeResizable(area, {
62
91
  containment: image.canvas,
63
92
  onResize: applyRect,
64
- onStop: applyRect
93
+ onStop: (rect) => {
94
+ applyRect(rect);
95
+ this.positionForm();
96
+ }
65
97
  });
66
98
  this.handlers.makeDraggable(area, {
67
99
  containment: image.canvas,
@@ -72,9 +104,9 @@ var AnnotateEdit = class {
72
104
  onStop: (pos) => {
73
105
  area.style.left = pos.left + "px";
74
106
  area.style.top = pos.top + "px";
107
+ this.positionForm();
75
108
  }
76
109
  });
77
- this.textarea.focus();
78
110
  this.form.addEventListener("keydown", (e) => {
79
111
  if (e.key === "Escape") {
80
112
  const cancelBtn = this.form.querySelector(".image-annotate-edit-close");
@@ -90,6 +122,13 @@ var AnnotateEdit = class {
90
122
  }
91
123
  this.addCancelButton(buttonRow);
92
124
  }
125
+ /** Recompute the form's horizontal position relative to the area. */
126
+ positionForm() {
127
+ const formRect = this.form.getBoundingClientRect();
128
+ const areaRect = this.area.getBoundingClientRect();
129
+ const left = computeNoteLeft(formRect.width, areaRect.left, areaRect.width, window.innerWidth);
130
+ this.form.style.left = left + "px";
131
+ }
93
132
  /** Tear down the edit form and interaction handlers. */
94
133
  destroy() {
95
134
  this.image.activeEdit = null;
@@ -116,6 +155,7 @@ var AnnotateEdit = class {
116
155
  } else {
117
156
  this.note.editable = true;
118
157
  const view = new AnnotateView(this.image, this.note);
158
+ this.note.view = view;
119
159
  view.resetPosition(this, text);
120
160
  this.image.notes.push(this.note);
121
161
  }
@@ -273,7 +313,13 @@ var AnnotateView = class {
273
313
  }
274
314
  /** Show the tooltip and apply hover styling. */
275
315
  show() {
316
+ this.tooltip.style.visibility = "hidden";
276
317
  this.tooltip.style.display = "block";
318
+ const noteRect = this.tooltip.getBoundingClientRect();
319
+ const areaRect = this.area.getBoundingClientRect();
320
+ const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
321
+ this.tooltip.style.left = left + "px";
322
+ this.tooltip.style.visibility = "";
277
323
  if (!this.editable) {
278
324
  this.area.classList.add("image-annotate-area-hover");
279
325
  } else {
@@ -371,17 +417,19 @@ function destroyDraggable(el) {
371
417
  var MIN_SIZE = 10;
372
418
  var CORNERS = ["nw", "ne", "sw", "se"];
373
419
  function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
374
- let left = startLeft, top = startTop, width = startWidth, height = startHeight;
420
+ let left, top, width, height;
375
421
  if (corner === "nw" || corner === "sw") {
376
422
  left = startLeft + dx;
377
423
  width = startWidth - dx;
378
424
  } else {
425
+ left = startLeft;
379
426
  width = startWidth + dx;
380
427
  }
381
428
  if (corner === "nw" || corner === "ne") {
382
429
  top = startTop + dy;
383
430
  height = startHeight - dy;
384
431
  } else {
432
+ top = startTop;
385
433
  height = startHeight + dy;
386
434
  }
387
435
  if (width < MIN_SIZE) {
@@ -557,6 +605,9 @@ var AnnotateImage = class {
557
605
  this.originalNextSibling = img.nextSibling;
558
606
  this.canvas = document.createElement("div");
559
607
  this.canvas.className = "image-annotate-canvas";
608
+ if (options.theme) {
609
+ this.canvas.dataset.theme = options.theme;
610
+ }
560
611
  this.viewOverlay = document.createElement("div");
561
612
  this.viewOverlay.className = "image-annotate-view";
562
613
  this.editOverlay = document.createElement("div");
@@ -657,6 +708,7 @@ var AnnotateImage = class {
657
708
  for (const note of this.notes) {
658
709
  note.view?.destroy();
659
710
  }
711
+ this.viewOverlay.replaceChildren();
660
712
  }
661
713
  createViews() {
662
714
  for (const note of this.notes) {
@@ -666,6 +718,7 @@ var AnnotateImage = class {
666
718
  /** Rebuild annotation views from the current notes array. */
667
719
  load() {
668
720
  this.destroyViews();
721
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
669
722
  this.createViews();
670
723
  this.notifyLoad();
671
724
  }
@@ -774,6 +827,7 @@ var AnnotateImage = class {
774
827
  this.api.load().then((notes) => {
775
828
  this.destroyViews();
776
829
  this.notes = notes;
830
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
777
831
  this.createViews();
778
832
  this.notifyLoad();
779
833
  }).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "annotate-image",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.4",
4
4
  "description": "Create Flickr-like comment annotations on images — draw rectangles, add notes, save via AJAX or static data",
5
5
  "license": "GPL-2.0",
6
6
  "repository": {
@@ -64,35 +64,35 @@
64
64
  "demo:watch:react": "esbuild src/react.tsx --bundle --format=esm --external:react --external:react-dom --outfile=dist/react.js --watch",
65
65
  "demo:watch:vue": "esbuild src/vue.ts --bundle --format=esm --external:vue --outfile=dist/vue.js --watch",
66
66
  "demo:watch:css": "esbuild src/annotation.css --minify --outfile=dist/css/annotate.min.css --watch",
67
- "demo:serve": "live-server --port=8080 --open=demo/index.html --watch=dist,demo",
67
+ "demo:serve": "browser-sync start --server --port 8080 --startPath demo/index.html --files 'dist/**/*' 'demo/**/*'",
68
68
  "demo:ci": "npm run build && concurrently -k \"npm:demo:watch:*\" \"npm:demo:serve:ci\"",
69
- "demo:serve:ci": "live-server --port=8080 --no-browser --watch=dist,demo"
69
+ "demo:serve:ci": "browser-sync start --server --port 8080 --no-open --files 'dist/**/*' 'demo/**/*'"
70
70
  },
71
71
  "devDependencies": {
72
- "@eslint/js": "^9.39.2",
72
+ "@eslint/js": "^9.39.4",
73
73
  "@playwright/test": "^1.58.2",
74
74
  "@testing-library/dom": "^10.4.1",
75
75
  "@testing-library/react": "^16.3.2",
76
- "@types/jquery": "^3.5.33",
76
+ "@types/jquery": "^3.5.34",
77
77
  "@types/react": "^18.3.28",
78
78
  "@types/react-dom": "^18.3.7",
79
79
  "@vue/test-utils": "^2.4.6",
80
- "concurrently": "^9.2.1",
81
- "esbuild": "^0.27.3",
82
- "eslint": "^9.39.2",
80
+ "browser-sync": "^3.0.4",
81
+ "concurrently": "^10.0.3",
82
+ "esbuild": "^0.28.1",
83
+ "eslint": "^9.39.4",
83
84
  "eslint-config-prettier": "^10.1.8",
84
85
  "eslint-plugin-react-hooks": "^7.0.1",
85
86
  "eslint-plugin-vue": "^10.8.0",
86
87
  "jquery": "^3.7.1",
87
- "jsdom": "^28.0.0",
88
- "live-server": "^1.2.2",
88
+ "jsdom": "^28.1.0",
89
89
  "prettier": "^3.8.1",
90
90
  "react": "^18.3.1",
91
91
  "react-dom": "^18.3.1",
92
92
  "typescript": "^5.9.3",
93
- "typescript-eslint": "^8.55.0",
94
- "vitest": "^4.0.18",
95
- "vue": "^3.5.28"
93
+ "typescript-eslint": "^8.57.0",
94
+ "vitest": "^4.1.0",
95
+ "vue": "^3.5.30"
96
96
  },
97
97
  "peerDependencies": {
98
98
  "jquery": ">=3.7.0",
package/readme.md CHANGED
@@ -1,9 +1,23 @@
1
1
  # Annotate Image
2
2
 
3
+ <p align="center">
4
+ <a href="https://pullpatchpush.com/annotate-image"><img src="assets/image-annotate-preview.jpg" alt="Annotate Image preview" width="387"></a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/annotate-image"><img src="https://img.shields.io/npm/v/annotate-image" alt="npm version"></a>
9
+ <a href="https://www.npmjs.com/package/annotate-image"><img src="https://img.shields.io/npm/dm/annotate-image" alt="npm downloads"></a>
10
+ <a href="https://bundlephobia.com/package/annotate-image"><img src="https://img.shields.io/bundlephobia/minzip/annotate-image" alt="bundle size"></a>
11
+ <img src="https://img.shields.io/badge/TypeScript-strict-blue" alt="TypeScript strict">
12
+ <a href="https://www.npmjs.com/package/annotate-image"><img src="https://img.shields.io/npm/l/annotate-image" alt="license"></a>
13
+ </p>
14
+
3
15
  A JavaScript image annotation plugin that creates Flickr-like comment annotations on images. Users can draw rectangular regions on images, add text notes, and persist annotations via callbacks or AJAX.
4
16
 
5
17
  Works standalone (vanilla JS), or with jQuery, React, or Vue. Framework adapters are tree-shakeable — only the one you import gets bundled.
6
18
 
19
+ **[Documentation & Live Demo](https://pullpatchpush.com/annotate-image)**
20
+
7
21
  ## Installation
8
22
 
9
23
  ```sh
@@ -172,6 +186,7 @@ Creates an annotation layer on an image element. Returns an `AnnotateImage` inst
172
186
  | `onLoad` | `(notes: NoteData[]) => void` | — | Called after notes are loaded |
173
187
  | `onError` | `(ctx: AnnotateErrorContext) => void` | — | Called on API errors (defaults to `console.error`) |
174
188
  | `autoResize` | `boolean` | `true` | Re-scale annotations when the container resizes |
189
+ | `theme` | `string` | — | CSS theme name; sets `data-theme` on the canvas for variable scoping |
175
190
 
176
191
  #### `AnnotateApi`
177
192
 
@@ -295,6 +310,75 @@ annotate(document.getElementById('myImage'), {
295
310
  });
296
311
  ```
297
312
 
313
+ ## Theming
314
+
315
+ The plugin uses CSS custom properties for all visual styling. Override these variables in your CSS to create custom themes.
316
+
317
+ ### Using the `theme` option
318
+
319
+ Set `theme` in options to add a `data-theme` attribute to the canvas element, enabling CSS scoping:
320
+
321
+ ```js
322
+ annotate(document.getElementById('myImage'), {
323
+ theme: 'dark',
324
+ notes: [/* ... */],
325
+ });
326
+ ```
327
+
328
+ ```css
329
+ .image-annotate-canvas[data-theme="dark"] {
330
+ --image-annotate-area-border: rgba(255, 255, 255, 0.7);
331
+ --image-annotate-area-bg: rgba(100, 149, 237, 0.15);
332
+ --image-annotate-note-bg: #2a2a3e;
333
+ --image-annotate-note-text: #e0e0f0;
334
+ --image-annotate-note-radius: 4px;
335
+ --image-annotate-note-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
336
+ /* ... see demo for full example */
337
+ }
338
+ ```
339
+
340
+ Multiple instances on the same page can use different themes.
341
+
342
+ Button icons (save, delete, cancel) use CSS `mask-image`, so their color automatically follows `--image-annotate-button-text`. Dark themes just need to set the text color — no icon overrides required.
343
+
344
+ ### Available CSS variables
345
+
346
+ Set on `.image-annotate-canvas`:
347
+
348
+ | Variable | Default | Description |
349
+ |---|---|---|
350
+ | `--image-annotate-canvas-border` | `none` | Border around the canvas |
351
+ | `--image-annotate-font-family` | `Verdana, sans-serif` | Font for notes and buttons |
352
+ | `--image-annotate-font-size` | `12px` | Font size for notes and buttons |
353
+ | `--image-annotate-area-border` | `#000` | Annotation rectangle border color |
354
+ | `--image-annotate-area-inner-border` | `#fff` | Annotation inner border color |
355
+ | `--image-annotate-area-bg` | `transparent` | Annotation rectangle fill |
356
+ | `--image-annotate-area-border-width` | `1px` | Annotation border thickness |
357
+ | `--image-annotate-area-border-style` | `solid` | Annotation border style |
358
+ | `--image-annotate-area-radius` | `0` | Annotation border-radius |
359
+ | `--image-annotate-hover-color` | `yellow` | Hover border color (read-only) |
360
+ | `--image-annotate-hover-editable-color` | `#00ad00` | Hover border color (editable) |
361
+ | `--image-annotate-hover-bg` | `transparent` | Hover background fill |
362
+ | `--image-annotate-note-bg` | `#e7ffe7` | Tooltip background |
363
+ | `--image-annotate-note-border` | `#397f39` | Tooltip border |
364
+ | `--image-annotate-note-text` | `#000` | Tooltip text color |
365
+ | `--image-annotate-note-radius` | `0` | Tooltip border-radius |
366
+ | `--image-annotate-note-shadow` | `none` | Tooltip box-shadow |
367
+ | `--image-annotate-note-max-width` | `300px` | Tooltip max width |
368
+ | `--image-annotate-edit-bg` | `#fffee3` | Edit form background |
369
+ | `--image-annotate-edit-border` | `#000` | Edit form border |
370
+ | `--image-annotate-edit-radius` | `0` | Edit form border-radius |
371
+ | `--image-annotate-edit-shadow` | `none` | Edit form box-shadow |
372
+ | `--image-annotate-edit-max-width` | `300px` | Edit form max width |
373
+ | `--image-annotate-button-bg` | `#fff` | Button background |
374
+ | `--image-annotate-button-bg-hover` | `#eee` | Button hover background |
375
+ | `--image-annotate-button-border` | `#ccc` | Button border |
376
+ | `--image-annotate-button-text` | `#000` | Button text and icon color |
377
+ | `--image-annotate-add-bg` | `rgba(0,0,0,0.4)` | Add Note button background |
378
+ | `--image-annotate-add-bg-hover` | `rgba(0,0,0,0.6)` | Add Note button hover background |
379
+ | `--image-annotate-add-border` | `1px solid rgba(255,255,255,0.5)` | Add Note button border |
380
+ | `--image-annotate-add-radius` | `4px` | Add Note button border-radius |
381
+
298
382
  ## Tree Shaking
299
383
 
300
384
  Each entry point (`annotate-image`, `annotate-image/jquery`, `annotate-image/react`, `annotate-image/vue`) is a separate bundle. Importing one does not pull in the others. Unused framework adapters are excluded automatically by bundlers that support package `exports`.
@@ -326,13 +410,13 @@ The plugin supports keyboard navigation:
326
410
 
327
411
  ## Demos
328
412
 
329
- Run the demo server locally:
413
+ Try the [live examples](https://pullpatchpush.com/annotate-image/examples) online, or run the demo server locally:
330
414
 
331
415
  ```sh
332
416
  npm run demo
333
417
  ```
334
418
 
335
- This opens a browser at `http://localhost:8080/demo/index.html` with links to demos including static annotations, AJAX loading, vanilla JS, React, Vue, and multiple instances.
419
+ This opens a browser at `http://localhost:8080/demo/index.html` with links to demos including static annotations, AJAX loading, vanilla JS, React, Vue, multiple instances, and CSS theming.
336
420
 
337
421
  ## Build
338
422