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

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;
@@ -270,7 +309,13 @@ var AnnotateView = class {
270
309
  }
271
310
  /** Show the tooltip and apply hover styling. */
272
311
  show() {
312
+ this.tooltip.style.visibility = "hidden";
273
313
  this.tooltip.style.display = "block";
314
+ const noteRect = this.tooltip.getBoundingClientRect();
315
+ const areaRect = this.area.getBoundingClientRect();
316
+ const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
317
+ this.tooltip.style.left = left + "px";
318
+ this.tooltip.style.visibility = "";
274
319
  if (!this.editable) {
275
320
  this.area.classList.add("image-annotate-area-hover");
276
321
  } else {
@@ -368,17 +413,19 @@ function destroyDraggable(el) {
368
413
  var MIN_SIZE = 10;
369
414
  var CORNERS = ["nw", "ne", "sw", "se"];
370
415
  function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
371
- let left = startLeft, top = startTop, width = startWidth, height = startHeight;
416
+ let left, top, width, height;
372
417
  if (corner === "nw" || corner === "sw") {
373
418
  left = startLeft + dx;
374
419
  width = startWidth - dx;
375
420
  } else {
421
+ left = startLeft;
376
422
  width = startWidth + dx;
377
423
  }
378
424
  if (corner === "nw" || corner === "ne") {
379
425
  top = startTop + dy;
380
426
  height = startHeight - dy;
381
427
  } else {
428
+ top = startTop;
382
429
  height = startHeight + dy;
383
430
  }
384
431
  if (width < MIN_SIZE) {
@@ -554,6 +601,9 @@ var AnnotateImage = class {
554
601
  this.originalNextSibling = img.nextSibling;
555
602
  this.canvas = document.createElement("div");
556
603
  this.canvas.className = "image-annotate-canvas";
604
+ if (options.theme) {
605
+ this.canvas.dataset.theme = options.theme;
606
+ }
557
607
  this.viewOverlay = document.createElement("div");
558
608
  this.viewOverlay.className = "image-annotate-view";
559
609
  this.editOverlay = document.createElement("div");
@@ -663,6 +713,7 @@ var AnnotateImage = class {
663
713
  /** Rebuild annotation views from the current notes array. */
664
714
  load() {
665
715
  this.destroyViews();
716
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
666
717
  this.createViews();
667
718
  this.notifyLoad();
668
719
  }
@@ -771,6 +822,7 @@ var AnnotateImage = class {
771
822
  this.api.load().then((notes) => {
772
823
  this.destroyViews();
773
824
  this.notes = notes;
825
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
774
826
  this.createViews();
775
827
  this.notifyLoad();
776
828
  }).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,r=t+o+e;r>i&&(o-=r-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 r=this.image.options.labels?.placeholder??"";r&&(this.textarea.placeholder=r),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 l=document.createElement("div");l.className="image-annotate-edit-buttons",this.form.appendChild(l),this.addSaveButton(l,i),i&&this.addDeleteButton(l,i),this.addCancelButton(l)}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=()=>{this.image.setMode("view"),n?n.resetPosition(this,o):(this.note.editable=!0,new D(this.image,this.note).resetPosition(this,o),this.image.notes.push(this.note)),this.image.notifySave(C(this.note)),this.destroy(),this.image.flushPendingRescale()},r=z(this.area),s=k(this.area),d=this.image.toNatural({top:r.top,left:r.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(l=>{l.annotation_id!=null&&(this.note.id=l.annotation_id),h()}).catch(l=>{this.busy=!1;let a=l instanceof Error?l:new Error(String(l));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 r=h instanceof Error?h:new Error(String(h));this.image.reportError({type:"delete",error:r,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,r=parseFloat(e.style.left)||0,s=parseFloat(e.style.top)||0,d=parseFloat(e.style.width)||e.offsetWidth,l=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=r-(v.left-p.left),c=s-(v.top-p.top);a=g,y=c,N=p.width-d+g,E=p.height-l+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(r+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(r+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,r){let s,d,l,a;return e==="nw"||e==="sw"?(s=t+h,l=i-h):(s=t,l=i+h),e==="nw"||e==="ne"?(d=n+r,a=o-r):(d=n,a=o+r),l<b&&((e==="nw"||e==="sw")&&(s=t+i-b),l=b),a<b&&((e==="nw"||e==="ne")&&(d=n+o-b),a=b),{left:s,top:d,width:l,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(r){if(r.button!==0)return;r.preventDefault(),r.stopPropagation(),o.setPointerCapture&&o.setPointerCapture(r.pointerId);let s=r.clientX,d=r.clientY,l=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=l-(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,l,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,l,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 r=document.createElement("div");if(r.className="image-annotate-edit-area",this.editOverlay.appendChild(r),!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:l,height:a}=d.contentRect;l===0||a===0||this.rescale(l,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 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;
@@ -270,7 +309,13 @@ var AnnotateView = class {
270
309
  }
271
310
  /** Show the tooltip and apply hover styling. */
272
311
  show() {
312
+ this.tooltip.style.visibility = "hidden";
273
313
  this.tooltip.style.display = "block";
314
+ const noteRect = this.tooltip.getBoundingClientRect();
315
+ const areaRect = this.area.getBoundingClientRect();
316
+ const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
317
+ this.tooltip.style.left = left + "px";
318
+ this.tooltip.style.visibility = "";
274
319
  if (!this.editable) {
275
320
  this.area.classList.add("image-annotate-area-hover");
276
321
  } else {
@@ -368,17 +413,19 @@ function destroyDraggable(el) {
368
413
  var MIN_SIZE = 10;
369
414
  var CORNERS = ["nw", "ne", "sw", "se"];
370
415
  function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
371
- let left = startLeft, top = startTop, width = startWidth, height = startHeight;
416
+ let left, top, width, height;
372
417
  if (corner === "nw" || corner === "sw") {
373
418
  left = startLeft + dx;
374
419
  width = startWidth - dx;
375
420
  } else {
421
+ left = startLeft;
376
422
  width = startWidth + dx;
377
423
  }
378
424
  if (corner === "nw" || corner === "ne") {
379
425
  top = startTop + dy;
380
426
  height = startHeight - dy;
381
427
  } else {
428
+ top = startTop;
382
429
  height = startHeight + dy;
383
430
  }
384
431
  if (width < MIN_SIZE) {
@@ -554,6 +601,9 @@ var AnnotateImage = class {
554
601
  this.originalNextSibling = img.nextSibling;
555
602
  this.canvas = document.createElement("div");
556
603
  this.canvas.className = "image-annotate-canvas";
604
+ if (options.theme) {
605
+ this.canvas.dataset.theme = options.theme;
606
+ }
557
607
  this.viewOverlay = document.createElement("div");
558
608
  this.viewOverlay.className = "image-annotate-view";
559
609
  this.editOverlay = document.createElement("div");
@@ -663,6 +713,7 @@ var AnnotateImage = class {
663
713
  /** Rebuild annotation views from the current notes array. */
664
714
  load() {
665
715
  this.destroyViews();
716
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
666
717
  this.createViews();
667
718
  this.notifyLoad();
668
719
  }
@@ -771,6 +822,7 @@ var AnnotateImage = class {
771
822
  this.api.load().then((notes) => {
772
823
  this.destroyViews();
773
824
  this.notes = notes;
825
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
774
826
  this.createViews();
775
827
  this.notifyLoad();
776
828
  }).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),l=Math.max(0,Math.min(e.left,t-n));return{top:Math.max(0,Math.min(e.top,i-o)),left:l,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,r=t+o+e;r>n&&(o-=r-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 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 r=this.image.options.labels?.placeholder??"";r&&(this.textarea.placeholder=r),l.appendChild(this.textarea),this.form.appendChild(l),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 h=document.createElement("div");h.className="image-annotate-edit-buttons",this.form.appendChild(h),this.addSaveButton(h,n),n&&this.addDeleteButton(h,n),this.addCancelButton(h)}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,l=()=>{this.image.setMode("view"),i?i.resetPosition(this,o):(this.note.editable=!0,new D(this.image,this.note).resetPosition(this,o),this.image.notes.push(this.note)),this.image.notifySave(C(this.note)),this.destroy(),this.image.flushPendingRescale()},r=S(this.area),s=z(this.area),d=this.image.toNatural({top:r.top,left:r.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(h=>{h.annotation_id!=null&&(this.note.id=h.annotation_id),l()}).catch(h=>{this.busy=!1;let a=h instanceof Error?h:new Error(String(h));this.image.reportError({type:"save",error:a,note:this.note})})):l()}),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 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 r=l instanceof Error?l:new Error(String(l));this.image.reportError({type:"delete",error:r,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,l=n.clientY,r=parseFloat(e.style.left)||0,s=parseFloat(e.style.top)||0,d=parseFloat(e.style.width)||e.offsetWidth,h=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=r-(v.left-p.left),c=s-(v.top-p.top);a=g,y=c,N=p.width-d+g,E=p.height-h+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-l,c=w(r+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-l,c=w(r+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,l,r){let s,d,h,a;return e==="nw"||e==="sw"?(s=t+l,h=n-l):(s=t,h=n+l),e==="nw"||e==="ne"?(d=i+r,a=o-r):(d=i,a=o+r),h<b&&((e==="nw"||e==="sw")&&(s=t+n-b),h=b),a<b&&((e==="nw"||e==="ne")&&(d=i+o-b),a=b),{left:s,top:d,width:h,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(r){if(r.button!==0)return;r.preventDefault(),r.stopPropagation(),o.setPointerCapture&&o.setPointerCapture(r.pointerId);let s=r.clientX,d=r.clientY,h=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=h-(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,h,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,h,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,l=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=l/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 r=document.createElement("div");if(r.className="image-annotate-edit-area",this.editOverlay.appendChild(r),!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:h,height:a}=d.contentRect;h===0||a===0||this.rescale(h,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()}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 l=this.data("annotateImage");return l&&(l.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;
@@ -279,7 +318,13 @@ var AnnotateView = class {
279
318
  }
280
319
  /** Show the tooltip and apply hover styling. */
281
320
  show() {
321
+ this.tooltip.style.visibility = "hidden";
282
322
  this.tooltip.style.display = "block";
323
+ const noteRect = this.tooltip.getBoundingClientRect();
324
+ const areaRect = this.area.getBoundingClientRect();
325
+ const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
326
+ this.tooltip.style.left = left + "px";
327
+ this.tooltip.style.visibility = "";
283
328
  if (!this.editable) {
284
329
  this.area.classList.add("image-annotate-area-hover");
285
330
  } else {
@@ -377,17 +422,19 @@ function destroyDraggable(el) {
377
422
  var MIN_SIZE = 10;
378
423
  var CORNERS = ["nw", "ne", "sw", "se"];
379
424
  function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
380
- let left = startLeft, top = startTop, width = startWidth, height = startHeight;
425
+ let left, top, width, height;
381
426
  if (corner === "nw" || corner === "sw") {
382
427
  left = startLeft + dx;
383
428
  width = startWidth - dx;
384
429
  } else {
430
+ left = startLeft;
385
431
  width = startWidth + dx;
386
432
  }
387
433
  if (corner === "nw" || corner === "ne") {
388
434
  top = startTop + dy;
389
435
  height = startHeight - dy;
390
436
  } else {
437
+ top = startTop;
391
438
  height = startHeight + dy;
392
439
  }
393
440
  if (width < MIN_SIZE) {
@@ -563,6 +610,9 @@ var AnnotateImage = class {
563
610
  this.originalNextSibling = img.nextSibling;
564
611
  this.canvas = document.createElement("div");
565
612
  this.canvas.className = "image-annotate-canvas";
613
+ if (options.theme) {
614
+ this.canvas.dataset.theme = options.theme;
615
+ }
566
616
  this.viewOverlay = document.createElement("div");
567
617
  this.viewOverlay.className = "image-annotate-view";
568
618
  this.editOverlay = document.createElement("div");
@@ -672,6 +722,7 @@ var AnnotateImage = class {
672
722
  /** Rebuild annotation views from the current notes array. */
673
723
  load() {
674
724
  this.destroyViews();
725
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
675
726
  this.createViews();
676
727
  this.notifyLoad();
677
728
  }
@@ -780,6 +831,7 @@ var AnnotateImage = class {
780
831
  this.api.load().then((notes) => {
781
832
  this.destroyViews();
782
833
  this.notes = notes;
834
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
783
835
  this.createViews();
784
836
  this.notifyLoad();
785
837
  }).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;
@@ -273,7 +312,13 @@ var AnnotateView = class {
273
312
  }
274
313
  /** Show the tooltip and apply hover styling. */
275
314
  show() {
315
+ this.tooltip.style.visibility = "hidden";
276
316
  this.tooltip.style.display = "block";
317
+ const noteRect = this.tooltip.getBoundingClientRect();
318
+ const areaRect = this.area.getBoundingClientRect();
319
+ const left = computeNoteLeft(noteRect.width, areaRect.left, areaRect.width, window.innerWidth);
320
+ this.tooltip.style.left = left + "px";
321
+ this.tooltip.style.visibility = "";
277
322
  if (!this.editable) {
278
323
  this.area.classList.add("image-annotate-area-hover");
279
324
  } else {
@@ -371,17 +416,19 @@ function destroyDraggable(el) {
371
416
  var MIN_SIZE = 10;
372
417
  var CORNERS = ["nw", "ne", "sw", "se"];
373
418
  function computeResize(corner, startLeft, startTop, startWidth, startHeight, dx, dy) {
374
- let left = startLeft, top = startTop, width = startWidth, height = startHeight;
419
+ let left, top, width, height;
375
420
  if (corner === "nw" || corner === "sw") {
376
421
  left = startLeft + dx;
377
422
  width = startWidth - dx;
378
423
  } else {
424
+ left = startLeft;
379
425
  width = startWidth + dx;
380
426
  }
381
427
  if (corner === "nw" || corner === "ne") {
382
428
  top = startTop + dy;
383
429
  height = startHeight - dy;
384
430
  } else {
431
+ top = startTop;
385
432
  height = startHeight + dy;
386
433
  }
387
434
  if (width < MIN_SIZE) {
@@ -557,6 +604,9 @@ var AnnotateImage = class {
557
604
  this.originalNextSibling = img.nextSibling;
558
605
  this.canvas = document.createElement("div");
559
606
  this.canvas.className = "image-annotate-canvas";
607
+ if (options.theme) {
608
+ this.canvas.dataset.theme = options.theme;
609
+ }
560
610
  this.viewOverlay = document.createElement("div");
561
611
  this.viewOverlay.className = "image-annotate-view";
562
612
  this.editOverlay = document.createElement("div");
@@ -666,6 +716,7 @@ var AnnotateImage = class {
666
716
  /** Rebuild annotation views from the current notes array. */
667
717
  load() {
668
718
  this.destroyViews();
719
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
669
720
  this.createViews();
670
721
  this.notifyLoad();
671
722
  }
@@ -774,6 +825,7 @@ var AnnotateImage = class {
774
825
  this.api.load().then((notes) => {
775
826
  this.destroyViews();
776
827
  this.notes = notes;
828
+ clampNotes(this.notes, this.naturalWidth, this.naturalHeight);
777
829
  this.createViews();
778
830
  this.notifyLoad();
779
831
  }).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.3",
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
+ "browser-sync": "^3.0.4",
80
81
  "concurrently": "^9.2.1",
81
- "esbuild": "^0.27.3",
82
- "eslint": "^9.39.2",
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