+Trix editor for annotations

This commit is contained in:
Simon Martens
2026-01-10 18:03:55 +01:00
parent b8c1dec24f
commit 21f07f0e7f
7 changed files with 6625 additions and 837 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,9 @@
"name": "caveman_views", "name": "caveman_views",
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": {
"trix": "^2.1.16"
},
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.0.0", "@tailwindcss/postcss": "^4.0.0",
"daisyui": "^5.0.0-beta.8", "daisyui": "^5.0.0-beta.8",
@@ -1100,6 +1103,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1266,6 +1276,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -2241,6 +2260,18 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/trix": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/trix/-/trix-2.1.16.tgz",
"integrity": "sha512-XtZgWI+oBvLzX7CWnkIf+ZWC+chL+YG/TkY43iMTV0Zl+CJjn18B1GJUCEWJ8qgfpcyMBuysnNAfPWiv2sV14A==",
"license": "MIT",
"dependencies": {
"dompurify": "^3.2.5"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/ulid": { "node_modules/ulid": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/ulid/-/ulid-2.4.0.tgz", "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.4.0.tgz",

View File

@@ -30,5 +30,8 @@
"css": "postcss transform/site.css -o assets/style.css", "css": "postcss transform/site.css -o assets/style.css",
"preview": "vite preview" "preview": "vite preview"
}, },
"type": "module" "type": "module",
"dependencies": {
"trix": "^2.1.16"
}
} }

View File

@@ -10,5 +10,72 @@
<div class="inputlabelrow"> <div class="inputlabelrow">
<label for="annotation" class="inputlabel">{{ $label }}</label> <label for="annotation" class="inputlabel">{{ $label }}</label>
</div> </div>
<textarea name="annotation" id="annotation" class="inputinput" placeholder="" autocomplete="off" rows="2">{{- $annotation -}}</textarea>
<trix-toolbar id="annotation-toolbar">
<div class="trix-toolbar-container">
<!-- Text formatting group -->
<span class="trix-toolbar-group">
<button type="button" class="trix-toolbar-button" data-trix-attribute="bold" data-trix-key="b" title="Bold">
<i class="ri-bold"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="italic" data-trix-key="i" title="Italic">
<i class="ri-italic"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="strike" title="Strikethrough">
<i class="ri-strikethrough"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="Link">
<i class="ri-links-line"></i>
</button>
</span>
<!-- Block formatting group -->
<span class="trix-toolbar-group">
<button type="button" class="trix-toolbar-button" data-trix-attribute="heading1" title="Heading">
<i class="ri-h-1"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="quote" title="Quote">
<i class="ri-double-quotes-l"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="bullet" title="Bullets">
<i class="ri-list-unordered"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-attribute="number" title="Numbers">
<i class="ri-list-ordered"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-action="decreaseNestingLevel" title="Decrease Indent">
<i class="ri-indent-decrease"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-action="increaseNestingLevel" title="Increase Indent">
<i class="ri-indent-increase"></i>
</button>
</span>
<!-- History group -->
<span class="trix-toolbar-group">
<button type="button" class="trix-toolbar-button" data-trix-action="undo" data-trix-key="z" title="Undo">
<i class="ri-arrow-go-back-line"></i>
</button>
<button type="button" class="trix-toolbar-button" data-trix-action="redo" data-trix-key="shift+z" title="Redo">
<i class="ri-arrow-go-forward-line"></i>
</button>
</span>
</div>
<!-- Link dialog (required for link functionality) -->
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="Enter a URL…" aria-label="URL" required data-trix-input>
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" value="Link" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="Unlink" data-trix-method="removeAttribute">
</div>
</div>
</div>
</div>
</trix-toolbar>
<textarea hidden id="annotation" name="annotation" autocomplete="off">{{- $annotation -}}</textarea>
<trix-editor input="annotation" toolbar="annotation-toolbar"></trix-editor>
</div> </div>

View File

