svg preview renderer
This commit is contained in:
parent
43a8412f06
commit
faa9599849
29 changed files with 1027 additions and 254 deletions
|
|
@ -3,3 +3,12 @@
|
|||
@import "tailwindcss/utilities";
|
||||
|
||||
/* This file is for your main application CSS */
|
||||
code .highlighted {
|
||||
@apply bg-ctp-mauve/10;
|
||||
transition:background-color .5s;
|
||||
margin:0 -24px;
|
||||
padding:0 24px;
|
||||
width:calc(100% + 48px);
|
||||
display:inline-block
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,16 +22,9 @@ import { Socket } from "phoenix";
|
|||
import { LiveSocket } from "phoenix_live_view";
|
||||
import topbar from "../vendor/topbar";
|
||||
import CodeBlockHook from "./hooks/code_block_hook";
|
||||
import {
|
||||
DropdownAnimation,
|
||||
SearchableDropdown,
|
||||
} from "./hooks/searchable_dropdown";
|
||||
import { SelectHook } from "./hooks/select";
|
||||
|
||||
let Hooks = {
|
||||
CodeBlockHook,
|
||||
SearchableDropdown,
|
||||
DropdownAnimation,
|
||||
};
|
||||
let Hooks = { CodeBlockHook, SelectHook };
|
||||
|
||||
Hooks.ClickOutside = {
|
||||
mounted() {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,10 @@ const CodeBlockHook = {
|
|||
this.el.dataset.highlightedLines || "[]",
|
||||
);
|
||||
|
||||
console.log(code);
|
||||
console.log("language", language);
|
||||
|
||||
const lines = code.split("\n");
|
||||
|
||||
console.log(lines.length, lines);
|
||||
|
||||
// Convert line numbers to decorations
|
||||
const decorations = highlightedLines
|
||||
.map((line) => {
|
||||
|
|
@ -40,7 +39,16 @@ const CodeBlockHook = {
|
|||
codeToHtml(code, {
|
||||
lang: language,
|
||||
theme: "catppuccin-mocha",
|
||||
decorations,
|
||||
transformers: [
|
||||
{
|
||||
line(node, line) {
|
||||
node.properties["data-line"] = line;
|
||||
if (highlightedLines.includes(line)) {
|
||||
this.addClassToHast(node, "highlighted");
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}).then((html) => {
|
||||
console.log(html);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
SearchableDropdown = {
|
||||
mounted() {
|
||||
this.el.addEventListener("input", (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const dropdownId = this.el.dataset.dropdownId;
|
||||
const optionsContainer = document.querySelector(`#${dropdownId}-options`);
|
||||
const options = optionsContainer.querySelectorAll("li button");
|
||||
|
||||
options.forEach((option) => {
|
||||
const text = option.textContent.toLowerCase();
|
||||
option.parentElement.style.display = text.includes(query)
|
||||
? "block"
|
||||
: "none";
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const DropdownAnimation = {
|
||||
mounted() {
|
||||
this.el.addEventListener("transitionend", (e) => {
|
||||
if (
|
||||
e.propertyName === "opacity" &&
|
||||
this.el.classList.contains("fade-out")
|
||||
) {
|
||||
this.el.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export { SearchableDropdown, DropdownAnimation };
|
||||
186
assets/js/hooks/select.js
Normal file
186
assets/js/hooks/select.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
const SelectHook = {
|
||||
mounted() {
|
||||
const container = this.el;
|
||||
const trigger = container.querySelector('[id$="-trigger"]');
|
||||
const dropdown = container.querySelector('[id$="-dropdown"]');
|
||||
const search = container.querySelector('[id$="-search"]');
|
||||
const options = container.querySelector('[id$="-options"]');
|
||||
const hiddenInput = container.querySelector('input[type="hidden"]');
|
||||
const selectedText = container.querySelector('[id$="-selected"]');
|
||||
|
||||
const toggleDropdown = (event) => {
|
||||
// Prevent the event from bubbling up to any parent form
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const isExpanded = dropdown.classList.contains("block");
|
||||
if (!isExpanded) {
|
||||
dropdown.classList.remove("hidden");
|
||||
// Add animation classes
|
||||
dropdown.classList.add(
|
||||
"block",
|
||||
"animate-in",
|
||||
"fade-in",
|
||||
"duration-200",
|
||||
);
|
||||
search.value = "";
|
||||
filterOptions("");
|
||||
search.focus();
|
||||
} else {
|
||||
// Add animation classes for hiding
|
||||
dropdown.classList.remove("animate-in", "fade-in");
|
||||
dropdown.classList.add("animate-out", "fade-out");
|
||||
setTimeout(() => {
|
||||
dropdown.classList.remove("block", "animate-out", "fade-out");
|
||||
dropdown.classList.add("hidden");
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const filterOptions = (query) => {
|
||||
const items = options.querySelectorAll("li");
|
||||
items.forEach((item) => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
if (text.includes(query.toLowerCase())) {
|
||||
item.style.display = "";
|
||||
} else {
|
||||
item.style.display = "none";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectOption = (option, event) => {
|
||||
// Prevent form submission
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const value = option.dataset.value;
|
||||
const label = option.querySelector("span").textContent;
|
||||
|
||||
// Update hidden input
|
||||
hiddenInput.value = value;
|
||||
|
||||
// Update visible text
|
||||
selectedText.textContent = label;
|
||||
|
||||
// Update check mark position
|
||||
options.querySelectorAll("li").forEach((li) => {
|
||||
const checkmark = li.querySelector("span:last-child");
|
||||
if (checkmark) checkmark.remove();
|
||||
|
||||
if (li.dataset.value === value) {
|
||||
li.setAttribute("aria-selected", "true");
|
||||
const checkMarkSpan = document.createElement("span");
|
||||
checkMarkSpan.className =
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4 ctp-text-lavender";
|
||||
checkMarkSpan.innerHTML = `
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
`;
|
||||
li.appendChild(checkMarkSpan);
|
||||
} else {
|
||||
li.setAttribute("aria-selected", "false");
|
||||
}
|
||||
});
|
||||
|
||||
toggleDropdown();
|
||||
|
||||
// Dispatch change event without bubbling
|
||||
const changeEvent = new Event("change", { bubbles: false });
|
||||
hiddenInput.dispatchEvent(changeEvent);
|
||||
|
||||
// Push the value to the LiveView
|
||||
this.pushEventTo(this.el, "change", { value: value });
|
||||
};
|
||||
|
||||
// Event Listeners
|
||||
trigger.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDropdown(e);
|
||||
});
|
||||
|
||||
search.addEventListener("input", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
filterOptions(e.target.value);
|
||||
});
|
||||
|
||||
options.querySelectorAll("li").forEach((option) => {
|
||||
option.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectOption(option, e);
|
||||
});
|
||||
});
|
||||
|
||||
// Prevent dropdowns from causing form submission
|
||||
container.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!container.contains(e.target)) {
|
||||
dropdown.classList.add("hidden");
|
||||
dropdown.classList.remove("block");
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
container.addEventListener("keydown", (e) => {
|
||||
const items = Array.from(options.querySelectorAll("li")).filter(
|
||||
(li) => li.style.display !== "none",
|
||||
);
|
||||
const currentIdx = items.findIndex((li) => li === document.activeElement);
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
if (currentIdx < items.length - 1) {
|
||||
items[currentIdx + 1].focus();
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
if (currentIdx > 0) {
|
||||
items[currentIdx - 1].focus();
|
||||
}
|
||||
break;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
if (document.activeElement.tagName === "LI") {
|
||||
selectOption(document.activeElement, e);
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
toggleDropdown(e);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent the dropdown from closing when clicking inside it
|
||||
dropdown.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Prevent the search input from submitting the form
|
||||
search.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export { SelectHook };
|
||||
Loading…
Add table
Add a link
Reference in a new issue