vue-fillable-text 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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/FillableText.vue.d.ts +22 -0
- package/dist/FillableTextEditor.vue.d.ts +28 -0
- package/dist/core.d.ts +12 -0
- package/dist/index.d.ts +5 -0
- package/dist/types.d.ts +60 -0
- package/dist/vue-fillable-text.cjs +6 -0
- package/dist/vue-fillable-text.css +1 -0
- package/dist/vue-fillable-text.js +673 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Evgeniia Iaroslavtseva
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# vue-fillable-text
|
|
2
|
+
|
|
3
|
+
Typed Vue 3 components for documents that mix prose with interactive input and select gaps. It is the Vue counterpart of `react-fillable-text` and uses the same version 1 document schema.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install vue-fillable-text
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Import the styles once:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import "vue-fillable-text/styles.css";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```vue
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { ref } from "vue";
|
|
22
|
+
import { FillableText, type FillableDocument, type FillableValues } from "vue-fillable-text";
|
|
23
|
+
import "vue-fillable-text/styles.css";
|
|
24
|
+
|
|
25
|
+
const document: FillableDocument = {
|
|
26
|
+
version: 1,
|
|
27
|
+
content: [
|
|
28
|
+
{ type: "text", text: "The capital of France is " },
|
|
29
|
+
{ type: "gap", id: "capital", control: "input", originalText: "Paris", placeholder: "Enter a city…" },
|
|
30
|
+
{ type: "text", text: "." },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
const values = ref<FillableValues>({});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<FillableText v-model="values" :document="document" @gap-change="(id, value) => console.log(id, value)" />
|
|
38
|
+
</template>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Editor
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<script setup lang="ts">
|
|
45
|
+
import { ref } from "vue";
|
|
46
|
+
import { FillableTextEditor, type FillableDocument } from "vue-fillable-text";
|
|
47
|
+
|
|
48
|
+
const document = ref<FillableDocument>({ version: 1, content: [{ type: "text", text: "Select text to create a gap." }] });
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template><FillableTextEditor v-model="document" /></template>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The editor supports input and select gaps, plain-text paste, keyboard deletion, and undo/redo with Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z.
|
|
55
|
+
|
|
56
|
+
## Components and events
|
|
57
|
+
|
|
58
|
+
- `FillableText`: `v-model` contains answers; accepts `document`, `disabled`, `gapStates`, `getGapState`, and `components`; emits `gap-change`.
|
|
59
|
+
- `FillableTextEditor`: `v-model` contains the document; also supports uncontrolled use with `defaultValue`; emits `change`, `gap-create`, `gap-update`, `gap-remove`, and `selection-change`.
|
|
60
|
+
- Custom controls receive `modelValue`, `id`, `state`, `disabled`, `ariaLabel`, plus `placeholder` or `options`, and must emit `update:modelValue`.
|
|
61
|
+
- Core helpers: `createGap`, `updateGap`, `removeGap`, `deleteGap`, `serialize`, `deserialize`, `validateDocument`, and `toPlainText`.
|
|
62
|
+
|
|
63
|
+
Documents can be exchanged directly with `react-fillable-text`; answers are deliberately stored separately.
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install
|
|
69
|
+
npm test
|
|
70
|
+
npm run typecheck
|
|
71
|
+
npm run build
|
|
72
|
+
npm pack --dry-run
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FillableComponents, FillableDocument, FillableValues, GapNode, GapState } from "./types";
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
document: FillableDocument;
|
|
4
|
+
modelValue: FillableValues;
|
|
5
|
+
components?: FillableComponents;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
gapStates?: Partial<Record<string, GapState>>;
|
|
9
|
+
getGapState?: (gap: GapNode) => GapState;
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:modelValue": (values: FillableValues) => any;
|
|
13
|
+
gapChange: (id: string, value: string) => any;
|
|
14
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
15
|
+
"onUpdate:modelValue"?: ((values: FillableValues) => any) | undefined;
|
|
16
|
+
onGapChange?: ((id: string, value: string) => any) | undefined;
|
|
17
|
+
}>, {
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
class: string;
|
|
20
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: typeof __VLS_export;
|
|
22
|
+
export default _default;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FillableDocument, GapNode, TextSelection } from "./types";
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
modelValue?: FillableDocument;
|
|
4
|
+
defaultValue?: FillableDocument;
|
|
5
|
+
readOnly?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
"update:modelValue": (document: FillableDocument) => any;
|
|
10
|
+
change: (document: FillableDocument) => any;
|
|
11
|
+
gapCreate: (gap: GapNode) => any;
|
|
12
|
+
gapUpdate: (gap: GapNode) => any;
|
|
13
|
+
gapRemove: (gap: GapNode) => any;
|
|
14
|
+
selectionChange: (selection: TextSelection | null) => any;
|
|
15
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
16
|
+
"onUpdate:modelValue"?: ((document: FillableDocument) => any) | undefined;
|
|
17
|
+
onChange?: ((document: FillableDocument) => any) | undefined;
|
|
18
|
+
onGapCreate?: ((gap: GapNode) => any) | undefined;
|
|
19
|
+
onGapUpdate?: ((gap: GapNode) => any) | undefined;
|
|
20
|
+
onGapRemove?: ((gap: GapNode) => any) | undefined;
|
|
21
|
+
onSelectionChange?: ((selection: TextSelection | null) => any) | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
class: string;
|
|
24
|
+
defaultValue: FillableDocument;
|
|
25
|
+
readOnly: boolean;
|
|
26
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
27
|
+
declare const _default: typeof __VLS_export;
|
|
28
|
+
export default _default;
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContentNode, FillableDocument, GapNode, InputGapNode, SelectGapNode, TextSelection, ValidationResult } from "./types";
|
|
2
|
+
export declare const EMPTY_DOCUMENT: FillableDocument;
|
|
3
|
+
export declare function normalizeContent(nodes: ContentNode[]): ContentNode[];
|
|
4
|
+
export declare function toPlainText(document: FillableDocument): string;
|
|
5
|
+
export declare function serialize(document: FillableDocument): string;
|
|
6
|
+
export declare function deserialize(value: string | unknown): FillableDocument;
|
|
7
|
+
export declare function validateDocument(value: unknown): ValidationResult;
|
|
8
|
+
export declare function createGap(document: FillableDocument, selection: TextSelection, gap: Omit<InputGapNode, "type" | "originalText"> | Omit<SelectGapNode, "type" | "originalText">): FillableDocument;
|
|
9
|
+
export declare function updateGap(document: FillableDocument, id: string, gap: GapNode): FillableDocument;
|
|
10
|
+
export declare function removeGap(document: FillableDocument, id: string): FillableDocument;
|
|
11
|
+
export declare function deleteGap(document: FillableDocument, id: string): FillableDocument;
|
|
12
|
+
export declare function slugifyOption(label: string): string;
|
package/dist/index.d.ts
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Component } from "vue";
|
|
2
|
+
export interface FillableDocument {
|
|
3
|
+
version: 1;
|
|
4
|
+
content: ContentNode[];
|
|
5
|
+
}
|
|
6
|
+
export type ContentNode = TextNode | GapNode;
|
|
7
|
+
export interface TextNode {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SelectOption {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
16
|
+
export interface InputGapNode {
|
|
17
|
+
type: "gap";
|
|
18
|
+
id: string;
|
|
19
|
+
control: "input";
|
|
20
|
+
originalText: string;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SelectGapNode {
|
|
24
|
+
type: "gap";
|
|
25
|
+
id: string;
|
|
26
|
+
control: "select";
|
|
27
|
+
originalText: string;
|
|
28
|
+
options: SelectOption[];
|
|
29
|
+
}
|
|
30
|
+
export type GapNode = InputGapNode | SelectGapNode;
|
|
31
|
+
export type FillableValues = Record<string, string>;
|
|
32
|
+
export type GapState = "default" | "success" | "error";
|
|
33
|
+
export interface GapControlProps {
|
|
34
|
+
id: string;
|
|
35
|
+
modelValue: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
ariaLabel?: string;
|
|
39
|
+
state?: GapState;
|
|
40
|
+
}
|
|
41
|
+
export interface SelectGapControlProps extends GapControlProps {
|
|
42
|
+
options: SelectOption[];
|
|
43
|
+
}
|
|
44
|
+
export interface FillableComponents {
|
|
45
|
+
input?: Component;
|
|
46
|
+
select?: Component;
|
|
47
|
+
}
|
|
48
|
+
export interface TextSelection {
|
|
49
|
+
text: string;
|
|
50
|
+
from: number;
|
|
51
|
+
to: number;
|
|
52
|
+
}
|
|
53
|
+
export interface ValidationError {
|
|
54
|
+
path: string;
|
|
55
|
+
message: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ValidationResult {
|
|
58
|
+
valid: boolean;
|
|
59
|
+
errors: ValidationError[];
|
|
60
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("vue"),fe={key:0},ve=["data-state","aria-invalid","aria-label","value","disabled","placeholder","size","onInput"],he=["data-state"],ye=["aria-invalid","aria-label","value","disabled","onChange"],xe=["value"],ke=e.defineComponent({__name:"FillableText",props:{document:{},modelValue:{},components:{},disabled:{type:Boolean,default:!1},class:{default:""},gapStates:{},getGapState:{}},emits:["update:modelValue","gapChange"],setup(a,{emit:r}){const g=a,c=r,b=e.computed(()=>["fillable-text",g.class]);function m(p,y){c("update:modelValue",{...g.modelValue,[p]:y}),c("gapChange",p,y)}function k(p){return g.getGapState?.(p)??g.gapStates?.[p.id]??"default"}return(p,y)=>(e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(b.value)},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(a.document.content,(s,u)=>(e.openBlock(),e.createElementBlock(e.Fragment,{key:s.type==="gap"?s.id:`text-${u}`},[s.type==="text"?(e.openBlock(),e.createElementBlock("span",fe,e.toDisplayString(s.text),1)):s.control==="input"&&a.components?.input?(e.openBlock(),e.createBlock(e.resolveDynamicComponent(a.components?.input),{key:1,id:s.id,"model-value":a.modelValue[s.id]??"",disabled:a.disabled,placeholder:s.placeholder??"Type answer","aria-label":`Fill in ${s.originalText}`,state:k(s),"onUpdate:modelValue":h=>m(s.id,h)},null,8,["id","model-value","disabled","placeholder","aria-label","state","onUpdate:modelValue"])):s.control==="input"?(e.openBlock(),e.createElementBlock("input",{key:2,class:"fillable-text__input","data-state":k(s),"aria-invalid":k(s)==="error"||void 0,"aria-label":`Fill in ${s.originalText}`,value:a.modelValue[s.id]??"",disabled:a.disabled,placeholder:s.placeholder??"Type answer",size:Math.max(8,(a.modelValue[s.id]??"").length||(s.placeholder??"").length||8),onInput:h=>m(s.id,h.target.value)},null,40,ve)):a.components?.select?(e.openBlock(),e.createBlock(e.resolveDynamicComponent(a.components?.select),{key:3,id:s.id,"model-value":a.modelValue[s.id]??"",disabled:a.disabled,options:s.options,"aria-label":`Fill in ${s.originalText}`,state:k(s),"onUpdate:modelValue":h=>m(s.id,h)},null,8,["id","model-value","disabled","options","aria-label","state","onUpdate:modelValue"])):(e.openBlock(),e.createElementBlock("span",{key:4,class:"fillable-text__select-wrap","data-state":k(s)},[e.createElementVNode("select",{class:"fillable-text__select","aria-invalid":k(s)==="error"||void 0,"aria-label":`Fill in ${s.originalText}`,value:a.modelValue[s.id]??"",disabled:a.disabled,onChange:h=>m(s.id,h.target.value)},[y[0]||(y[0]=e.createElementVNode("option",{value:"",disabled:""},"Select an option",-1)),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(s.options,h=>(e.openBlock(),e.createElementBlock("option",{key:h.id,value:h.value},e.toDisplayString(h.label),9,xe))),128))],40,ye)],8,he))],64))),128))],2))}}),be={version:1,content:[{type:"text",text:""}]};function B(a){const r=[];for(const g of a)if(g.type==="text"){if(!g.text)continue;const c=r.at(-1);c?.type==="text"?c.text+=g.text:r.push({...g})}else r.push(g);return r.length?r:[{type:"text",text:""}]}function O(a){return a.content.map(r=>r.type==="text"?r.text:r.originalText).join("")}function Ee(a){return JSON.stringify(a,null,2)}function Ce(a){const r=typeof a=="string"?JSON.parse(a):a,g=J(r);if(!g.valid)throw new Error(g.errors.map(c=>`${c.path}: ${c.message}`).join("; "));return r}function A(a){return typeof a=="object"&&a!==null&&!Array.isArray(a)}function J(a){const r=[];if(!A(a))return{valid:!1,errors:[{path:"",message:"Document must be an object"}]};if(a.version!==1&&r.push({path:"version",message:"Only schema version 1 is supported"}),!Array.isArray(a.content))return{valid:!1,errors:[...r,{path:"content",message:"Content must be an array"}]};const g=new Set;return a.content.forEach((c,b)=>{const m=`content[${b}]`;if(!A(c)||c.type!=="text"&&c.type!=="gap")return void r.push({path:m,message:"Unknown node type"});if(c.type==="text"){typeof c.text!="string"&&r.push({path:`${m}.text`,message:"Text must be a string"});return}if(typeof c.id!="string"||!c.id.trim()?r.push({path:`${m}.id`,message:"Gap id is required"}):g.has(c.id)?r.push({path:`${m}.id`,message:"Gap id must be unique"}):g.add(c.id),(typeof c.originalText!="string"||!c.originalText)&&r.push({path:`${m}.originalText`,message:"Original text is required"}),c.control!=="input"&&c.control!=="select"&&r.push({path:`${m}.control`,message:"Control must be input or select"}),c.control==="select")if(!Array.isArray(c.options)||c.options.length<2)r.push({path:`${m}.options`,message:"Select gap requires at least 2 options"});else{const k=new Set;c.options.forEach((p,y)=>{const s=`${m}.options[${y}]`;(!A(p)||typeof p.label!="string"||!p.label.trim())&&r.push({path:`${s}.label`,message:"Option label is required"}),!A(p)||typeof p.value!="string"||!p.value.trim()?r.push({path:`${s}.value`,message:"Option value is required"}):k.has(p.value)?r.push({path:`${s}.value`,message:"Option value must be unique"}):k.add(p.value)})}}),{valid:r.length===0,errors:r}}function Ne(a){return a.type==="text"?a.text.length:a.originalText.length}function Y(a,r,g){if(r.from<0||r.to<=r.from||r.to>O(a).length)throw new Error("Selection is outside the document");let c=0;const b=[];let m=!1;for(const k of a.content){const p=c,y=p+Ne(k);if(c=y,r.to<=p||r.from>=y){b.push(k);continue}if(k.type==="gap")throw new Error("A selection cannot overlap an existing gap");if(r.from<p||r.to>y)throw new Error("A gap must be created inside one text node");const s=r.from-p,u=r.to-p;s&&b.push({type:"text",text:k.text.slice(0,s)}),b.push({type:"gap",...g,originalText:k.text.slice(s,u)}),u<k.text.length&&b.push({type:"text",text:k.text.slice(u)}),m=!0}if(!m)throw new Error("No editable text selected");return{version:1,content:B(b)}}function W(a,r,g){return{version:1,content:a.content.map(c=>c.type==="gap"&&c.id===r?g:c)}}function Q(a,r){return{version:1,content:B(a.content.map(g=>g.type==="gap"&&g.id===r?{type:"text",text:g.originalText}:g))}}function I(a,r){return{version:1,content:B(a.content.filter(g=>g.type!=="gap"||g.id!==r))}}function U(a){return a.trim().toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-|-$/g,"")}const we=["aria-label"],Ve=["contenteditable"],Te={key:0,"data-text":""},Be={key:0,"data-line-break":""},Se={"data-line":""},$e=["tabindex","data-gap-id","data-gap-length","data-gap-original","aria-label","onClick"],De={key:0,class:"editor-gap__chevron",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},Oe={class:"editor-meta","aria-live":"polite"},Me={key:0,class:"editor-hint"},Ae={class:"gap-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"gap-dialog-title"},Fe={class:"eyebrow"},Le={id:"gap-dialog-title"},Re={class:"segmented",role:"radiogroup","aria-label":"Gap type"},ze=["aria-checked"],Ge=["aria-checked"],Ie={key:1,class:"options-block"},Ue=["value","placeholder","aria-label","onInput"],qe=["aria-label","onClick"],je={key:2,class:"form-error",role:"alert"},Pe={key:0,class:"danger-actions"},Ke=e.defineComponent({__name:"FillableTextEditor",props:{modelValue:{},defaultValue:{default:()=>({version:1,content:[{type:"text",text:"Start writing…"}]})},readOnly:{type:Boolean,default:!1},class:{default:""}},emits:["update:modelValue","change","gapCreate","gapUpdate","gapRemove","selectionChange"],setup(a,{emit:r}){const g=a,c=r,b=e.ref(g.defaultValue),m=e.computed(()=>g.modelValue??b.value),k=e.ref(),p=e.ref(),y=e.ref(null),s=e.ref(null),u=e.ref(null),h=e.ref(null),E=e.ref(""),S=e.ref(null),F=[],L=[];let w=null;const Z=e.computed(()=>m.value.content.reduce((t,n)=>t+$(n),0)),q=e.computed(()=>m.value.content.filter(t=>t.type==="gap").length);function ee(t){return`${t}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,6)}`}function $(t){return t.type==="text"?t.text.length:t.originalText.length}function C(t,n=!0){n&&(F.push(m.value),L.length=0),g.modelValue===void 0&&(b.value=t),c("update:modelValue",t),c("change",t)}function V(){y.value=null,s.value=null,c("selectionChange",null)}function j(t){let n=0;for(const l of m.value.content){const o=n,d=o+$(l);if(n=d,l.type==="gap"&&l.id===t)return{gap:l,start:o,end:d}}return null}function te(t,n,l){let o=0;const d=[];for(const i of m.value.content){const f=o,v=f+$(i);if(o=v,i.type!=="gap")continue;(t===n&&(l==="backward"?v===t:f===t)||t<n&&t<v&&n>f)&&d.push({gap:i,start:f,end:v})}return l==="backward"?d.at(-1)??null:d[0]??null}function ne(t,n){let l=0;const o=[];for(const d of m.value.content){const i=l;if(l=i+$(d),d.type==="gap"){o.push(d);continue}const v=Math.max(0,t-i),x=Math.min(d.text.length,n-i);o.push(v<x?{type:"text",text:d.text.slice(0,v)+d.text.slice(x)}:d)}return{version:1,content:B(o)}}function le(t,n){let l=0,o=!1;const d=[];for(const i of m.value.content){const f=l,v=f+$(i);if(l=v,!o&&i.type==="text"&&t>=f&&t<=v){const x=t-f;d.push({type:"text",text:i.text.slice(0,x)},{type:"text",text:n},{type:"text",text:i.text.slice(x)}),o=!0}else d.push(i)}return o||d.push({type:"text",text:n}),{version:1,content:B(d)}}function R(t){const n=new Map(m.value.content.filter(i=>i.type==="gap").map(i=>[i.id,i])),l=[],o=i=>{const f=i.replaceAll("","");if(!f)return;const v=l.at(-1);v?.type==="text"?v.text+=f:l.push({type:"text",text:f})},d=i=>{if(i instanceof HTMLElement){if(i.dataset.gapId){const f=n.get(i.dataset.gapId);f?l.push(f):o(i.dataset.gapOriginal??"");return}if(i.tagName==="BR"){o(`
|
|
2
|
+
`);return}if(i.tagName==="DIV"||i.tagName==="P"){const f=l.at(-1);l.length&&!(f?.type==="text"&&f.text.endsWith(`
|
|
3
|
+
`))&&o(`
|
|
4
|
+
`)}}if(i.nodeType===Node.TEXT_NODE){o(i.textContent??"");return}i.childNodes.forEach(d)};return t.childNodes.forEach(d),B(l)}function P(t){return t.nodeType===Node.TEXT_NODE?t.textContent?.replaceAll("","").length??0:t instanceof HTMLElement&&t.dataset.gapLength!==void 0?Number(t.dataset.gapLength):t instanceof HTMLElement&&t.tagName==="BR"?1:Array.from(t.childNodes).reduce((n,l)=>n+P(l),0)}function T(t,n,l){if(n!==t&&!t.contains(n))return null;const o=t.ownerDocument.createRange();return o.selectNodeContents(t),o.setEnd(n,l),R(o.cloneContents()).reduce((d,i)=>d+$(i),0)}function ae(t,n){let l=n,o=null;const d=v=>{if(!o){if(v.nodeType===Node.TEXT_NODE){const x=v.textContent??"",N=x.replaceAll("","").length;l<=N?o={node:v,offset:N?l:x.length}:l-=N;return}if(v instanceof HTMLElement&&(v.tagName==="BR"||v.dataset.gapLength!==void 0)){const x=P(v),N=v.parentNode;l<=x&&N?o={node:N,offset:Array.from(N.childNodes).indexOf(v)+(l?1:0)}:l-=x;return}v.childNodes.forEach(d)}};d(t);const i=o??{node:t,offset:t.childNodes.length},f=t.ownerDocument.createRange();f.setStart(i.node,i.offset),f.collapse(!0),t.ownerDocument.getSelection()?.removeAllRanges(),t.ownerDocument.getSelection()?.addRange(f)}e.watch(m,async()=>{w!==null&&(await e.nextTick(),p.value&&ae(p.value,w),w=null)});function M(){if(g.readOnly||!p.value||!k.value)return;const t=window.getSelection();if(!t||!t.rangeCount||t.isCollapsed)return V();const n=t.getRangeAt(0);if(!p.value.contains(n.commonAncestorContainer))return V();const l=T(p.value,n.startContainer,n.startOffset),o=T(p.value,n.endContainer,n.endOffset);if(l===null||o===null||o<=l)return V();const d=O(m.value).slice(l,o);if(!d.trim())return V();const i=n.getBoundingClientRect(),f=k.value.getBoundingClientRect(),v=i.top-f.top-52,x=v<8?"below":"above";y.value={text:d,from:l,to:o},s.value={left:Math.min(Math.max(i.left-f.left+i.width/2,76),f.width-76),top:x==="below"?i.bottom-f.top+10:v,placement:x},c("selectionChange",y.value)}function oe(t){t.preventDefault(),y.value&&(h.value=null,u.value={id:ee("gap"),originalText:y.value.text,control:"input",placeholder:`Enter ${y.value.text.toLocaleLowerCase()}…`,options:[y.value.text,""]},E.value="")}function ie(t){g.readOnly||(h.value=t.id,u.value={id:t.id,originalText:t.originalText,control:t.control,placeholder:t.control==="input"?t.placeholder??"":"",options:t.control==="select"?t.options.map(n=>n.label):[t.originalText,""]},E.value="",V())}function K(t){if(!(!u.value||u.value.control===t)){if(t==="select"){const n=[...u.value.options];n[0]=u.value.originalText,n.length<2&&n.push(""),u.value={...u.value,control:t,options:n}}else u.value={...u.value,control:t,originalText:u.value.options[0]?.trim()||u.value.originalText};E.value=""}}function re(t){if(t.control==="input")return{type:"gap",id:t.id,control:"input",originalText:t.originalText,placeholder:t.placeholder.trim()||void 0};const n=t.options.map((l,o)=>({id:`${t.id}-option-${o+1}`,label:l.trim(),value:U(l)||`option-${o+1}`}));return{type:"gap",id:t.id,control:"select",originalText:n[0].label,options:n}}function se(){if(!u.value)return;let t=u.value;if(t.control==="input"&&!t.originalText.trim())return void(E.value="Original text is required.");if(t.control==="select"){if(!t.options[0]?.trim())return void(E.value="The first option must contain the original text.");const l=t.options.map(d=>d.trim()).filter(Boolean),o=l.map(U);if(l.length<2)return void(E.value="Add at least two non-empty options.");if(new Set(o).size!==o.length)return void(E.value="Every option needs a unique value.");t={...t,options:l}}const n=re(t);try{h.value?(C(W(m.value,h.value,n)),c("gapUpdate",n)):y.value&&(C(Y(m.value,y.value,n)),c("gapCreate",n)),u.value=null,V()}catch(l){E.value=l instanceof Error?l.message:"Could not create gap."}}function H(t){if(!h.value)return;const n=j(h.value);n&&(C(t?I(m.value,h.value):Q(m.value,h.value)),c("gapRemove",n.gap),u.value=null,h.value=null)}function ce(t){const n=t.currentTarget;S.value=null;const l=window.getSelection()?.rangeCount?window.getSelection().getRangeAt(0):null;l&&(w=T(n,l.endContainer,l.endOffset)),C({version:1,content:R(n)})}function ue(t){t.preventDefault();const n=window.getSelection()?.rangeCount?window.getSelection().getRangeAt(0):null;if(!n||!p.value)return;n.deleteContents();const l=window.document.createTextNode(t.clipboardData?.getData("text/plain")??"");n.insertNode(l),n.setStartAfter(l),n.collapse(!0),w=T(p.value,n.endContainer,n.endOffset),C({version:1,content:R(p.value)})}function de(t){t.clipboardData?.setData("text/plain",window.getSelection()?.toString()||O(m.value)),t.preventDefault()}function pe(t){const n=t.metaKey||t.ctrlKey;if(n&&t.key.toLocaleLowerCase()==="z"){t.preventDefault();const D=t.shiftKey?L.pop():F.pop();if(!D)return;(t.shiftKey?F:L).push(m.value),C(D,!1);return}if(t.key==="Enter"&&!t.altKey&&!n&&p.value){const D=window.getSelection()?.rangeCount?window.getSelection().getRangeAt(0):null;if(!D?.collapsed)return;const G=T(p.value,D.endContainer,D.endOffset);if(G===null)return;t.preventDefault(),w=G+1,C(le(G,`
|
|
5
|
+
`)),V();return}if(t.key!=="Backspace"&&t.key!=="Delete"){S.value=null;return}if(t.preventDefault(),!p.value)return;const l=t.key==="Backspace"?"backward":"forward",o=t.target instanceof HTMLElement?t.target.closest("[data-gap-id]"):null,d=o?.dataset.gapId?j(o.dataset.gapId):null,i=window.getSelection()?.rangeCount?window.getSelection().getRangeAt(0):null;if(!i)return;const f=T(p.value,i.startContainer,i.startOffset),v=T(p.value,i.endContainer,i.endOffset);if(f===null||v===null)return;const x=d??te(f,v,l);if(x){S.value===x.gap.id?(w=x.start,C(I(m.value,x.gap.id)),c("gapRemove",x.gap),S.value=null):S.value=x.gap.id;return}const N=O(m.value).length,z=f===v&&l==="backward"?Math.max(0,f-1):f,_=f===v&&l==="forward"?Math.min(N,v+1):v;_>z&&(w=z,C(ne(z,_)))}function me(t,n){u.value&&(u.value.options[t]=n)}function ge(t){u.value&&u.value.options.splice(t,1)}function X(){y.value&&M()}return e.onMounted(()=>window.addEventListener("scroll",X,!0)),e.onBeforeUnmount(()=>window.removeEventListener("scroll",X,!0)),(t,n)=>(e.openBlock(),e.createElementBlock("div",{ref_key:"shell",ref:k,class:e.normalizeClass(["fillable-editor",g.class])},[s.value&&y.value&&!u.value?(e.openBlock(),e.createElementBlock("button",{key:0,class:e.normalizeClass(["selection-toolbar",`selection-toolbar--${s.value.placement}`]),style:e.normalizeStyle({left:`${s.value.left}px`,top:`${s.value.top}px`}),"aria-label":`Turn ${y.value.text} into a gap`,onMousedown:oe},[...n[10]||(n[10]=[e.createElementVNode("span",{"aria-hidden":"true"},"+",-1),e.createTextVNode(" Add gap ",-1)])],46,we)):e.createCommentVNode("",!0),e.createElementVNode("div",{ref_key:"editor",ref:p,class:"fillable-editor__surface",contenteditable:!a.readOnly,spellcheck:"true",role:"textbox","aria-multiline":"true","aria-label":"Fillable text editor",onInput:ce,onSelect:M,onMouseup:M,onKeyup:M,onKeydown:pe,onPaste:ue,onCopy:de},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(m.value.content,(l,o)=>(e.openBlock(),e.createElementBlock(e.Fragment,{key:l.type==="gap"?l.id:`text-${o}`},[l.type==="text"?(e.openBlock(),e.createElementBlock("span",Te,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(l.text.split(`
|
|
6
|
+
`),(d,i)=>(e.openBlock(),e.createElementBlock(e.Fragment,{key:i},[i>0?(e.openBlock(),e.createElementBlock("br",Be)):e.createCommentVNode("",!0),e.createElementVNode("span",Se,e.toDisplayString(d||""),1)],64))),128))])):(e.openBlock(),e.createElementBlock("button",{key:1,type:"button",tabindex:a.readOnly?-1:0,contenteditable:"false","data-gap-id":l.id,"data-gap-length":l.originalText.length,"data-gap-original":l.originalText,class:e.normalizeClass(["editor-gap",`editor-gap--${l.control}`,{"editor-gap--keyboard-selected":S.value===l.id}]),"aria-label":`${l.control} gap: ${l.originalText}. Click to edit.`,onClick:d=>ie(l)},[e.createElementVNode("span",null,e.toDisplayString(l.originalText),1),l.control==="select"?(e.openBlock(),e.createElementBlock("svg",De,[...n[11]||(n[11]=[e.createElementVNode("path",{d:"m8 10 4 4 4-4"},null,-1)])])):e.createCommentVNode("",!0),e.createElementVNode("small",null,e.toDisplayString(l.control),1)],10,$e))],64))),128))],40,Ve),e.createElementVNode("div",Oe,[e.createElementVNode("span",null,e.toDisplayString(Z.value)+" characters",1),n[12]||(n[12]=e.createElementVNode("i",null,null,-1)),e.createElementVNode("span",null,e.toDisplayString(q.value)+" "+e.toDisplayString(q.value===1?"gap":"gaps"),1),a.readOnly?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("span",Me,"Select any text to make it fillable"))]),u.value?(e.openBlock(),e.createElementBlock("div",{key:1,class:"gap-dialog-backdrop",onMousedown:n[9]||(n[9]=e.withModifiers(l=>u.value=null,["self"]))},[e.createElementVNode("section",Ae,[e.createElementVNode("header",null,[e.createElementVNode("div",null,[e.createElementVNode("span",Fe,e.toDisplayString(h.value?"Gap settings":"New interactive field"),1),e.createElementVNode("h2",Le,e.toDisplayString(h.value?"Edit gap":"Create a gap"),1)]),e.createElementVNode("button",{class:"icon-button","aria-label":"Close",onClick:n[0]||(n[0]=l=>u.value=null)},"×")]),n[19]||(n[19]=e.createElementVNode("label",{class:"field-label"},"Field type",-1)),e.createElementVNode("div",Re,[e.createElementVNode("button",{role:"radio","aria-checked":u.value.control==="input",class:e.normalizeClass({active:u.value.control==="input"}),onClick:n[1]||(n[1]=l=>K("input"))},[...n[13]||(n[13]=[e.createElementVNode("span",{class:"type-icon"},"T",-1),e.createElementVNode("span",null,[e.createElementVNode("strong",null,"Text input"),e.createElementVNode("small",null,"Free-form response")],-1)])],10,ze),e.createElementVNode("button",{role:"radio","aria-checked":u.value.control==="select",class:e.normalizeClass({active:u.value.control==="select"}),onClick:n[2]||(n[2]=l=>K("select"))},[...n[14]||(n[14]=[e.createElementVNode("span",{class:"type-icon"},[e.createElementVNode("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},[e.createElementVNode("path",{d:"M6 7h8M6 12h8M6 17h8"}),e.createElementVNode("path",{d:"m17 9 2 3 2-3"})])],-1),e.createElementVNode("span",null,[e.createElementVNode("strong",null,"Select"),e.createElementVNode("small",null,"Choose from a list")],-1)])],10,Ge)]),u.value.control==="input"?(e.openBlock(),e.createElementBlock(e.Fragment,{key:0},[n[15]||(n[15]=e.createElementVNode("label",{class:"field-label",for:"original-text"},"Original text",-1)),e.withDirectives(e.createElementVNode("input",{id:"original-text","onUpdate:modelValue":n[3]||(n[3]=l=>u.value.originalText=l),class:"dialog-input"},null,512),[[e.vModelText,u.value.originalText]]),n[16]||(n[16]=e.createElementVNode("label",{class:"field-label",for:"placeholder"},"Placeholder",-1)),e.withDirectives(e.createElementVNode("input",{id:"placeholder","onUpdate:modelValue":n[4]||(n[4]=l=>u.value.placeholder=l),class:"dialog-input"},null,512),[[e.vModelText,u.value.placeholder]])],64)):(e.openBlock(),e.createElementBlock("div",Ie,[n[17]||(n[17]=e.createElementVNode("label",{class:"field-label"},[e.createTextVNode("Options "),e.createElementVNode("span",null,"Minimum 2")],-1)),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(u.value.options,(l,o)=>(e.openBlock(),e.createElementBlock("div",{key:o,class:"option-row"},[e.createElementVNode("span",{class:e.normalizeClass(["option-correct-marker",{"option-correct-marker--empty":o!==0}])},"✓",2),e.createElementVNode("input",{class:"dialog-input",value:l,placeholder:o===0?"Original text":"Type an option","aria-label":`Option ${o+1}`,onInput:d=>me(o,d.target.value)},null,40,Ue),e.createElementVNode("button",{class:"icon-button small","aria-label":`Remove option ${o+1}`,onClick:d=>ge(o)},"×",8,qe)]))),128)),e.createElementVNode("button",{class:"add-option",onClick:n[5]||(n[5]=l=>u.value.options.push(""))},"+ Add option")])),E.value?(e.openBlock(),e.createElementBlock("p",je,e.toDisplayString(E.value),1)):e.createCommentVNode("",!0),e.createElementVNode("footer",null,[h.value?(e.openBlock(),e.createElementBlock("div",Pe,[e.createElementVNode("button",{onClick:n[6]||(n[6]=l=>H(!1))},"Convert to text"),e.createElementVNode("button",{onClick:n[7]||(n[7]=l=>H(!0))},"Delete")])):e.createCommentVNode("",!0),n[18]||(n[18]=e.createElementVNode("span",null,null,-1)),e.createElementVNode("button",{class:"button secondary",onClick:n[8]||(n[8]=l=>u.value=null)},"Cancel"),e.createElementVNode("button",{class:"button primary",onClick:se},e.toDisplayString(h.value?"Save changes":"Create gap"),1)])])],32)):e.createCommentVNode("",!0)],2))}});exports.EMPTY_DOCUMENT=be;exports.FillableText=ke;exports.FillableTextEditor=Ke;exports.createGap=Y;exports.deleteGap=I;exports.deserialize=Ce;exports.normalizeContent=B;exports.removeGap=Q;exports.serialize=Ee;exports.slugifyOption=U;exports.toPlainText=O;exports.updateGap=W;exports.validateDocument=J;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.fillable-text,.fillable-editor{--fillable-accent: #f0644d;--fillable-accent-dark: #d94e38;--fillable-ink: #17221f;--fillable-muted: #64716d;--fillable-surface: #fffefa;--fillable-line: #d9ded8;--fillable-gap-border: #ee765f;--fillable-gap-background: #fff2ed;--fillable-gap-radius: 8px}.fillable-text *,.fillable-editor *,.gap-dialog-backdrop *{box-sizing:border-box}.fillable-text{font-family:Georgia,serif;font-size:20px;line-height:2.35;white-space:pre-wrap}.fillable-text__input,.fillable-text__select{border:0;outline:0;background:transparent;color:#35413d;font:700 12px ui-sans-serif,system-ui,sans-serif}.fillable-text__input{min-width:100px;margin:0 3px;padding:4px 5px 3px;border-bottom:1.5px solid #b8c0bc;border-radius:5px 5px 0 0;background:#f3f4f1}.fillable-text__input::placeholder{color:#89928e;font-weight:500}.fillable-text__input:focus{background:#f8f9f7;border-bottom-color:#6f7c77;box-shadow:0 2px #6f7c77}.fillable-text__select-wrap{position:relative;display:inline-flex;margin:0 4px;border:1px solid #c3cac6;border-radius:6px;background:#f3f4f1;color:#6f7c77}.fillable-text__select{padding:6px 29px 6px 9px;appearance:none;cursor:pointer}.fillable-text__select-wrap:after{content:"";position:absolute;top:50%;right:11px;width:7px;height:7px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:translateY(-70%) rotate(45deg);pointer-events:none}.fillable-text__select-wrap:focus-within{background:#f8f9f7;border-color:#7b8782;box-shadow:0 0 0 3px #4c58531a}.fillable-text__input[data-state=success],.fillable-text__select-wrap[data-state=success]{color:#267454;background:#edf8f2;border-color:#4b9b78}.fillable-text__input[data-state=error],.fillable-text__select-wrap[data-state=error]{color:#a74235;background:#fff0ed;border-color:#d66556}.fillable-text__input:disabled,.fillable-text__select:disabled{cursor:not-allowed;opacity:.58}.fillable-editor{position:relative;display:flex;width:100%;flex-direction:column}.fillable-editor__surface{min-height:180px;outline:none;white-space:pre-wrap;caret-color:var(--fillable-accent);font:23px/2.15 Georgia,serif}.fillable-editor__surface:focus{margin-left:-15px;padding-left:12px;box-shadow:inset 3px 0 #f2b4a7}.editor-gap{position:relative;display:inline-flex;align-items:center;gap:6px;margin:0 4px;padding:4px 9px;border:1.5px solid var(--fillable-gap-border);border-radius:var(--fillable-gap-radius);background:var(--fillable-gap-background);color:#9d3c2b;box-shadow:0 2px #df583f1f;vertical-align:middle;cursor:pointer;font:700 13px/1.4 ui-sans-serif,system-ui,sans-serif}.editor-gap:hover,.editor-gap:focus-visible{outline:3px solid rgba(240,100,77,.16);background:#ffe7de}.editor-gap__chevron{width:14px;height:14px;flex:none}.editor-gap small{position:absolute;top:calc(100% + 5px);left:50%;color:#b2786d;transform:translate(-50%);font-size:7px;letter-spacing:.12em;text-transform:uppercase}.editor-gap--keyboard-selected,.editor-gap--keyboard-selected:hover{outline:3px solid rgba(240,100,77,.28);background:#ffe1d7;box-shadow:0 0 0 1px var(--fillable-accent)}.editor-meta{display:flex;align-items:center;gap:8px;margin-top:auto;padding-top:18px;border-top:1px dashed #d8dcd7;color:#909995;font-size:9px}.editor-meta i{width:3px;height:3px;border-radius:50%;background:#a9b1ae}.editor-hint{margin-left:auto;color:#78837f}.selection-toolbar{position:absolute;z-index:12;display:flex;align-items:center;gap:5px;padding:9px 12px;border:0;border-radius:7px;background:var(--fillable-ink);color:#fff;box-shadow:0 8px 22px #17221f40;transform:translate(-50%);cursor:pointer;font-size:10px;font-weight:750}.selection-toolbar:after{content:"";position:absolute;top:100%;left:50%;border:5px solid transparent;border-top-color:var(--fillable-ink);transform:translate(-50%)}.selection-toolbar--below:after{top:auto;bottom:100%;border-top-color:transparent;border-bottom-color:var(--fillable-ink)}.gap-dialog-backdrop{position:fixed;inset:0;z-index:100;display:grid;place-items:center;padding:20px;background:#111d1a94;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.gap-dialog{width:min(520px,100%);max-height:calc(100vh - 40px);overflow:auto;padding:25px;border-radius:13px;background:#fffefa;color:#17221f;box-shadow:0 24px 80px #00000042;font-family:ui-sans-serif,system-ui,sans-serif}.gap-dialog header{display:flex;align-items:start;justify-content:space-between;margin-bottom:24px}.gap-dialog h2{margin:4px 0 0;font:500 30px Georgia,serif;letter-spacing:-.035em}.gap-dialog .eyebrow{color:#65716d;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}.gap-dialog button,.gap-dialog input{font:inherit}.gap-dialog .icon-button{display:grid;place-items:center;padding:6px;border:0;background:transparent;color:#7d8884;cursor:pointer}.gap-dialog .field-label{display:flex;justify-content:space-between;margin:18px 0 7px;color:#61706b;font-size:10px;font-weight:800;letter-spacing:.1em;text-transform:uppercase}.gap-dialog .field-label span{color:#9ca5a2;font-size:10px;font-weight:500;letter-spacing:.02em;text-transform:none}.gap-dialog .dialog-input{width:100%;height:42px;padding:0 12px;border:1px solid #cfd5d0;border-radius:7px;outline:none;background:#fff;font-size:12px}.gap-dialog .dialog-input:focus{border-color:var(--fillable-accent);box-shadow:0 0 0 3px #f0644d1c}.gap-dialog .segmented{display:grid;grid-template-columns:1fr 1fr;gap:9px}.gap-dialog .segmented button{display:flex;align-items:center;gap:10px;padding:11px;border:1px solid #d2d7d2;border-radius:8px;background:#fff;text-align:left;cursor:pointer}.gap-dialog .segmented button.active{border-color:var(--fillable-accent);background:#fff5f1;box-shadow:inset 0 0 0 1px var(--fillable-accent)}.gap-dialog .segmented strong,.gap-dialog .segmented small{display:block}.gap-dialog .segmented strong{font-size:11px}.gap-dialog .segmented small{margin-top:2px;color:#85908c;font-size:9px}.gap-dialog .type-icon{display:grid;width:30px;height:30px;place-items:center;border-radius:6px;background:#f0f2ee;font-family:Georgia,serif;font-weight:700}.gap-dialog .type-icon svg{display:block;width:16px;height:16px}.gap-dialog .segmented .active .type-icon{background:#fadde5;color:var(--fillable-accent-dark)}.gap-dialog .option-row{display:grid;grid-template-columns:24px 1fr 28px;align-items:center;gap:7px;margin-bottom:7px}.gap-dialog .option-correct-marker{display:grid;width:22px;height:22px;place-items:center;border-radius:50%;background:#e9f6ef;color:#267454}.gap-dialog .option-correct-marker--empty{visibility:hidden}.gap-dialog .add-option{display:flex;align-items:center;gap:4px;padding:5px 0;border:0;background:none;color:var(--fillable-accent-dark);cursor:pointer;font-size:10px;font-weight:750}.gap-dialog .form-error{padding:9px 11px;border-radius:6px;background:#fff0eb;color:#bd3c2b;font-size:10px}.gap-dialog footer{display:flex;align-items:center;gap:8px;margin-top:24px;padding-top:18px;border-top:1px solid #e1e4df}.gap-dialog footer>span{flex:1}.gap-dialog .button{padding:9px 13px;border:1px solid #ccd2cc;border-radius:7px;background:#fff;cursor:pointer;font-size:10px;font-weight:750}.gap-dialog .button.primary{border-color:var(--fillable-accent);background:var(--fillable-accent);color:#fff}.gap-dialog .button.primary:hover{background:var(--fillable-accent-dark)}.gap-dialog .danger-actions{display:flex;gap:4px}.gap-dialog .danger-actions button{padding:6px;border:0;background:none;color:#a24638;cursor:pointer;font-size:9px}@media(max-width:560px){.fillable-editor__surface{font-size:20px}.editor-hint{display:none}.gap-dialog footer{flex-wrap:wrap}.gap-dialog .danger-actions{width:100%}}
|
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { defineComponent as ue, computed as z, openBlock as h, createElementBlock as x, normalizeClass as L, Fragment as D, renderList as I, toDisplayString as T, createBlock as ne, resolveDynamicComponent as le, createElementVNode as i, ref as O, watch as Se, onMounted as Ee, onBeforeUnmount as Oe, normalizeStyle as De, createTextVNode as ae, createCommentVNode as A, withModifiers as Ne, withDirectives as oe, vModelText as ie, nextTick as Me } from "vue";
|
|
2
|
+
const Ve = { key: 0 }, Ae = ["data-state", "aria-invalid", "aria-label", "value", "disabled", "placeholder", "size", "onInput"], Le = ["data-state"], Re = ["aria-invalid", "aria-label", "value", "disabled", "onChange"], Fe = ["value"], ut = /* @__PURE__ */ ue({
|
|
3
|
+
__name: "FillableText",
|
|
4
|
+
props: {
|
|
5
|
+
document: {},
|
|
6
|
+
modelValue: {},
|
|
7
|
+
components: {},
|
|
8
|
+
disabled: { type: Boolean, default: !1 },
|
|
9
|
+
class: { default: "" },
|
|
10
|
+
gapStates: {},
|
|
11
|
+
getGapState: {}
|
|
12
|
+
},
|
|
13
|
+
emits: ["update:modelValue", "gapChange"],
|
|
14
|
+
setup(l, { emit: r }) {
|
|
15
|
+
const g = l, u = r, C = z(() => ["fillable-text", g.class]);
|
|
16
|
+
function f(d, b) {
|
|
17
|
+
u("update:modelValue", { ...g.modelValue, [d]: b }), u("gapChange", d, b);
|
|
18
|
+
}
|
|
19
|
+
function k(d) {
|
|
20
|
+
return g.getGapState?.(d) ?? g.gapStates?.[d.id] ?? "default";
|
|
21
|
+
}
|
|
22
|
+
return (d, b) => (h(), x("div", {
|
|
23
|
+
class: L(C.value)
|
|
24
|
+
}, [
|
|
25
|
+
(h(!0), x(D, null, I(l.document.content, (s, c) => (h(), x(D, {
|
|
26
|
+
key: s.type === "gap" ? s.id : `text-${c}`
|
|
27
|
+
}, [
|
|
28
|
+
s.type === "text" ? (h(), x("span", Ve, T(s.text), 1)) : s.control === "input" && l.components?.input ? (h(), ne(le(l.components?.input), {
|
|
29
|
+
key: 1,
|
|
30
|
+
id: s.id,
|
|
31
|
+
"model-value": l.modelValue[s.id] ?? "",
|
|
32
|
+
disabled: l.disabled,
|
|
33
|
+
placeholder: s.placeholder ?? "Type answer",
|
|
34
|
+
"aria-label": `Fill in ${s.originalText}`,
|
|
35
|
+
state: k(s),
|
|
36
|
+
"onUpdate:modelValue": (y) => f(s.id, y)
|
|
37
|
+
}, null, 8, ["id", "model-value", "disabled", "placeholder", "aria-label", "state", "onUpdate:modelValue"])) : s.control === "input" ? (h(), x("input", {
|
|
38
|
+
key: 2,
|
|
39
|
+
class: "fillable-text__input",
|
|
40
|
+
"data-state": k(s),
|
|
41
|
+
"aria-invalid": k(s) === "error" || void 0,
|
|
42
|
+
"aria-label": `Fill in ${s.originalText}`,
|
|
43
|
+
value: l.modelValue[s.id] ?? "",
|
|
44
|
+
disabled: l.disabled,
|
|
45
|
+
placeholder: s.placeholder ?? "Type answer",
|
|
46
|
+
size: Math.max(8, (l.modelValue[s.id] ?? "").length || (s.placeholder ?? "").length || 8),
|
|
47
|
+
onInput: (y) => f(s.id, y.target.value)
|
|
48
|
+
}, null, 40, Ae)) : l.components?.select ? (h(), ne(le(l.components?.select), {
|
|
49
|
+
key: 3,
|
|
50
|
+
id: s.id,
|
|
51
|
+
"model-value": l.modelValue[s.id] ?? "",
|
|
52
|
+
disabled: l.disabled,
|
|
53
|
+
options: s.options,
|
|
54
|
+
"aria-label": `Fill in ${s.originalText}`,
|
|
55
|
+
state: k(s),
|
|
56
|
+
"onUpdate:modelValue": (y) => f(s.id, y)
|
|
57
|
+
}, null, 8, ["id", "model-value", "disabled", "options", "aria-label", "state", "onUpdate:modelValue"])) : (h(), x("span", {
|
|
58
|
+
key: 4,
|
|
59
|
+
class: "fillable-text__select-wrap",
|
|
60
|
+
"data-state": k(s)
|
|
61
|
+
}, [
|
|
62
|
+
i("select", {
|
|
63
|
+
class: "fillable-text__select",
|
|
64
|
+
"aria-invalid": k(s) === "error" || void 0,
|
|
65
|
+
"aria-label": `Fill in ${s.originalText}`,
|
|
66
|
+
value: l.modelValue[s.id] ?? "",
|
|
67
|
+
disabled: l.disabled,
|
|
68
|
+
onChange: (y) => f(s.id, y.target.value)
|
|
69
|
+
}, [
|
|
70
|
+
b[0] || (b[0] = i("option", {
|
|
71
|
+
value: "",
|
|
72
|
+
disabled: ""
|
|
73
|
+
}, "Select an option", -1)),
|
|
74
|
+
(h(!0), x(D, null, I(s.options, (y) => (h(), x("option", {
|
|
75
|
+
key: y.id,
|
|
76
|
+
value: y.value
|
|
77
|
+
}, T(y.label), 9, Fe))), 128))
|
|
78
|
+
], 40, Re)
|
|
79
|
+
], 8, Le))
|
|
80
|
+
], 64))), 128))
|
|
81
|
+
], 2));
|
|
82
|
+
}
|
|
83
|
+
}), ct = { version: 1, content: [{ type: "text", text: "" }] };
|
|
84
|
+
function G(l) {
|
|
85
|
+
const r = [];
|
|
86
|
+
for (const g of l)
|
|
87
|
+
if (g.type === "text") {
|
|
88
|
+
if (!g.text) continue;
|
|
89
|
+
const u = r.at(-1);
|
|
90
|
+
u?.type === "text" ? u.text += g.text : r.push({ ...g });
|
|
91
|
+
} else r.push(g);
|
|
92
|
+
return r.length ? r : [{ type: "text", text: "" }];
|
|
93
|
+
}
|
|
94
|
+
function j(l) {
|
|
95
|
+
return l.content.map((r) => r.type === "text" ? r.text : r.originalText).join("");
|
|
96
|
+
}
|
|
97
|
+
function pt(l) {
|
|
98
|
+
return JSON.stringify(l, null, 2);
|
|
99
|
+
}
|
|
100
|
+
function dt(l) {
|
|
101
|
+
const r = typeof l == "string" ? JSON.parse(l) : l, g = Be(r);
|
|
102
|
+
if (!g.valid) throw new Error(g.errors.map((u) => `${u.path}: ${u.message}`).join("; "));
|
|
103
|
+
return r;
|
|
104
|
+
}
|
|
105
|
+
function q(l) {
|
|
106
|
+
return typeof l == "object" && l !== null && !Array.isArray(l);
|
|
107
|
+
}
|
|
108
|
+
function Be(l) {
|
|
109
|
+
const r = [];
|
|
110
|
+
if (!q(l)) return { valid: !1, errors: [{ path: "", message: "Document must be an object" }] };
|
|
111
|
+
if (l.version !== 1 && r.push({ path: "version", message: "Only schema version 1 is supported" }), !Array.isArray(l.content)) return { valid: !1, errors: [...r, { path: "content", message: "Content must be an array" }] };
|
|
112
|
+
const g = /* @__PURE__ */ new Set();
|
|
113
|
+
return l.content.forEach((u, C) => {
|
|
114
|
+
const f = `content[${C}]`;
|
|
115
|
+
if (!q(u) || u.type !== "text" && u.type !== "gap") return void r.push({ path: f, message: "Unknown node type" });
|
|
116
|
+
if (u.type === "text") {
|
|
117
|
+
typeof u.text != "string" && r.push({ path: `${f}.text`, message: "Text must be a string" });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (typeof u.id != "string" || !u.id.trim() ? r.push({ path: `${f}.id`, message: "Gap id is required" }) : g.has(u.id) ? r.push({ path: `${f}.id`, message: "Gap id must be unique" }) : g.add(u.id), (typeof u.originalText != "string" || !u.originalText) && r.push({ path: `${f}.originalText`, message: "Original text is required" }), u.control !== "input" && u.control !== "select" && r.push({ path: `${f}.control`, message: "Control must be input or select" }), u.control === "select")
|
|
121
|
+
if (!Array.isArray(u.options) || u.options.length < 2) r.push({ path: `${f}.options`, message: "Select gap requires at least 2 options" });
|
|
122
|
+
else {
|
|
123
|
+
const k = /* @__PURE__ */ new Set();
|
|
124
|
+
u.options.forEach((d, b) => {
|
|
125
|
+
const s = `${f}.options[${b}]`;
|
|
126
|
+
(!q(d) || typeof d.label != "string" || !d.label.trim()) && r.push({ path: `${s}.label`, message: "Option label is required" }), !q(d) || typeof d.value != "string" || !d.value.trim() ? r.push({ path: `${s}.value`, message: "Option value is required" }) : k.has(d.value) ? r.push({ path: `${s}.value`, message: "Option value must be unique" }) : k.add(d.value);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}), { valid: r.length === 0, errors: r };
|
|
130
|
+
}
|
|
131
|
+
function Ge(l) {
|
|
132
|
+
return l.type === "text" ? l.text.length : l.originalText.length;
|
|
133
|
+
}
|
|
134
|
+
function Ie(l, r, g) {
|
|
135
|
+
if (r.from < 0 || r.to <= r.from || r.to > j(l).length) throw new Error("Selection is outside the document");
|
|
136
|
+
let u = 0;
|
|
137
|
+
const C = [];
|
|
138
|
+
let f = !1;
|
|
139
|
+
for (const k of l.content) {
|
|
140
|
+
const d = u, b = d + Ge(k);
|
|
141
|
+
if (u = b, r.to <= d || r.from >= b) {
|
|
142
|
+
C.push(k);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (k.type === "gap") throw new Error("A selection cannot overlap an existing gap");
|
|
146
|
+
if (r.from < d || r.to > b) throw new Error("A gap must be created inside one text node");
|
|
147
|
+
const s = r.from - d, c = r.to - d;
|
|
148
|
+
s && C.push({ type: "text", text: k.text.slice(0, s) }), C.push({ type: "gap", ...g, originalText: k.text.slice(s, c) }), c < k.text.length && C.push({ type: "text", text: k.text.slice(c) }), f = !0;
|
|
149
|
+
}
|
|
150
|
+
if (!f) throw new Error("No editable text selected");
|
|
151
|
+
return { version: 1, content: G(C) };
|
|
152
|
+
}
|
|
153
|
+
function Ue(l, r, g) {
|
|
154
|
+
return { version: 1, content: l.content.map((u) => u.type === "gap" && u.id === r ? g : u) };
|
|
155
|
+
}
|
|
156
|
+
function qe(l, r) {
|
|
157
|
+
return { version: 1, content: G(l.content.map((g) => g.type === "gap" && g.id === r ? { type: "text", text: g.originalText } : g)) };
|
|
158
|
+
}
|
|
159
|
+
function re(l, r) {
|
|
160
|
+
return { version: 1, content: G(l.content.filter((g) => g.type !== "gap" || g.id !== r)) };
|
|
161
|
+
}
|
|
162
|
+
function se(l) {
|
|
163
|
+
return l.trim().toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-|-$/g, "");
|
|
164
|
+
}
|
|
165
|
+
const ze = ["aria-label"], je = ["contenteditable"], Ke = {
|
|
166
|
+
key: 0,
|
|
167
|
+
"data-text": ""
|
|
168
|
+
}, Pe = {
|
|
169
|
+
key: 0,
|
|
170
|
+
"data-line-break": ""
|
|
171
|
+
}, He = { "data-line": "" }, Xe = ["tabindex", "data-gap-id", "data-gap-length", "data-gap-original", "aria-label", "onClick"], Je = {
|
|
172
|
+
key: 0,
|
|
173
|
+
class: "editor-gap__chevron",
|
|
174
|
+
viewBox: "0 0 24 24",
|
|
175
|
+
fill: "none",
|
|
176
|
+
stroke: "currentColor",
|
|
177
|
+
"stroke-width": "2",
|
|
178
|
+
"stroke-linecap": "round",
|
|
179
|
+
"stroke-linejoin": "round",
|
|
180
|
+
"aria-hidden": "true"
|
|
181
|
+
}, _e = {
|
|
182
|
+
class: "editor-meta",
|
|
183
|
+
"aria-live": "polite"
|
|
184
|
+
}, We = {
|
|
185
|
+
key: 0,
|
|
186
|
+
class: "editor-hint"
|
|
187
|
+
}, Ye = {
|
|
188
|
+
class: "gap-dialog",
|
|
189
|
+
role: "dialog",
|
|
190
|
+
"aria-modal": "true",
|
|
191
|
+
"aria-labelledby": "gap-dialog-title"
|
|
192
|
+
}, Qe = { class: "eyebrow" }, Ze = { id: "gap-dialog-title" }, et = {
|
|
193
|
+
class: "segmented",
|
|
194
|
+
role: "radiogroup",
|
|
195
|
+
"aria-label": "Gap type"
|
|
196
|
+
}, tt = ["aria-checked"], nt = ["aria-checked"], lt = {
|
|
197
|
+
key: 1,
|
|
198
|
+
class: "options-block"
|
|
199
|
+
}, at = ["value", "placeholder", "aria-label", "onInput"], ot = ["aria-label", "onClick"], it = {
|
|
200
|
+
key: 2,
|
|
201
|
+
class: "form-error",
|
|
202
|
+
role: "alert"
|
|
203
|
+
}, rt = {
|
|
204
|
+
key: 0,
|
|
205
|
+
class: "danger-actions"
|
|
206
|
+
}, ft = /* @__PURE__ */ ue({
|
|
207
|
+
__name: "FillableTextEditor",
|
|
208
|
+
props: {
|
|
209
|
+
modelValue: {},
|
|
210
|
+
defaultValue: { default: () => ({ version: 1, content: [{ type: "text", text: "Start writing…" }] }) },
|
|
211
|
+
readOnly: { type: Boolean, default: !1 },
|
|
212
|
+
class: { default: "" }
|
|
213
|
+
},
|
|
214
|
+
emits: ["update:modelValue", "change", "gapCreate", "gapUpdate", "gapRemove", "selectionChange"],
|
|
215
|
+
setup(l, { emit: r }) {
|
|
216
|
+
const g = l, u = r, C = O(g.defaultValue), f = z(() => g.modelValue ?? C.value), k = O(), d = O(), b = O(null), s = O(null), c = O(null), y = O(null), $ = O(""), R = O(null), K = [], P = [];
|
|
217
|
+
let N = null;
|
|
218
|
+
const ce = z(() => f.value.content.reduce((e, t) => e + F(t), 0)), _ = z(() => f.value.content.filter((e) => e.type === "gap").length);
|
|
219
|
+
function pe(e) {
|
|
220
|
+
return `${e}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
221
|
+
}
|
|
222
|
+
function F(e) {
|
|
223
|
+
return e.type === "text" ? e.text.length : e.originalText.length;
|
|
224
|
+
}
|
|
225
|
+
function S(e, t = !0) {
|
|
226
|
+
t && (K.push(f.value), P.length = 0), g.modelValue === void 0 && (C.value = e), u("update:modelValue", e), u("change", e);
|
|
227
|
+
}
|
|
228
|
+
function M() {
|
|
229
|
+
b.value = null, s.value = null, u("selectionChange", null);
|
|
230
|
+
}
|
|
231
|
+
function W(e) {
|
|
232
|
+
let t = 0;
|
|
233
|
+
for (const n of f.value.content) {
|
|
234
|
+
const a = t, p = a + F(n);
|
|
235
|
+
if (t = p, n.type === "gap" && n.id === e) return { gap: n, start: a, end: p };
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function de(e, t, n) {
|
|
240
|
+
let a = 0;
|
|
241
|
+
const p = [];
|
|
242
|
+
for (const o of f.value.content) {
|
|
243
|
+
const v = a, m = v + F(o);
|
|
244
|
+
if (a = m, o.type !== "gap") continue;
|
|
245
|
+
(e === t && (n === "backward" ? m === e : v === e) || e < t && e < m && t > v) && p.push({ gap: o, start: v, end: m });
|
|
246
|
+
}
|
|
247
|
+
return n === "backward" ? p.at(-1) ?? null : p[0] ?? null;
|
|
248
|
+
}
|
|
249
|
+
function fe(e, t) {
|
|
250
|
+
let n = 0;
|
|
251
|
+
const a = [];
|
|
252
|
+
for (const p of f.value.content) {
|
|
253
|
+
const o = n;
|
|
254
|
+
if (n = o + F(p), p.type === "gap") {
|
|
255
|
+
a.push(p);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const m = Math.max(0, e - o), w = Math.min(p.text.length, t - o);
|
|
259
|
+
a.push(m < w ? { type: "text", text: p.text.slice(0, m) + p.text.slice(w) } : p);
|
|
260
|
+
}
|
|
261
|
+
return { version: 1, content: G(a) };
|
|
262
|
+
}
|
|
263
|
+
function ge(e, t) {
|
|
264
|
+
let n = 0, a = !1;
|
|
265
|
+
const p = [];
|
|
266
|
+
for (const o of f.value.content) {
|
|
267
|
+
const v = n, m = v + F(o);
|
|
268
|
+
if (n = m, !a && o.type === "text" && e >= v && e <= m) {
|
|
269
|
+
const w = e - v;
|
|
270
|
+
p.push({ type: "text", text: o.text.slice(0, w) }, { type: "text", text: t }, { type: "text", text: o.text.slice(w) }), a = !0;
|
|
271
|
+
} else p.push(o);
|
|
272
|
+
}
|
|
273
|
+
return a || p.push({ type: "text", text: t }), { version: 1, content: G(p) };
|
|
274
|
+
}
|
|
275
|
+
function H(e) {
|
|
276
|
+
const t = new Map(f.value.content.filter((o) => o.type === "gap").map((o) => [o.id, o])), n = [], a = (o) => {
|
|
277
|
+
const v = o.replaceAll("", "");
|
|
278
|
+
if (!v) return;
|
|
279
|
+
const m = n.at(-1);
|
|
280
|
+
m?.type === "text" ? m.text += v : n.push({ type: "text", text: v });
|
|
281
|
+
}, p = (o) => {
|
|
282
|
+
if (o instanceof HTMLElement) {
|
|
283
|
+
if (o.dataset.gapId) {
|
|
284
|
+
const v = t.get(o.dataset.gapId);
|
|
285
|
+
v ? n.push(v) : a(o.dataset.gapOriginal ?? "");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (o.tagName === "BR") {
|
|
289
|
+
a(`
|
|
290
|
+
`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (o.tagName === "DIV" || o.tagName === "P") {
|
|
294
|
+
const v = n.at(-1);
|
|
295
|
+
n.length && !(v?.type === "text" && v.text.endsWith(`
|
|
296
|
+
`)) && a(`
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (o.nodeType === Node.TEXT_NODE) {
|
|
301
|
+
a(o.textContent ?? "");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
o.childNodes.forEach(p);
|
|
305
|
+
};
|
|
306
|
+
return e.childNodes.forEach(p), G(n);
|
|
307
|
+
}
|
|
308
|
+
function Y(e) {
|
|
309
|
+
return e.nodeType === Node.TEXT_NODE ? e.textContent?.replaceAll("", "").length ?? 0 : e instanceof HTMLElement && e.dataset.gapLength !== void 0 ? Number(e.dataset.gapLength) : e instanceof HTMLElement && e.tagName === "BR" ? 1 : Array.from(e.childNodes).reduce((t, n) => t + Y(n), 0);
|
|
310
|
+
}
|
|
311
|
+
function V(e, t, n) {
|
|
312
|
+
if (t !== e && !e.contains(t)) return null;
|
|
313
|
+
const a = e.ownerDocument.createRange();
|
|
314
|
+
return a.selectNodeContents(e), a.setEnd(t, n), H(a.cloneContents()).reduce((p, o) => p + F(o), 0);
|
|
315
|
+
}
|
|
316
|
+
function ve(e, t) {
|
|
317
|
+
let n = t, a = null;
|
|
318
|
+
const p = (m) => {
|
|
319
|
+
if (!a) {
|
|
320
|
+
if (m.nodeType === Node.TEXT_NODE) {
|
|
321
|
+
const w = m.textContent ?? "", E = w.replaceAll("", "").length;
|
|
322
|
+
n <= E ? a = { node: m, offset: E ? n : w.length } : n -= E;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (m instanceof HTMLElement && (m.tagName === "BR" || m.dataset.gapLength !== void 0)) {
|
|
326
|
+
const w = Y(m), E = m.parentNode;
|
|
327
|
+
n <= w && E ? a = { node: E, offset: Array.from(E.childNodes).indexOf(m) + (n ? 1 : 0) } : n -= w;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
m.childNodes.forEach(p);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
p(e);
|
|
334
|
+
const o = a ?? { node: e, offset: e.childNodes.length }, v = e.ownerDocument.createRange();
|
|
335
|
+
v.setStart(o.node, o.offset), v.collapse(!0), e.ownerDocument.getSelection()?.removeAllRanges(), e.ownerDocument.getSelection()?.addRange(v);
|
|
336
|
+
}
|
|
337
|
+
Se(f, async () => {
|
|
338
|
+
N !== null && (await Me(), d.value && ve(d.value, N), N = null);
|
|
339
|
+
});
|
|
340
|
+
function U() {
|
|
341
|
+
if (g.readOnly || !d.value || !k.value) return;
|
|
342
|
+
const e = window.getSelection();
|
|
343
|
+
if (!e || !e.rangeCount || e.isCollapsed) return M();
|
|
344
|
+
const t = e.getRangeAt(0);
|
|
345
|
+
if (!d.value.contains(t.commonAncestorContainer)) return M();
|
|
346
|
+
const n = V(d.value, t.startContainer, t.startOffset), a = V(d.value, t.endContainer, t.endOffset);
|
|
347
|
+
if (n === null || a === null || a <= n) return M();
|
|
348
|
+
const p = j(f.value).slice(n, a);
|
|
349
|
+
if (!p.trim()) return M();
|
|
350
|
+
const o = t.getBoundingClientRect(), v = k.value.getBoundingClientRect(), m = o.top - v.top - 52, w = m < 8 ? "below" : "above";
|
|
351
|
+
b.value = { text: p, from: n, to: a }, s.value = { left: Math.min(Math.max(o.left - v.left + o.width / 2, 76), v.width - 76), top: w === "below" ? o.bottom - v.top + 10 : m, placement: w }, u("selectionChange", b.value);
|
|
352
|
+
}
|
|
353
|
+
function me(e) {
|
|
354
|
+
e.preventDefault(), b.value && (y.value = null, c.value = { id: pe("gap"), originalText: b.value.text, control: "input", placeholder: `Enter ${b.value.text.toLocaleLowerCase()}…`, options: [b.value.text, ""] }, $.value = "");
|
|
355
|
+
}
|
|
356
|
+
function he(e) {
|
|
357
|
+
g.readOnly || (y.value = e.id, c.value = { id: e.id, originalText: e.originalText, control: e.control, placeholder: e.control === "input" ? e.placeholder ?? "" : "", options: e.control === "select" ? e.options.map((t) => t.label) : [e.originalText, ""] }, $.value = "", M());
|
|
358
|
+
}
|
|
359
|
+
function Q(e) {
|
|
360
|
+
if (!(!c.value || c.value.control === e)) {
|
|
361
|
+
if (e === "select") {
|
|
362
|
+
const t = [...c.value.options];
|
|
363
|
+
t[0] = c.value.originalText, t.length < 2 && t.push(""), c.value = { ...c.value, control: e, options: t };
|
|
364
|
+
} else c.value = { ...c.value, control: e, originalText: c.value.options[0]?.trim() || c.value.originalText };
|
|
365
|
+
$.value = "";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function xe(e) {
|
|
369
|
+
if (e.control === "input") return { type: "gap", id: e.id, control: "input", originalText: e.originalText, placeholder: e.placeholder.trim() || void 0 };
|
|
370
|
+
const t = e.options.map((n, a) => ({ id: `${e.id}-option-${a + 1}`, label: n.trim(), value: se(n) || `option-${a + 1}` }));
|
|
371
|
+
return { type: "gap", id: e.id, control: "select", originalText: t[0].label, options: t };
|
|
372
|
+
}
|
|
373
|
+
function ye() {
|
|
374
|
+
if (!c.value) return;
|
|
375
|
+
let e = c.value;
|
|
376
|
+
if (e.control === "input" && !e.originalText.trim()) return void ($.value = "Original text is required.");
|
|
377
|
+
if (e.control === "select") {
|
|
378
|
+
if (!e.options[0]?.trim()) return void ($.value = "The first option must contain the original text.");
|
|
379
|
+
const n = e.options.map((p) => p.trim()).filter(Boolean), a = n.map(se);
|
|
380
|
+
if (n.length < 2) return void ($.value = "Add at least two non-empty options.");
|
|
381
|
+
if (new Set(a).size !== a.length) return void ($.value = "Every option needs a unique value.");
|
|
382
|
+
e = { ...e, options: n };
|
|
383
|
+
}
|
|
384
|
+
const t = xe(e);
|
|
385
|
+
try {
|
|
386
|
+
y.value ? (S(Ue(f.value, y.value, t)), u("gapUpdate", t)) : b.value && (S(Ie(f.value, b.value, t)), u("gapCreate", t)), c.value = null, M();
|
|
387
|
+
} catch (n) {
|
|
388
|
+
$.value = n instanceof Error ? n.message : "Could not create gap.";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function Z(e) {
|
|
392
|
+
if (!y.value) return;
|
|
393
|
+
const t = W(y.value);
|
|
394
|
+
t && (S(e ? re(f.value, y.value) : qe(f.value, y.value)), u("gapRemove", t.gap), c.value = null, y.value = null);
|
|
395
|
+
}
|
|
396
|
+
function be(e) {
|
|
397
|
+
const t = e.currentTarget;
|
|
398
|
+
R.value = null;
|
|
399
|
+
const n = window.getSelection()?.rangeCount ? window.getSelection().getRangeAt(0) : null;
|
|
400
|
+
n && (N = V(t, n.endContainer, n.endOffset)), S({ version: 1, content: H(t) });
|
|
401
|
+
}
|
|
402
|
+
function we(e) {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
const t = window.getSelection()?.rangeCount ? window.getSelection().getRangeAt(0) : null;
|
|
405
|
+
if (!t || !d.value) return;
|
|
406
|
+
t.deleteContents();
|
|
407
|
+
const n = window.document.createTextNode(e.clipboardData?.getData("text/plain") ?? "");
|
|
408
|
+
t.insertNode(n), t.setStartAfter(n), t.collapse(!0), N = V(d.value, t.endContainer, t.endOffset), S({ version: 1, content: H(d.value) });
|
|
409
|
+
}
|
|
410
|
+
function ke(e) {
|
|
411
|
+
e.clipboardData?.setData("text/plain", window.getSelection()?.toString() || j(f.value)), e.preventDefault();
|
|
412
|
+
}
|
|
413
|
+
function Ce(e) {
|
|
414
|
+
const t = e.metaKey || e.ctrlKey;
|
|
415
|
+
if (t && e.key.toLocaleLowerCase() === "z") {
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
const B = e.shiftKey ? P.pop() : K.pop();
|
|
418
|
+
if (!B) return;
|
|
419
|
+
(e.shiftKey ? K : P).push(f.value), S(B, !1);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (e.key === "Enter" && !e.altKey && !t && d.value) {
|
|
423
|
+
const B = window.getSelection()?.rangeCount ? window.getSelection().getRangeAt(0) : null;
|
|
424
|
+
if (!B?.collapsed) return;
|
|
425
|
+
const J = V(d.value, B.endContainer, B.endOffset);
|
|
426
|
+
if (J === null) return;
|
|
427
|
+
e.preventDefault(), N = J + 1, S(ge(J, `
|
|
428
|
+
`)), M();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (e.key !== "Backspace" && e.key !== "Delete") {
|
|
432
|
+
R.value = null;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (e.preventDefault(), !d.value) return;
|
|
436
|
+
const n = e.key === "Backspace" ? "backward" : "forward", a = e.target instanceof HTMLElement ? e.target.closest("[data-gap-id]") : null, p = a?.dataset.gapId ? W(a.dataset.gapId) : null, o = window.getSelection()?.rangeCount ? window.getSelection().getRangeAt(0) : null;
|
|
437
|
+
if (!o) return;
|
|
438
|
+
const v = V(d.value, o.startContainer, o.startOffset), m = V(d.value, o.endContainer, o.endOffset);
|
|
439
|
+
if (v === null || m === null) return;
|
|
440
|
+
const w = p ?? de(v, m, n);
|
|
441
|
+
if (w) {
|
|
442
|
+
R.value === w.gap.id ? (N = w.start, S(re(f.value, w.gap.id)), u("gapRemove", w.gap), R.value = null) : R.value = w.gap.id;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const E = j(f.value).length, X = v === m && n === "backward" ? Math.max(0, v - 1) : v, te = v === m && n === "forward" ? Math.min(E, m + 1) : m;
|
|
446
|
+
te > X && (N = X, S(fe(X, te)));
|
|
447
|
+
}
|
|
448
|
+
function Te(e, t) {
|
|
449
|
+
c.value && (c.value.options[e] = t);
|
|
450
|
+
}
|
|
451
|
+
function $e(e) {
|
|
452
|
+
c.value && c.value.options.splice(e, 1);
|
|
453
|
+
}
|
|
454
|
+
function ee() {
|
|
455
|
+
b.value && U();
|
|
456
|
+
}
|
|
457
|
+
return Ee(() => window.addEventListener("scroll", ee, !0)), Oe(() => window.removeEventListener("scroll", ee, !0)), (e, t) => (h(), x("div", {
|
|
458
|
+
ref_key: "shell",
|
|
459
|
+
ref: k,
|
|
460
|
+
class: L(["fillable-editor", g.class])
|
|
461
|
+
}, [
|
|
462
|
+
s.value && b.value && !c.value ? (h(), x("button", {
|
|
463
|
+
key: 0,
|
|
464
|
+
class: L(["selection-toolbar", `selection-toolbar--${s.value.placement}`]),
|
|
465
|
+
style: De({ left: `${s.value.left}px`, top: `${s.value.top}px` }),
|
|
466
|
+
"aria-label": `Turn ${b.value.text} into a gap`,
|
|
467
|
+
onMousedown: me
|
|
468
|
+
}, [...t[10] || (t[10] = [
|
|
469
|
+
i("span", { "aria-hidden": "true" }, "+", -1),
|
|
470
|
+
ae(" Add gap ", -1)
|
|
471
|
+
])], 46, ze)) : A("", !0),
|
|
472
|
+
i("div", {
|
|
473
|
+
ref_key: "editor",
|
|
474
|
+
ref: d,
|
|
475
|
+
class: "fillable-editor__surface",
|
|
476
|
+
contenteditable: !l.readOnly,
|
|
477
|
+
spellcheck: "true",
|
|
478
|
+
role: "textbox",
|
|
479
|
+
"aria-multiline": "true",
|
|
480
|
+
"aria-label": "Fillable text editor",
|
|
481
|
+
onInput: be,
|
|
482
|
+
onSelect: U,
|
|
483
|
+
onMouseup: U,
|
|
484
|
+
onKeyup: U,
|
|
485
|
+
onKeydown: Ce,
|
|
486
|
+
onPaste: we,
|
|
487
|
+
onCopy: ke
|
|
488
|
+
}, [
|
|
489
|
+
(h(!0), x(D, null, I(f.value.content, (n, a) => (h(), x(D, {
|
|
490
|
+
key: n.type === "gap" ? n.id : `text-${a}`
|
|
491
|
+
}, [
|
|
492
|
+
n.type === "text" ? (h(), x("span", Ke, [
|
|
493
|
+
(h(!0), x(D, null, I(n.text.split(`
|
|
494
|
+
`), (p, o) => (h(), x(D, { key: o }, [
|
|
495
|
+
o > 0 ? (h(), x("br", Pe)) : A("", !0),
|
|
496
|
+
i("span", He, T(p || ""), 1)
|
|
497
|
+
], 64))), 128))
|
|
498
|
+
])) : (h(), x("button", {
|
|
499
|
+
key: 1,
|
|
500
|
+
type: "button",
|
|
501
|
+
tabindex: l.readOnly ? -1 : 0,
|
|
502
|
+
contenteditable: "false",
|
|
503
|
+
"data-gap-id": n.id,
|
|
504
|
+
"data-gap-length": n.originalText.length,
|
|
505
|
+
"data-gap-original": n.originalText,
|
|
506
|
+
class: L(["editor-gap", `editor-gap--${n.control}`, { "editor-gap--keyboard-selected": R.value === n.id }]),
|
|
507
|
+
"aria-label": `${n.control} gap: ${n.originalText}. Click to edit.`,
|
|
508
|
+
onClick: (p) => he(n)
|
|
509
|
+
}, [
|
|
510
|
+
i("span", null, T(n.originalText), 1),
|
|
511
|
+
n.control === "select" ? (h(), x("svg", Je, [...t[11] || (t[11] = [
|
|
512
|
+
i("path", { d: "m8 10 4 4 4-4" }, null, -1)
|
|
513
|
+
])])) : A("", !0),
|
|
514
|
+
i("small", null, T(n.control), 1)
|
|
515
|
+
], 10, Xe))
|
|
516
|
+
], 64))), 128))
|
|
517
|
+
], 40, je),
|
|
518
|
+
i("div", _e, [
|
|
519
|
+
i("span", null, T(ce.value) + " characters", 1),
|
|
520
|
+
t[12] || (t[12] = i("i", null, null, -1)),
|
|
521
|
+
i("span", null, T(_.value) + " " + T(_.value === 1 ? "gap" : "gaps"), 1),
|
|
522
|
+
l.readOnly ? A("", !0) : (h(), x("span", We, "Select any text to make it fillable"))
|
|
523
|
+
]),
|
|
524
|
+
c.value ? (h(), x("div", {
|
|
525
|
+
key: 1,
|
|
526
|
+
class: "gap-dialog-backdrop",
|
|
527
|
+
onMousedown: t[9] || (t[9] = Ne((n) => c.value = null, ["self"]))
|
|
528
|
+
}, [
|
|
529
|
+
i("section", Ye, [
|
|
530
|
+
i("header", null, [
|
|
531
|
+
i("div", null, [
|
|
532
|
+
i("span", Qe, T(y.value ? "Gap settings" : "New interactive field"), 1),
|
|
533
|
+
i("h2", Ze, T(y.value ? "Edit gap" : "Create a gap"), 1)
|
|
534
|
+
]),
|
|
535
|
+
i("button", {
|
|
536
|
+
class: "icon-button",
|
|
537
|
+
"aria-label": "Close",
|
|
538
|
+
onClick: t[0] || (t[0] = (n) => c.value = null)
|
|
539
|
+
}, "×")
|
|
540
|
+
]),
|
|
541
|
+
t[19] || (t[19] = i("label", { class: "field-label" }, "Field type", -1)),
|
|
542
|
+
i("div", et, [
|
|
543
|
+
i("button", {
|
|
544
|
+
role: "radio",
|
|
545
|
+
"aria-checked": c.value.control === "input",
|
|
546
|
+
class: L({ active: c.value.control === "input" }),
|
|
547
|
+
onClick: t[1] || (t[1] = (n) => Q("input"))
|
|
548
|
+
}, [...t[13] || (t[13] = [
|
|
549
|
+
i("span", { class: "type-icon" }, "T", -1),
|
|
550
|
+
i("span", null, [
|
|
551
|
+
i("strong", null, "Text input"),
|
|
552
|
+
i("small", null, "Free-form response")
|
|
553
|
+
], -1)
|
|
554
|
+
])], 10, tt),
|
|
555
|
+
i("button", {
|
|
556
|
+
role: "radio",
|
|
557
|
+
"aria-checked": c.value.control === "select",
|
|
558
|
+
class: L({ active: c.value.control === "select" }),
|
|
559
|
+
onClick: t[2] || (t[2] = (n) => Q("select"))
|
|
560
|
+
}, [...t[14] || (t[14] = [
|
|
561
|
+
i("span", { class: "type-icon" }, [
|
|
562
|
+
i("svg", {
|
|
563
|
+
viewBox: "0 0 24 24",
|
|
564
|
+
fill: "none",
|
|
565
|
+
stroke: "currentColor",
|
|
566
|
+
"stroke-width": "2",
|
|
567
|
+
"stroke-linecap": "round",
|
|
568
|
+
"stroke-linejoin": "round",
|
|
569
|
+
"aria-hidden": "true"
|
|
570
|
+
}, [
|
|
571
|
+
i("path", { d: "M6 7h8M6 12h8M6 17h8" }),
|
|
572
|
+
i("path", { d: "m17 9 2 3 2-3" })
|
|
573
|
+
])
|
|
574
|
+
], -1),
|
|
575
|
+
i("span", null, [
|
|
576
|
+
i("strong", null, "Select"),
|
|
577
|
+
i("small", null, "Choose from a list")
|
|
578
|
+
], -1)
|
|
579
|
+
])], 10, nt)
|
|
580
|
+
]),
|
|
581
|
+
c.value.control === "input" ? (h(), x(D, { key: 0 }, [
|
|
582
|
+
t[15] || (t[15] = i("label", {
|
|
583
|
+
class: "field-label",
|
|
584
|
+
for: "original-text"
|
|
585
|
+
}, "Original text", -1)),
|
|
586
|
+
oe(i("input", {
|
|
587
|
+
id: "original-text",
|
|
588
|
+
"onUpdate:modelValue": t[3] || (t[3] = (n) => c.value.originalText = n),
|
|
589
|
+
class: "dialog-input"
|
|
590
|
+
}, null, 512), [
|
|
591
|
+
[ie, c.value.originalText]
|
|
592
|
+
]),
|
|
593
|
+
t[16] || (t[16] = i("label", {
|
|
594
|
+
class: "field-label",
|
|
595
|
+
for: "placeholder"
|
|
596
|
+
}, "Placeholder", -1)),
|
|
597
|
+
oe(i("input", {
|
|
598
|
+
id: "placeholder",
|
|
599
|
+
"onUpdate:modelValue": t[4] || (t[4] = (n) => c.value.placeholder = n),
|
|
600
|
+
class: "dialog-input"
|
|
601
|
+
}, null, 512), [
|
|
602
|
+
[ie, c.value.placeholder]
|
|
603
|
+
])
|
|
604
|
+
], 64)) : (h(), x("div", lt, [
|
|
605
|
+
t[17] || (t[17] = i("label", { class: "field-label" }, [
|
|
606
|
+
ae("Options "),
|
|
607
|
+
i("span", null, "Minimum 2")
|
|
608
|
+
], -1)),
|
|
609
|
+
(h(!0), x(D, null, I(c.value.options, (n, a) => (h(), x("div", {
|
|
610
|
+
key: a,
|
|
611
|
+
class: "option-row"
|
|
612
|
+
}, [
|
|
613
|
+
i("span", {
|
|
614
|
+
class: L(["option-correct-marker", { "option-correct-marker--empty": a !== 0 }])
|
|
615
|
+
}, "✓", 2),
|
|
616
|
+
i("input", {
|
|
617
|
+
class: "dialog-input",
|
|
618
|
+
value: n,
|
|
619
|
+
placeholder: a === 0 ? "Original text" : "Type an option",
|
|
620
|
+
"aria-label": `Option ${a + 1}`,
|
|
621
|
+
onInput: (p) => Te(a, p.target.value)
|
|
622
|
+
}, null, 40, at),
|
|
623
|
+
i("button", {
|
|
624
|
+
class: "icon-button small",
|
|
625
|
+
"aria-label": `Remove option ${a + 1}`,
|
|
626
|
+
onClick: (p) => $e(a)
|
|
627
|
+
}, "×", 8, ot)
|
|
628
|
+
]))), 128)),
|
|
629
|
+
i("button", {
|
|
630
|
+
class: "add-option",
|
|
631
|
+
onClick: t[5] || (t[5] = (n) => c.value.options.push(""))
|
|
632
|
+
}, "+ Add option")
|
|
633
|
+
])),
|
|
634
|
+
$.value ? (h(), x("p", it, T($.value), 1)) : A("", !0),
|
|
635
|
+
i("footer", null, [
|
|
636
|
+
y.value ? (h(), x("div", rt, [
|
|
637
|
+
i("button", {
|
|
638
|
+
onClick: t[6] || (t[6] = (n) => Z(!1))
|
|
639
|
+
}, "Convert to text"),
|
|
640
|
+
i("button", {
|
|
641
|
+
onClick: t[7] || (t[7] = (n) => Z(!0))
|
|
642
|
+
}, "Delete")
|
|
643
|
+
])) : A("", !0),
|
|
644
|
+
t[18] || (t[18] = i("span", null, null, -1)),
|
|
645
|
+
i("button", {
|
|
646
|
+
class: "button secondary",
|
|
647
|
+
onClick: t[8] || (t[8] = (n) => c.value = null)
|
|
648
|
+
}, "Cancel"),
|
|
649
|
+
i("button", {
|
|
650
|
+
class: "button primary",
|
|
651
|
+
onClick: ye
|
|
652
|
+
}, T(y.value ? "Save changes" : "Create gap"), 1)
|
|
653
|
+
])
|
|
654
|
+
])
|
|
655
|
+
], 32)) : A("", !0)
|
|
656
|
+
], 2));
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
export {
|
|
660
|
+
ct as EMPTY_DOCUMENT,
|
|
661
|
+
ut as FillableText,
|
|
662
|
+
ft as FillableTextEditor,
|
|
663
|
+
Ie as createGap,
|
|
664
|
+
re as deleteGap,
|
|
665
|
+
dt as deserialize,
|
|
666
|
+
G as normalizeContent,
|
|
667
|
+
qe as removeGap,
|
|
668
|
+
pt as serialize,
|
|
669
|
+
se as slugifyOption,
|
|
670
|
+
j as toPlainText,
|
|
671
|
+
Ue as updateGap,
|
|
672
|
+
Be as validateDocument
|
|
673
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue-fillable-text",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed Vue components for documents with interactive input and select gaps.",
|
|
5
|
+
"author": "Evgeniia Iaroslavtseva",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": ["**/*.css"],
|
|
9
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
10
|
+
"main": "./dist/vue-fillable-text.cjs",
|
|
11
|
+
"module": "./dist/vue-fillable-text.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/vue-fillable-text.js",
|
|
17
|
+
"require": "./dist/vue-fillable-text.cjs"
|
|
18
|
+
},
|
|
19
|
+
"./styles.css": "./dist/vue-fillable-text.css"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "vite build && vue-tsc -p tsconfig.build.json",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "vue-tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "npm test && npm run typecheck && npm run build"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": { "vue": ">=3.3 <4" },
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
30
|
+
"@vue/test-utils": "^2.4.6",
|
|
31
|
+
"jsdom": "^26.1.0",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vite": "^7.1.7",
|
|
34
|
+
"vitest": "^3.2.7",
|
|
35
|
+
"vue": "^3.5.22",
|
|
36
|
+
"vue-tsc": "^3.0.8"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": { "access": "public" },
|
|
39
|
+
"engines": { "node": ">=20.19" },
|
|
40
|
+
"keywords": ["vue", "fillable-text", "fill-in-the-blank", "forms", "editor", "typescript"]
|
|
41
|
+
}
|