@@ -494,4 +494,155 @@
select + reset-button .rbi-button { select + reset-button .rbi-button {
@apply ml-3; @apply ml-3;
} }
/* Trix Custom Toolbar Styles */
.trix-toolbar-container {
@apply flex flex-wrap gap-1 px-2 py-1.5 bg-stone-200 border-b border-stone-300;
}
.trix-toolbar-group {
@apply inline-flex gap-0.5 items-center;
}
.trix-toolbar-group:not(:last-child) {
@apply mr-2 pr-2 border-r border-stone-400;
}
.trix-toolbar-button {
@apply w-7 h-7 inline-flex items-center justify-center
text-gray-700 hover:text-slate-900 hover:bg-stone-300
rounded transition-all duration-100
focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-1;
}
.trix-toolbar-button i {
@apply text-base pointer-events-none;
}
/* Active state for toggle buttons */
.trix-toolbar-button.trix-active {
@apply bg-slate-600 text-white hover:bg-slate-700 hover:text-white;
}
/* Disabled state */
.trix-toolbar-button:disabled {
@apply opacity-30 cursor-not-allowed hover:bg-transparent hover:text-gray-400;
}
.trix-toolbar-button:disabled i {
@apply text-gray-400;
}
/* Trix editor content area */
trix-editor {
@apply block w-full focus:border-none focus:outline-none px-3 py-2 min-h-[6rem];
}
/* Trix content formatting styles */
trix-editor h1 {
@apply text-2xl font-bold mt-4 mb-2 first:mt-0;
}
trix-editor blockquote {
@apply border-l-4 border-stone-400 pl-4 py-1 italic text-gray-700;
}
trix-editor ul {
@apply list-disc list-outside ml-1 my-2;
}
trix-editor ol {
@apply list-decimal list-outside ml-1 my-2;
}
trix-editor li {
@apply leading-normal;
margin-left: 1.5rem;
}
trix-editor ul > li {
list-style-type: disc;
}
trix-editor ol > li {
list-style-type: decimal;
}
trix-editor ul ul,
trix-editor ol ul {
@apply list-disc my-0;
}
trix-editor ul ol,
trix-editor ol ol {
@apply list-decimal my-0;
}
trix-editor ul ul > li,
trix-editor ol ul > li,
trix-editor ul ol > li,
trix-editor ol ol > li {
@apply leading-normal;
}
trix-editor a {
@apply text-blue-600 underline hover:text-blue-800;
}
trix-editor strong {
@apply font-bold;
}
trix-editor em {
@apply italic;
}
trix-editor del {
@apply line-through;
}
trix-editor pre {
@apply bg-stone-100 border border-stone-300 rounded px-3 py-2 my-2 font-mono text-sm overflow-x-auto;
}
/* Link dialog styling */
.trix-dialogs {
@apply relative;
}
.trix-dialog {
@apply hidden absolute top-full left-0 right-0 mt-1 p-3 bg-white border border-stone-300 rounded-md shadow-lg z-10;
}
.trix-dialog[data-trix-active] {
@apply block;
}
.trix-dialog__link-fields {
@apply flex flex-col gap-2;
}
.trix-input--dialog {
@apply w-full px-3 py-1.5 border border-stone-300 rounded-md
focus:border-slate-500 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-1;
}
.trix-button-group {
@apply flex gap-2;
}
.trix-button--dialog {
@apply px-4 py-1.5 text-sm font-medium rounded-md
border border-transparent shadow-sm
cursor-pointer transition-all duration-75
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500;
}
.trix-button--dialog[value="Link"] {
@apply bg-slate-700 text-white hover:bg-slate-800 active:bg-slate-900;
}
.trix-button--dialog[value="Unlink"] {
@apply bg-stone-200 text-gray-800 hover:bg-stone-300 active:bg-stone-400;
}
} }

View File

