actually-written 0.0.0-stage → 0.1.0
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/README.md +20 -2
- package/dist/activity.d.ts +52 -0
- package/dist/badge.d.ts +6 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +52 -0
- package/dist/textEdit.d.ts +5 -0
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Actually Written
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Record how the answers in your forms were written. Applicants see nothing different. Review every submission at https://actuallywritten.com/submissions.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install actually-written
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { track } from "actually-written";
|
|
11
|
+
|
|
12
|
+
track(document.querySelector("form"), { key: "aw_pk_…" });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Get your public key at https://actuallywritten.com/keys. It is safe to commit.
|
|
16
|
+
|
|
17
|
+
Add `<div class="actually-written-badge"></div>` inside the form to show people that their writing is recorded.
|
|
18
|
+
|
|
19
|
+
The form submits a hidden `actually_written_session` field. Store it with the submission to match it with its result.
|
|
20
|
+
|
|
21
|
+
Full documentation: https://actuallywritten.com/docs
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type Evidence = {
|
|
2
|
+
revisionCount: number;
|
|
3
|
+
typedWordCount: number;
|
|
4
|
+
pasteCount: number;
|
|
5
|
+
pastedWordCount: number;
|
|
6
|
+
largestPasteWordCount: number;
|
|
7
|
+
dictationCount?: number;
|
|
8
|
+
dictatedWordCount?: number;
|
|
9
|
+
dictationSeconds?: number;
|
|
10
|
+
pauseCount: number;
|
|
11
|
+
activeWritingSeconds: number;
|
|
12
|
+
idleSeconds: number;
|
|
13
|
+
};
|
|
14
|
+
type InitialActivity = {
|
|
15
|
+
revisionCount?: number;
|
|
16
|
+
evidence?: Partial<Evidence>;
|
|
17
|
+
};
|
|
18
|
+
type History = {
|
|
19
|
+
revisionCount: number;
|
|
20
|
+
evidence: Evidence;
|
|
21
|
+
};
|
|
22
|
+
export declare const createWritingActivity: (initial?: InitialActivity) => {
|
|
23
|
+
type: (text: string) => void;
|
|
24
|
+
backspace: (isRevisionCounted: boolean) => void;
|
|
25
|
+
copy: (text: string) => void;
|
|
26
|
+
paste: (text: string) => true | undefined;
|
|
27
|
+
confirmDictation: (text: string, seconds: number) => void;
|
|
28
|
+
dictate: (text: string, startedAt: number) => void;
|
|
29
|
+
stop: () => void;
|
|
30
|
+
endRevision: () => void;
|
|
31
|
+
read: (body: string) => {
|
|
32
|
+
wordCount: number;
|
|
33
|
+
revisionCount: number;
|
|
34
|
+
evidence: {
|
|
35
|
+
activeWritingSeconds: number;
|
|
36
|
+
idleSeconds: number;
|
|
37
|
+
dictationCount?: number | undefined;
|
|
38
|
+
dictatedWordCount?: number | undefined;
|
|
39
|
+
dictationSeconds?: number | undefined;
|
|
40
|
+
revisionCount: number;
|
|
41
|
+
typedWordCount: number;
|
|
42
|
+
pasteCount: number;
|
|
43
|
+
pastedWordCount: number;
|
|
44
|
+
largestPasteWordCount: number;
|
|
45
|
+
pauseCount: number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
include: (history: History, isRevisionCounted: boolean) => void;
|
|
49
|
+
clearRevisions: () => void;
|
|
50
|
+
readonly detectedWords: number;
|
|
51
|
+
};
|
|
52
|
+
export {};
|
package/dist/badge.d.ts
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type TrackOptions = {
|
|
2
|
+
key: string;
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const track: (root: HTMLElement | null, { key, endpoint }: TrackOptions) => {
|
|
6
|
+
id: `${string}-${string}-${string}-${string}-${string}`;
|
|
7
|
+
submit: () => Promise<void>;
|
|
8
|
+
stop: () => void;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
var v=e=>e.match(/\S+/gu)?.length||0,W=e=>e.replace(/\s+/gu," ").trim(),A=(e={})=>{let i=e.evidence||{},s={lastInputAt:null,pauseTimer:null},t={revisionCount:e.revisionCount||0,isBackspacing:!1,detectedWords:0,isInWord:!1,hasRecordedPause:!1,typedWordCount:i.typedWordCount||0,pasteCount:i.pasteCount||0,privateRevisionCount:i.revisionCount??e.revisionCount??0,pastedWordCount:i.pastedWordCount||0,largestPasteWordCount:i.largestPasteWordCount||0,dictationCount:i.dictationCount||0,dictatedWordCount:i.dictatedWordCount||0,dictationSeconds:i.dictationSeconds||0,copiedTexts:new Set,pauseCount:i.pauseCount||0,activeWritingSeconds:i.activeWritingSeconds||0,idleSeconds:i.idleSeconds||0},a=()=>{s.lastInputAt===null||t.hasRecordedPause||Date.now()-s.lastInputAt<7*1e3||(t.pauseCount++,t.hasRecordedPause=!0)},d=()=>{s.pauseTimer!==null&&clearTimeout(s.pauseTimer)},S=()=>{a(),d(),s.pauseTimer=null,s.lastInputAt=null,t.isBackspacing=!1},l=(n=Date.now())=>{a(),d();let r=s.lastInputAt===null?0:Math.max(0,n-s.lastInputAt)/1e3;r>=7?t.idleSeconds+=Math.min(r,1800):t.activeWritingSeconds+=r,s.lastInputAt=n,t.hasRecordedPause=!1,s.pauseTimer=setTimeout(a,7*1e3)};return{type:n=>{t.isBackspacing=!1,l();let r=Math.max(0,v(n)-(t.isInWord&&/^\S/u.test(n)?1:0));t.detectedWords+=r,t.typedWordCount+=r,t.isInWord=/\S$/u.test(n)},backspace:n=>{l(),!t.isBackspacing&&(t.isBackspacing=!0,t.privateRevisionCount++,n&&t.revisionCount++)},copy:n=>{W(n)&&t.copiedTexts.add(W(n))},paste:n=>{if(t.isBackspacing=!1,t.isInWord=!1,t.copiedTexts.has(W(n)))return;l();let r=v(n);return t.pasteCount++,t.pastedWordCount+=r,t.largestPasteWordCount=Math.max(t.largestPasteWordCount,r),!0},confirmDictation:(n,r)=>{let p=v(n);t.pasteCount=Math.max(0,t.pasteCount-1),t.pastedWordCount=Math.max(0,t.pastedWordCount-p),t.dictationCount++,t.dictatedWordCount+=p,t.dictationSeconds+=r,t.activeWritingSeconds+=r,t.detectedWords+=p},dictate:(n,r)=>{t.isBackspacing=!1,t.isInWord=!1;let p=Date.now(),y=Math.min(p,Math.max(r,s.lastInputAt??r));l(y);let m=(p-y)/1e3;t.activeWritingSeconds+=m,t.dictationSeconds+=m,s.lastInputAt=p;let E=v(n);t.dictationCount++,t.dictatedWordCount+=E,t.detectedWords+=E},stop:S,endRevision:()=>{t.isBackspacing=!1},read:n=>(a(),{wordCount:v(n),revisionCount:t.revisionCount,evidence:{revisionCount:t.privateRevisionCount,typedWordCount:t.typedWordCount,pasteCount:t.pasteCount,pastedWordCount:t.pastedWordCount,largestPasteWordCount:t.largestPasteWordCount,pauseCount:t.pauseCount,...t.dictationCount?{dictationCount:t.dictationCount,dictatedWordCount:t.dictatedWordCount,dictationSeconds:Math.round(t.dictationSeconds)}:{},activeWritingSeconds:Math.round(t.activeWritingSeconds),idleSeconds:Math.round(t.idleSeconds)}}),include:(n,r)=>{t.typedWordCount+=n.evidence.typedWordCount,t.pasteCount+=n.evidence.pasteCount,t.pastedWordCount+=n.evidence.pastedWordCount,t.largestPasteWordCount=Math.max(t.largestPasteWordCount,n.evidence.largestPasteWordCount),t.pauseCount+=n.evidence.pauseCount,t.activeWritingSeconds+=n.evidence.activeWritingSeconds,t.idleSeconds+=n.evidence.idleSeconds,t.privateRevisionCount+=n.evidence.revisionCount,r&&(t.revisionCount+=n.revisionCount)},clearRevisions:()=>{t.revisionCount=0},get detectedWords(){return t.detectedWords}}};var T=(e,i,s)=>{if(i>=s)return i;let t=Math.ceil((i+s)/2);return e(t)?T(e,t,s):T(e,i,t-1)},w=(e,i)=>{let s=Math.min(e.length,i.length),t=T(d=>e.slice(0,d)===i.slice(0,d),0,s),a=T(d=>e.slice(e.length-d)===i.slice(i.length-d),0,s-t);return{position:t,deleted:e.slice(t,e.length-a),inserted:i.slice(t,i.length-a)}};var P=".actually-written-badge",D=`
|
|
2
|
+
a {
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
height: 40px;
|
|
6
|
+
padding: 0 8px 0 6px;
|
|
7
|
+
border: 1px solid #000;
|
|
8
|
+
border-radius: 20px;
|
|
9
|
+
background: #fff;
|
|
10
|
+
color: #000;
|
|
11
|
+
font: 13px/1.2 system-ui, sans-serif;
|
|
12
|
+
text-decoration: none;
|
|
13
|
+
}
|
|
14
|
+
img {
|
|
15
|
+
width: 36px;
|
|
16
|
+
height: 36px;
|
|
17
|
+
}
|
|
18
|
+
.label {
|
|
19
|
+
display: grid;
|
|
20
|
+
max-width: 0;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
transition: max-width 0.2s;
|
|
24
|
+
}
|
|
25
|
+
a:hover .label,
|
|
26
|
+
a:focus-visible .label {
|
|
27
|
+
max-width: 280px;
|
|
28
|
+
}
|
|
29
|
+
.label > * {
|
|
30
|
+
margin: 0 2px 0 6px;
|
|
31
|
+
}
|
|
32
|
+
.status {
|
|
33
|
+
color: #666;
|
|
34
|
+
}
|
|
35
|
+
.state {
|
|
36
|
+
display: grid;
|
|
37
|
+
place-items: center;
|
|
38
|
+
width: 18px;
|
|
39
|
+
height: 18px;
|
|
40
|
+
margin-left: 6px;
|
|
41
|
+
border-radius: 50%;
|
|
42
|
+
background: #000;
|
|
43
|
+
color: #fff;
|
|
44
|
+
font-size: 10px;
|
|
45
|
+
}
|
|
46
|
+
[data-state="off"] .state {
|
|
47
|
+
background: #c00;
|
|
48
|
+
}
|
|
49
|
+
`,H=e=>{switch(e){case"recording":return{status:"Recording how this form is written",mark:"\u2713"};case"off":return{status:"Not recording",mark:"\u2715"};default:return e}},_=(e,i)=>{let s=e.shadowRoot??e.attachShadow({mode:"open"});s.innerHTML=`<style>${D}</style><a target="_blank" rel="noopener"><img alt=""><span class="label"><strong>Actually Written</strong><span class="status"></span></span><span class="state" aria-hidden="true"></span></a>`;let t=s.querySelector("a"),a=s.querySelector("img"),d=s.querySelector(".status"),S=s.querySelector(".state");if(!t||!a||!d||!S)throw new Error("Actually Written could not render its badge.");t.href=new URL("/process",i).href,a.src=new URL("/favicon.svg",i).href;let l=n=>{let r=H(n);t.dataset.state=n,t.setAttribute("aria-label",`Actually Written: ${r.status}`),d.textContent=r.status,S.textContent=r.mark};return l("recording"),{setState:l,remove:()=>s.replaceChildren()}},M=(e,i)=>{let s=[...e.querySelectorAll(P)].map(t=>_(t,i));return{setState:t=>s.forEach(a=>a.setState(t)),remove:()=>s.forEach(t=>t.remove())}};var I='textarea, [contenteditable=""], [contenteditable="true"], input:not([type]), input[type="text"], input[type="search"], input[type="url"]',B="actually_written_session",R="https://actuallywritten.com",O=6e4,N=["copy","cut","dragstart"],U=["ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Home","End","PageUp","PageDown"],k=e=>e instanceof HTMLElement&&e.matches(I),b=e=>e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement?e.value:e.innerText,F=e=>e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement?e.value.slice(e.selectionStart??0,e.selectionEnd??0):e.ownerDocument.getSelection()?.toString()??"",q=e=>e instanceof InputEvent&&/^insertFrom(?:Paste|Drop)$/u.test(e.inputType),$=e=>e instanceof HTMLFormElement?e:e.closest("form")??e.querySelector("form"),J=(e,{key:i,endpoint:s=R})=>{if(!e)throw new Error("Actually Written needs a form element to track.");let t=crypto.randomUUID(),a=A(),d=new AbortController,S={capture:!0,signal:d.signal},l={startedAt:null},n=new Map,r=$(e),p=Object.assign(document.createElement("input"),{type:"hidden",name:B,value:t});r?.append(p);let y=M(r??e,s),m=()=>{l.startedAt??(l.startedAt=Date.now())},E=o=>{let u=n.get(o);if(u)return u;let c=b(o),C={initialText:c,text:c,edits:[]};return n.set(o,C),C},L=()=>[e,...e.querySelectorAll(I)].filter(k).map((o,u)=>{let c=n.get(o);return{name:o.getAttribute("name")||o.id||`field-${u+1}`,text:b(o),...c?.edits.length?{initialText:c.initialText,edits:c.edits}:{}}}).filter(o=>o.text.trim()),g=(o,u)=>e.addEventListener(o,c=>{c.isTrusted&&k(c.target)&&u(c,c.target)},S);g("keydown",o=>{if(U.includes(o.key)&&a.endRevision(),o.key==="Backspace"){m(),a.backspace(!0);return}o.isComposing||o.ctrlKey||o.metaKey||o.altKey||(o.key.length===1||o.key==="Enter")&&(m(),a.type(o.key==="Enter"?`
|
|
50
|
+
`:o.key))}),g("compositionend",o=>{m(),a.type(o.data)}),g("pointerdown",()=>a.endRevision()),N.forEach(o=>g(o,(u,c)=>a.copy(F(c)))),g("paste",o=>{m(),a.paste(o.clipboardData?.getData("text/plain")??"")}),g("drop",o=>{m(),a.paste(o.dataTransfer?.getData("text/plain")??"")}),g("beforeinput",(o,u)=>E(u)),g("input",(o,u)=>{let c=E(u),C=b(u);C!==c.text&&(c.edits.push({at:Date.now(),...w(c.text,C),source:q(o)?"pasted":"typed"}),c.text=C)});let h=async()=>{let o=L(),{revisionCount:u,evidence:c}=a.read(o.map(f=>f.text).join(`
|
|
51
|
+
|
|
52
|
+
`)),C=l.startedAt===null?0:Math.round((Date.now()-l.startedAt)/1e3),x=JSON.stringify({key:i,sessionId:t,fields:o,metrics:{revisionCount:u,writingTimeSeconds:C,evidence:c}});try{let f=await fetch(new URL("/api/submissions",s),{method:"POST",headers:{"Content-Type":"text/plain"},body:x,keepalive:new Blob([x]).size<=O});if(f.ok)return;y.setState("off"),console.warn(`Actually Written could not record this submission (HTTP ${f.status}).`)}catch(f){y.setState("off"),console.warn("Actually Written could not record this submission.",f)}};return(r??e).addEventListener("submit",()=>{h()},S),{id:t,submit:h,stop:()=>{d.abort(),a.stop(),p.remove(),y.remove()}}};export{J as track};
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "actually-written",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Record how answers in your forms were written, then review each submission's writing evidence.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://actuallywritten.com/docs",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/thecontextcompany/pasta.git",
|
|
10
|
+
"directory": "packages/sdk"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"scripts": {
|
|
15
|
+
"prepublishOnly": "npm --prefix ../.. run sdk:build"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|