@veltdev/tiptap-velt-comments
Version:
Tiptap Extension to add Google Docs-style overlay comments to your Tiptap editor. Works with the Velt Collaboration SDK.
3 lines (2 loc) • 15.9 kB
JavaScript
import{Mark as t,mergeAttributes as e}from"@tiptap/core";const n={EDITOR_ID:"data-editor-id",LOCATION_ID:"data-velt-location-id",ANNOTATION_ID:"annotation-id",MULTI_THREAD_ANNOTATION_ID:"multi-thread-annotation-id"},o="tiptapVeltComments",r="tiptapVeltComments",a="__default__",i="velt-comment-text",c="VeltComments",s="terminal",d="TiptapVeltComments: ",l="debugMode",u="forceDebugMode";class g{static log=g.showLogs()?console.log:()=>{};static warn=g.showLogs()?console.warn:()=>{};static error=g.showLogs()?console.error:()=>{};static debug=g.showLogs()?console.debug:()=>{};static info=g.showLogs()?console.info:()=>{};static logsEnabled=!0;static catch=(t,e)=>{try{g.logsEnabled&&(void 0!==e?console.warn(d,t,e):console.warn(d,t))}catch(t){}};static showLogs(){try{return!!sessionStorage.getItem(l)||!!sessionStorage.getItem(u)}catch(t){return g.catch("Error in showLogs: ",t),!1}}}const h=(t,e=r)=>{try{const n=t.storage?.[e];return n||null}catch(t){return g.catch("[getExtensionStorage] Error accessing storage:",t),null}},f=(t,e=r)=>{const n=h(t,e);return n?.persistVeltMarks??!0},m=(t,e=r)=>{const n=h(t,e);return n?.editorId},I=(t,e,n=0,o)=>{const r=[];if(!e||!t||e.length>t.length)return r;const a=(t=>{if(!t||0===t.length)return[];const e=new Array(t.length).fill(0);let n=0,o=1;for(;o<t.length;)t[o]===t[n]?(n++,e[o]=n,o++):0!==n?n=e[n-1]:(e[o]=0,o++);return e})(e);let i=0,c=0;for(;i<t.length;)if(e[c]===t[i]&&(i++,c++),c===e.length){if(r.push(n+i-c),c=a[c-1],o&&r.length>=o)break}else i<t.length&&e[c]!==t[i]&&(0!==c?c=a[c-1]:i++);return r},T=(t,e,n)=>{const o=[];for(const r of e){let e=0,a=-1,i=0,c=-1,s=0;for(let o=0;o<t.length;o++){const d=t[o].text.length;if(-1===a&&r<e+d&&(a=o,i=r-e),-1===c&&r+n<=e+d){c=o,s=r+n-e;break}e+=d}if(-1!==a&&-1!==c){const e=t[a].pos+i,n=t[c].pos+s;o.push({from:e,to:n})}}return o},p=(t,e,n,o,r)=>{try{if(!e.trim())return 1;let a=[];a=n&&n.id?x(t,e,n.id):A(t,e);for(let t=0;t<a.length;t++)if(a[t].from===o&&a[t].to===r)return t+1;return 1}catch(t){return g.catch("Error finding occurrence index:",t),1}},N=(t,e,n,o)=>n?x(t,e,n,o):A(t,e,o),x=(t,e,n,o)=>{try{const r=[],a=document.getElementById(n);if(!a)return r;const i=t.view;if(a!==i.dom&&!i.dom.contains(a))return r;const c=i.posAtDOM(a,0),s=t.state.doc.content.size;if(null===c||"number"!=typeof c||!Number.isFinite(c)||c<0||c>s)return r;const d=t.state.doc.resolve(c).pos,l=a.textContent?.length||0,u=Math.max(0,Math.min(d,s)),g=Math.min(d+l,s),h=[];t.state.doc.nodesBetween(u,g,(t,e,n,o)=>{const r=t;r.isText&&h.push({text:r.text||"",pos:e})});const f=h.map(t=>t.text).join(""),m=I(f,e,0,o);return T(h,m,e.length).map(t=>({from:t.from,to:t.to}))}catch(t){return g.catch("Error finding text in DOM element:",t),[]}},A=(t,e,n)=>{try{const{doc:o}=t.state,r=[];o.descendants((t,e,n,o)=>{const a=t;a.isText&&r.push({text:a.text||"",pos:e})});const a=r.map(t=>t.text).join(""),i=I(a,e,0,n);return T(r,i,e.length).map(t=>({from:t.from,to:t.to}))}catch(t){return g.catch("Error finding text in document:",t),[]}},w=t=>{try{if(!t)return null;const e=m(t,r);if(e)return e;const o=t.view.dom;if(o){const t=o.closest(`[${n.EDITOR_ID}]`)?.getAttribute(n.EDITOR_ID);return t??null}return null}catch(t){return g.catch("Error finding editor ID:",t),null}},E=(t,e)=>{try{let n=t;return n||(n=w(e)||a),{editorId:n}}catch(e){return g.warn("[ensureEditorSetup] Error setting up editor:",e),{editorId:t||a}}},O=t=>{try{const{from:e}=t.state.selection,n=t.view.domAtPos(e).node,o=t.view.dom,r=100;let a=0,i=n;for(;i&&i!==document.body&&a<r;){if(!o.contains(i))return null;if(i.id)return i;i=i.parentElement,a++}return null}catch(t){return g.catch("Error finding parent node with ID:",t),null}},y=(t,e)=>{const{annotationId:n,currentText:o,targetTextNodeId:r}=e;let a=!1,i=r,c=[];const s=D(t,{annotationId:n,currentText:o,targetTextNodeId:r,nodes:e.nodes});return s?(a=s.changed,i=s.newId,c=s.searchResults):c=r?x(t,o,r):A(t,o),{targetTextNodeIdChanged:a,newTargetTextNodeId:i,searchResults:c}},D=(t,e)=>{const{annotationId:r,currentText:a,targetTextNodeId:i}=e;if(!i)return null;let c=null;if(t.state.doc.descendants((t,e)=>{const a=t.marks?.find?.(t=>t.type?.name===o&&t.attrs?.[n.ANNOTATION_ID]===r);a&&null===c&&(c=e)}),null===c)return null;let s=t.view.domAtPos(c).node;const d=t.view.dom;for(;s&&!s.id&&s!==document.body&&(s=s.parentElement||s,d.contains(s)););if(s?.id&&s.id!==i){const e=x(t,a,s.id);return{changed:!0,newId:s.id,searchResults:e}}return null},M=(t,e)=>{if(0===e.searchResults.length)return e.originalOccurrence;const n=e.nodes[0],o=e.nodes[e.nodes.length-1],r=n.pos,a=o.pos+(o.node.nodeSize||0);for(let t=0;t<e.searchResults.length;t++){const n=e.searchResults[t];if(n.from<=r&&n.to>=r||n.from<=a&&n.to>=a||r<=n.from&&a>=n.to){return t+1}}return e.originalOccurrence},_=(t,e,n,o,r)=>{try{if(!t||!e)return!1;if(o>=r)return g.warn("Invalid position range: from must be less than to"),!1;return t.chain().setVeltComment(e,n,o,r).run(),!0}catch(t){return g.catch("Error applying annotation mark:",t),!1}},C=(t,e,r)=>{try{if(!t||!e)return!1;const a=t.schema.marks[o];if(!a)return g.warn("Velt comment mark type not found"),!1;let i=!1;return t.chain().command(({tr:o})=>{const c=r?.from??0,s=r?.to??t.state.doc.content.size;return t.state.doc.descendants((t,r)=>{if(r<c||r>s)return;t.marks.find(t=>t.type===a&&t.attrs[n.ANNOTATION_ID]===e)&&(o.removeMark(r,r+t.nodeSize,a),i=!0)}),i}).run(),i}catch(t){return g.catch("Error removing annotation mark:",t),!1}};let v=!1,b=new Map;const $=new Map,L=()=>{try{const t=window.Velt;return t?.getCommentElement?t.getCommentElement():null}catch(t){return g.catch("Error getting Velt comment element:",t),null}},S=(t,e)=>{try{const n=L();if(!n)return void g.warn("Velt SDK not available, cannot update annotation context");n.updateContext(t,e)}catch(t){g.catch("Error updating annotation context:",t)}},k=(t,e)=>{try{const n=e||`subscriber-${Date.now()}-${Math.random()}`;if(!v){const t=L();t&&"function"==typeof t.getSelectedComments&&(t.getSelectedComments().subscribe(t=>{const e=new Map;if(t?.forEach(t=>{t?.annotationId&&e.set(t.annotationId,!0)}),!((t,e)=>{if(t.size!==e.size)return!1;for(const[n,o]of Array.from(t))if(e.get(n)!==o)return!1;return!0})(e,b)){b=e;const t=new Set;for(const[e]of Array.from(b))t.add(e);$.forEach(e=>{if("function"==typeof e)try{e(t)}catch(t){g.catch("Error in selected annotations subscriber:",t)}})}}),v=!0)}return $.set(n,t),()=>{$.delete(n)}}catch(t){return g.catch("Error subscribing to selected annotations:",t),()=>{}}},R=new Map,V=t=>{const e={annotations:new Map,commentAnnotations:[],selectedAnnotations:new Set,editorId:t};return R.set(t,e),e},H=t=>{let e=R.get(t);return e||(e=V(t)),e},U=(t,e)=>{try{const n=H(t);n.commentAnnotations=e;const o=new Map;for(const t of e){if(!t?.annotationId)continue;const e={annotationId:t.annotationId,multiThreadAnnotationId:t.multiThreadAnnotationId,context:t.context};o.set(t.annotationId,e)}n.annotations=o}catch(t){g.catch("Error updating annotations:",t)}},z=(t,e)=>{try{const n=R.get(t);return n&&n.annotations.get(e)||null}catch(t){return g.catch("Error getting annotation:",t),null}};async function B({editorId:t,editor:e,context:o}){const i="[addComment]";if(e)try{const c=(t=>{const e="[getCurrentSelectionContext]";try{if(!t?.state?.selection)return g.warn(`${e} No editor state or selection found`),null;const{from:o,to:r}=t.state.selection;if(o===r)return g.warn(`${e} Empty/collapsed selection (from === to)`),null;const a=t.state.doc.textBetween(o,r);if(!a||0===a.trim().length)return g.warn(`${e} No text or empty text found`),null;const i=O(t),c=i?.id||void 0;let s;if(i){const t=i.closest(`[${n.LOCATION_ID}]`);s=t?.getAttribute(n.LOCATION_ID)||void 0}return{text:a,from:o,to:r,occurrence:p(t,a,i,o,r),targetTextNodeId:c,locationId:s}}catch(t){return g.catch(`${e} Error getting selection context:`,t),null}})(e);if(!c)return void g.warn(`${i} No valid selection found, exiting`);const{editorId:s}=E(t,e),d=void 0!==t||s!==a,l={...o,textEditorConfig:{text:c.text,occurrence:c.occurrence,...d&&{editorId:s},targetTextNodeId:c.targetTextNodeId}};let u;c.locationId&&(u={id:c.locationId});const h=await(t=>{const e="[addVeltComment]";try{const n=L();return n?n.addManualComment({context:t.context,location:t.location}).then(t=>t?.annotation?{annotation:t.annotation,result:t}:(g.warn(`${e} No annotation in result`),{annotation:null,result:t})).catch(t=>(g.catch(`${e} Error adding Velt comment:`,t),{annotation:null,result:null})):(g.warn(`${e} Velt SDK not available, cannot add comment`),Promise.resolve({annotation:null,result:null}))}catch(t){return g.catch(`${e} Exception adding Velt comment:`,t),Promise.resolve({annotation:null,result:null})}})({context:l,location:u}),m=h.annotation;if(!m)return void g.warn(`${i} Failed to create comment via Velt SDK`);const I=((t,e=r)=>{try{return!1===f(t,e)}catch(t){return g.warn("[shouldApplyMark] Error checking persistVeltMarks, defaulting to true:",t),!0}})(e,r);I&&m.annotationId&&_(e,m.annotationId,m.multiThreadAnnotationId,c.from,c.to),m.annotationId?U(s,[m]):g.warn(`${i} No annotationId, skipping state update`),(t=>{const e="[resetEditorSelection]";try{const n=t;n?.chain?n.chain().focus().run():g.warn(`${e} Editor chain API not available`)}catch(t){g.warn(`${e} Error resetting selection:`,t)}})(e)}catch(t){g.catch(`${i} Error adding comment:`,t)}else g.catch(`${i} ERROR: No editor provided`)}const F=new Map,P=(t,e,n)=>{try{const o=F.get(t);o?o.editor=e:F.set(t,{editor:e,previousFilteredAnnotations:[]});const r=(t=>{try{const e=R.get(t);return e?new Set(e.selectedAnnotations):new Set}catch(t){return g.catch("Error getting selected annotations:",t),new Set}})(t),a=((t,e)=>t.filter(t=>t?.status?.type!==s||e.has(t.annotationId)))(n,r),i=F.get(t)?.previousFilteredAnnotations||[],c=new Set(a.map(t=>t.annotationId).filter(Boolean)),d=new Set(i.map(t=>t.annotationId).filter(Boolean)),l=Array.from(d).filter(t=>!c.has(t));for(const t of l)t&&C(e,t);const u=F.get(t);u&&(u.previousFilteredAnnotations=a),((t,e)=>{for(const n of e){const e=n?.context?.textEditorConfig;if(!e?.text||!n.annotationId)continue;const o=e.text,r=e.occurrence||1,a=e.targetTextNodeId,i=N(t,o,a,r);if(0===i.length)continue;const c=i[Math.min(r-1,i.length-1)];_(t,n.annotationId,n.multiThreadAnnotationId,c.from,c.to)}})(e,a)}catch(t){g.catch("[commentRenderer:updateComments] Error updating comments:",t)}},J=(t,e)=>{const n="[commentRenderer:updateSelection]";try{const e=F.get(t);if(!e||!e.editor)return void g.warn(`${n} No editor found for editorId: ${t}`);const o=(t=>{try{const e=R.get(t);return e?[...e.commentAnnotations]:[]}catch(t){return g.catch("Error getting comment annotations:",t),[]}})(t);P(t,e.editor,o)}catch(t){g.catch(`${n} Error updating selection:`,t)}},j=new Map,K=({editor:t,editorId:e,commentAnnotations:n})=>{const o="[renderComments]";try{if(!t)return void g.warn(`${o} No editor provided`);if(!(()=>{try{const t=window?.Velt;return!!t?.loaded}catch(t){return g.catch("Error checking Velt availability:",t),!1}})())return;n&&Array.isArray(n)||(n=[]);const{editorId:r}=E(e,t),i=JSON.parse(JSON.stringify(n)).filter(t=>{if(!t?.context?.textEditorConfig)return!1;const e=t.context.textEditorConfig.editorId;return r===a?void 0===e||e===a:e===r});if(U(r,i),!j.has(r)){const t=r,e=k(e=>{((t,e)=>{try{H(t).selectedAnnotations=new Set(e)}catch(t){g.catch("Error setting selected annotations:",t)}})(t,e),J(t)},t);j.set(r,e)}P(r,t,i)}catch(t){g.catch(`${o} Error rendering comments:`,t)}},q=new Map,G=(t,e,r)=>{const a="[handleContentUpdate]";try{const i=((t,e,r=o,a)=>{const i=new Map;try{if(!e.docChanged)return i;const o=new Map;t.state.doc.descendants((t,e,a,i)=>{const c=t;if(!c.isText)return;const s=c.marks.find(t=>t.type.name===r);if(!s)return;const d=s.attrs[n.ANNOTATION_ID];if("string"!=typeof d)return;const l=s.attrs[n.MULTI_THREAD_ANNOTATION_ID],u=o.get(d),g={node:{text:c.text||"",nodeSize:c.nodeSize},pos:e};u?u.nodes.push(g):o.set(d,{id:d,multiThreadAnnotationId:l,nodes:[g]})});for(const[e,n]of Array.from(o.entries())){const o=`[detectDocumentChanges:${e}]`,r=a(e);if(!r){g.warn(`${o} No stored data found, skipping`);continue}const c=[...n.nodes].sort((t,e)=>t.pos-e.pos).map(t=>t.node.text||"").join(""),s=c!==r.originalText,d=y(t,{annotationId:e,currentText:c,targetTextNodeId:r.targetTextNodeId,nodes:n.nodes}),l=d.targetTextNodeIdChanged,u=d.newTargetTextNodeId,h=d.searchResults,f=M(t,{searchResults:h,nodes:n.nodes,contentChanged:s,currentText:c,targetTextNodeId:l?u:r.targetTextNodeId,originalOccurrence:r.originalOccurrence}),m=f!==r.originalOccurrence;if(s||m||l){const t={annotationId:n.id,multiThreadAnnotationId:n.multiThreadAnnotationId,originalText:r.originalText,currentText:s?c:r.originalText,originalOccurrence:r.originalOccurrence,currentOccurrence:f,originalTargetTextNodeId:r.targetTextNodeId,newTargetTextNodeId:l?u:r.targetTextNodeId,annotation:r.annotation,contentChanged:s,occurrenceChanged:m,targetTextNodeIdChanged:l};i.set(e,t)}}}catch(t){g.catch("Error detecting document changes:",t)}return i})(e,r,o,e=>{const n=z(t,e);if(!n)return g.warn(`${a} Annotation not found in state: ${e}`),null;return{annotation:n,originalText:n.context?.textEditorConfig?.text||"",originalOccurrence:n.context?.textEditorConfig?.occurrence||1,targetTextNodeId:n.context?.textEditorConfig?.targetTextNodeId||""}});if(0===i.size)return;((t,e)=>{try{if(!t||0===e.size)return;for(const[n,o]of Array.from(e.entries()))(o.targetTextNodeIdChanged||o.contentChanged)&&C(t,n)}catch(t){g.catch("Error updating marks:",t)}})(e,i);for(const[e,n]of Array.from(i.entries())){const o=`${a}[${e}]`,r=z(t,e);if(!r){g.warn(`${o} Annotation not found in state, skipping`);continue}if(!r.context?.textEditorConfig){g.warn(`${o} No textEditorConfig, skipping`);continue}const i=JSON.parse(JSON.stringify(r.context.textEditorConfig));n.contentChanged&&(i.text=n.currentText),n.occurrenceChanged&&(i.occurrence=n.currentOccurrence),n.targetTextNodeIdChanged&&(i.targetTextNodeId=n.newTargetTextNodeId);const c={...r.context,textEditorConfig:i};S(e,c)}}catch(t){g.catch("Error handling content update:",t)}},Q=t.create({name:o,addOptions:()=>({HTMLAttributes:{},persistVeltMarks:!0,editorId:void 0,extensionName:void 0}),addStorage(){return{persistVeltMarks:this.options.persistVeltMarks,editorId:this.options.editorId}},parseHTML:()=>[{tag:i,getAttrs:t=>{const e=t.getAttribute(n.ANNOTATION_ID),o=t.getAttribute(n.MULTI_THREAD_ANNOTATION_ID);return{[n.ANNOTATION_ID]:e,[n.MULTI_THREAD_ANNOTATION_ID]:o}}}],renderHTML({HTMLAttributes:t}){return[i,e(this.options.HTMLAttributes,t),0]},addAttributes:()=>({[n.ANNOTATION_ID]:{default:null,parseHTML:t=>t.getAttribute(n.ANNOTATION_ID),renderHTML:t=>t[n.ANNOTATION_ID]?{[n.ANNOTATION_ID]:t[n.ANNOTATION_ID]}:{}},[n.MULTI_THREAD_ANNOTATION_ID]:{default:null,parseHTML:t=>t.getAttribute(n.MULTI_THREAD_ANNOTATION_ID),renderHTML:t=>t[n.MULTI_THREAD_ANNOTATION_ID]?{[n.MULTI_THREAD_ANNOTATION_ID]:t[n.MULTI_THREAD_ANNOTATION_ID]}:{}}}),addCommands(){return{setVeltComment:(t,e,o,r)=>({tr:a,state:i,dispatch:c})=>{const s=void 0!==o?o:i.selection.from,d=void 0!==r?r:i.selection.to;if(c){const o={};return e?o[n.MULTI_THREAD_ANNOTATION_ID]=e:t&&(o[n.ANNOTATION_ID]=t),a.addMark(s,d,this.type.create(o)),!0}return!1}}},onCreate(){const t=m(this.editor,r)||w(this.editor)||a;((t,e,n)=>{const o=t||a;V(o);const r={editorId:o,editor:e,config:n||{}};q.set(o,r)})(t,this.editor,{persistVeltMarks:this.options.persistVeltMarks,editorId:t,HTMLAttributes:this.options.HTMLAttributes})},onDestroy(){m(this.editor,r)||w(this.editor)},onTransaction({transaction:t,editor:e}){try{if(!t.docChanged)return;if(!f(e,r))return;let n=m(this.editor,r);n||(n=w(e)||a),G(n,e,t)}catch(t){const e=this.options.extensionName||c;g.catch(`[${e}:onTransaction] Error processing transaction:`,t)}}});export{Q as TiptapVeltComments,B as addComment,K as renderComments};
//# sourceMappingURL=index.js.map