@@ -1,6 +1,7 @@
// INFO: We import this so vite processes the stylesheet // INFO: We import this so vite processes the stylesheet
import "./site.css"; import "./site.css";
import Trix from "trix";
import { FilterPill } from "./filter-pill.js"; import { FilterPill } from "./filter-pill.js";
import { FilterList } from "./filter-list.js"; import { FilterList } from "./filter-list.js";
import { ScrollButton } from "./scroll-button.js"; import { ScrollButton } from "./scroll-button.js";
@@ -337,7 +338,7 @@ function FormLoad(form) {
// Attach resize handler to all textareas // Attach resize handler to all textareas
for (const textarea of textareas) { for (const textarea of textareas) {
console.log("Attaching input listener to:", textarea.name || textarea.id); console.log("Attaching input listener to:", textarea.name || textarea.id);
textarea.addEventListener('input', function() { textarea.addEventListener("input", function () {
console.log("Input event on textarea:", this.name || this.id); console.log("Input event on textarea:", this.name || this.id);
TextareaAutoResize(this); TextareaAutoResize(this);
}); });
@@ -369,9 +370,7 @@ function FormLoad(form) {
const target = mutation.target; const target = mutation.target;
// Check if this element or its children contain textareas // Check if this element or its children contain textareas
if (target instanceof HTMLElement) { if (target instanceof HTMLElement) {
const textareasInTarget = target.matches("textarea") const textareasInTarget = target.matches("textarea") ? [target] : Array.from(target.querySelectorAll("textarea"));
? [target]
: Array.from(target.querySelectorAll("textarea"));
for (const textarea of textareasInTarget) { for (const textarea of textareasInTarget) {
// Only resize if now visible // Only resize if now visible
@@ -392,9 +391,9 @@ function FormLoad(form) {
// Handle boolean checkboxes // Handle boolean checkboxes
const booleanCheckboxes = form.querySelectorAll('input[type="checkbox"][data-boolean-checkbox]'); const booleanCheckboxes = form.querySelectorAll('input[type="checkbox"][data-boolean-checkbox]');
booleanCheckboxes.forEach(checkbox => { booleanCheckboxes.forEach((checkbox) => {
// Ensure each boolean checkbox has proper value handling // Ensure each boolean checkbox has proper value handling
checkbox.value = 'true'; checkbox.value = "true";
// Add change handler to manage hidden input // Add change handler to manage hidden input
const updateHiddenInput = () => { const updateHiddenInput = () => {
@@ -406,10 +405,10 @@ function FormLoad(form) {
// If checkbox is unchecked, add hidden input with false value // If checkbox is unchecked, add hidden input with false value
if (!checkbox.checked) { if (!checkbox.checked) {
const hidden = document.createElement('input'); const hidden = document.createElement("input");
hidden.type = 'hidden'; hidden.type = "hidden";
hidden.name = checkbox.name; hidden.name = checkbox.name;
hidden.value = 'false'; hidden.value = "false";
checkbox.parentNode.insertBefore(hidden, checkbox); checkbox.parentNode.insertBefore(hidden, checkbox);
} }
}; };
@@ -418,7 +417,7 @@ function FormLoad(form) {
updateHiddenInput(); updateHiddenInput();
// Update on change // Update on change
checkbox.addEventListener('change', updateHiddenInput); checkbox.addEventListener("change", updateHiddenInput);
}); });
} }
@@ -443,22 +442,4 @@ window.HookupRBChange = HookupRBChange;
window.FormLoad = FormLoad; window.FormLoad = FormLoad;
window.TextareaAutoResize = TextareaAutoResize; window.TextareaAutoResize = TextareaAutoResize;
export { export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote, AlmanachEditPage, RelationsEditor, EditPage, FabMenu };
FilterList,
ScrollButton,
AbbreviationTooltips,
MultiSelectSimple,
MultiSelectRole,
ToolTip,
PopupImage,
TabList,
FilterPill,
ImageReel,
IntLink,
ItemsEditor,
SingleSelectRemote,
AlmanachEditPage,
RelationsEditor,
EditPage,
FabMenu,
};