feat: gaps in tailwind migration closed
This commit is contained in:
226
assets/js/hooks/app_shell.js
Normal file
226
assets/js/hooks/app_shell.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import {
|
||||
SIDEBAR_STORAGE_KEY,
|
||||
ASSISTANT_STORAGE_KEY,
|
||||
UI_LANGUAGE_STORAGE_KEY,
|
||||
WORKBENCH_SESSION_STORAGE_KEY_PREFIX
|
||||
} from "../constants.js";
|
||||
import {
|
||||
parseJsonObject,
|
||||
setMediaThumbnailLoaded,
|
||||
syncMediaThumbnailState,
|
||||
clamp
|
||||
} from "../utils/dom.js";
|
||||
import { shellWidth, setShellWidth, persistWidth, readStoredSize } from "../utils/layout.js";
|
||||
import {
|
||||
parseShortcutConfig,
|
||||
normalizeShortcutKey,
|
||||
shortcutMatchesEvent,
|
||||
shortcutTargetIsEditable
|
||||
} from "../utils/shortcuts.js";
|
||||
import { syncTitlebarOverlayInsets } from "../bridges/titlebar_overlay.js";
|
||||
import { runMenuRuntimeCommand } from "../bridges/menu_runtime.js";
|
||||
|
||||
export const AppShell = {
|
||||
mounted() {
|
||||
this.shortcuts = parseShortcutConfig(this.el.dataset.shortcuts);
|
||||
this.currentProjectId = this.el.dataset.projectId || "";
|
||||
this.syncStoredLayout();
|
||||
this.syncStoredUiLanguage();
|
||||
this.destroyOverlaySync = syncTitlebarOverlayInsets();
|
||||
|
||||
this.workbenchStorageKey = (projectId) =>
|
||||
projectId ? `${WORKBENCH_SESSION_STORAGE_KEY_PREFIX}${projectId}` : null;
|
||||
|
||||
this.restoreStoredWorkbenchSession = () => {
|
||||
const projectId = this.el.dataset.projectId || "";
|
||||
const storageKey = this.workbenchStorageKey(projectId);
|
||||
|
||||
if (!storageKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const session = parseJsonObject(window.localStorage.getItem(storageKey));
|
||||
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.pushEvent("restore_workbench_session", { session });
|
||||
return true;
|
||||
};
|
||||
|
||||
this.persistWorkbenchSession = () => {
|
||||
const projectId = this.el.dataset.projectId || "";
|
||||
const storageKey = this.workbenchStorageKey(projectId);
|
||||
const session = this.el.dataset.workbenchSession;
|
||||
|
||||
if (!storageKey || !session) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(storageKey, session);
|
||||
};
|
||||
|
||||
this.handleMouseDown = (event) => {
|
||||
const handle = event.target.closest("[data-role='resize-handle']");
|
||||
|
||||
if (!handle || !this.el.contains(handle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const target = handle.dataset.resize;
|
||||
const startX = event.clientX;
|
||||
const startWidth =
|
||||
target === "assistant"
|
||||
? shellWidth("[data-testid='assistant-shell']")
|
||||
: shellWidth("[data-testid='sidebar-shell']");
|
||||
|
||||
const min = target === "assistant" ? 280 : 200;
|
||||
const max = target === "assistant" ? 640 : 500;
|
||||
const invert = target === "assistant";
|
||||
|
||||
const onMouseMove = (moveEvent) => {
|
||||
const delta = invert ? startX - moveEvent.clientX : moveEvent.clientX - startX;
|
||||
const width = clamp(startWidth + delta, min, max);
|
||||
const selector = target === "assistant" ? "[data-testid='assistant-shell']" : "[data-testid='sidebar-shell']";
|
||||
|
||||
setShellWidth(selector, width);
|
||||
persistWidth(target, width);
|
||||
};
|
||||
|
||||
const onMouseUp = (upEvent) => {
|
||||
const delta = invert ? startX - upEvent.clientX : upEvent.clientX - startX;
|
||||
const width = clamp(startWidth + delta, min, max);
|
||||
|
||||
persistWidth(target, width);
|
||||
this.pushEvent("resize_panel", { target, width });
|
||||
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
this.el.addEventListener("mousedown", this.handleMouseDown);
|
||||
|
||||
this.handleNativeMenuAction = (event) => {
|
||||
const action = event.detail?.action;
|
||||
const ackId = event.detail?.ackId;
|
||||
|
||||
if (action) {
|
||||
this.pushEvent("native_menu_action", { action }, () => {
|
||||
if (ackId) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("bds:native-menu-action-ack", { detail: { ackId } })
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.handleChange = (event) => {
|
||||
const select = event.target.closest(".status-bar-language-select");
|
||||
|
||||
if (select && this.el.contains(select)) {
|
||||
window.localStorage.setItem(UI_LANGUAGE_STORAGE_KEY, select.value);
|
||||
}
|
||||
};
|
||||
|
||||
this.handleShortcutKeyDown = (event) => {
|
||||
if (shortcutTargetIsEditable(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shortcut = this.shortcuts.find((candidate) => shortcutMatchesEvent(candidate, event));
|
||||
|
||||
if (!shortcut) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.pushEvent("shortcut", {
|
||||
key: normalizeShortcutKey(event.key),
|
||||
meta: event.metaKey,
|
||||
ctrl: event.ctrlKey,
|
||||
alt: event.altKey,
|
||||
shift: event.shiftKey,
|
||||
tag: event.target?.tagName || null,
|
||||
contentEditable: event.target?.isContentEditable || false
|
||||
});
|
||||
};
|
||||
|
||||
this.handleThumbnailLoad = (event) => {
|
||||
if (event.target instanceof HTMLImageElement && event.target.classList.contains("media-thumbnail-image")) {
|
||||
setMediaThumbnailLoaded(event.target, true);
|
||||
}
|
||||
};
|
||||
|
||||
this.handleThumbnailError = (event) => {
|
||||
if (event.target instanceof HTMLImageElement && event.target.classList.contains("media-thumbnail-image")) {
|
||||
setMediaThumbnailLoaded(event.target, false);
|
||||
}
|
||||
};
|
||||
|
||||
this.handleEvent("menu-runtime-command", ({ action }) => {
|
||||
if (action) {
|
||||
runMenuRuntimeCommand(String(action));
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction);
|
||||
window.addEventListener("keydown", this.handleShortcutKeyDown, true);
|
||||
this.el.addEventListener("load", this.handleThumbnailLoad, true);
|
||||
this.el.addEventListener("error", this.handleThumbnailError, true);
|
||||
this.el.addEventListener("change", this.handleChange);
|
||||
syncMediaThumbnailState(this.el);
|
||||
this.restoreStoredWorkbenchSession();
|
||||
},
|
||||
|
||||
updated() {
|
||||
const nextProjectId = this.el.dataset.projectId || "";
|
||||
|
||||
if (nextProjectId !== this.currentProjectId) {
|
||||
this.currentProjectId = nextProjectId;
|
||||
|
||||
if (this.restoreStoredWorkbenchSession()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
syncMediaThumbnailState(this.el);
|
||||
this.persistWorkbenchSession();
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.el.removeEventListener("mousedown", this.handleMouseDown);
|
||||
this.el.removeEventListener("load", this.handleThumbnailLoad, true);
|
||||
this.el.removeEventListener("error", this.handleThumbnailError, true);
|
||||
this.el.removeEventListener("change", this.handleChange);
|
||||
window.removeEventListener("bds:native-menu-action", this.handleNativeMenuAction);
|
||||
window.removeEventListener("keydown", this.handleShortcutKeyDown, true);
|
||||
if (this.destroyOverlaySync) {
|
||||
this.destroyOverlaySync();
|
||||
}
|
||||
},
|
||||
|
||||
syncStoredLayout() {
|
||||
this.pushEvent("sync_layout", {
|
||||
sidebar_width: readStoredSize(SIDEBAR_STORAGE_KEY, shellWidth("[data-testid='sidebar-shell']"), 200, 500),
|
||||
assistant_sidebar_width: readStoredSize(ASSISTANT_STORAGE_KEY, 360, 280, 640)
|
||||
});
|
||||
},
|
||||
|
||||
syncStoredUiLanguage() {
|
||||
const stored = window.localStorage.getItem(UI_LANGUAGE_STORAGE_KEY);
|
||||
|
||||
if (stored) {
|
||||
this.pushEvent("sync_ui_language", { language: stored });
|
||||
}
|
||||
}
|
||||
};
|
||||
139
assets/js/hooks/chat_surface.js
Normal file
139
assets/js/hooks/chat_surface.js
Normal file
@@ -0,0 +1,139 @@
|
||||
export const ChatSurface = {
|
||||
mounted() {
|
||||
this.stickToBottom = true;
|
||||
this.scrollContainer = null;
|
||||
|
||||
this.autoResize = () => {
|
||||
const textarea = this.el.querySelector(".chat-input");
|
||||
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = getComputedStyle(textarea);
|
||||
const minHeight = parseFloat(styles.getPropertyValue("--chat-input-min-height")) || 20;
|
||||
const maxHeight = parseFloat(styles.getPropertyValue("--chat-input-max-height")) || 160;
|
||||
|
||||
textarea.rows = 1;
|
||||
textarea.style.minHeight = `${minHeight}px`;
|
||||
|
||||
if (textarea.value.trim() === "") {
|
||||
textarea.style.height = `${minHeight}px`;
|
||||
textarea.style.maxHeight = `${minHeight}px`;
|
||||
textarea.style.overflowY = "hidden";
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.style.maxHeight = `${maxHeight}px`;
|
||||
textarea.style.height = "0px";
|
||||
const nextHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
|
||||
textarea.style.height = `${nextHeight}px`;
|
||||
textarea.style.overflowY = nextHeight >= maxHeight ? "auto" : "hidden";
|
||||
};
|
||||
|
||||
this.syncScrollContainer = () => {
|
||||
const nextContainer = this.el.querySelector(".chat-messages");
|
||||
|
||||
if (nextContainer === this.scrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.scrollContainer) {
|
||||
this.scrollContainer.removeEventListener("scroll", this.handleScroll);
|
||||
}
|
||||
|
||||
this.scrollContainer = nextContainer;
|
||||
|
||||
if (this.scrollContainer) {
|
||||
this.scrollContainer.addEventListener("scroll", this.handleScroll);
|
||||
}
|
||||
};
|
||||
|
||||
this.scrollToBottom = (force = false) => {
|
||||
if (!this.scrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (force || this.stickToBottom) {
|
||||
this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
this.syncExpandedSurfaces = () => {
|
||||
this.el
|
||||
.querySelectorAll(".chat-inline-surface[data-expanded='true']")
|
||||
.forEach((surface) => {
|
||||
surface.open = true;
|
||||
});
|
||||
};
|
||||
|
||||
this.surfaceObserver = new MutationObserver(() => {
|
||||
this.syncExpandedSurfaces();
|
||||
});
|
||||
|
||||
this.handleScroll = () => {
|
||||
if (!this.scrollContainer) {
|
||||
this.stickToBottom = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const distanceFromBottom =
|
||||
this.scrollContainer.scrollHeight -
|
||||
this.scrollContainer.scrollTop -
|
||||
this.scrollContainer.clientHeight;
|
||||
|
||||
this.stickToBottom = distanceFromBottom < 48;
|
||||
};
|
||||
|
||||
this.handleInput = (event) => {
|
||||
if (!event.target.closest(".chat-input")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stickToBottom = true;
|
||||
this.autoResize();
|
||||
};
|
||||
|
||||
this.handleKeyDown = (event) => {
|
||||
if (!event.target.closest(".chat-input")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
||||
event.preventDefault();
|
||||
|
||||
const sendButton = this.el.querySelector("[data-testid='chat-send-button']");
|
||||
|
||||
if (sendButton && !sendButton.disabled) {
|
||||
sendButton.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.el.addEventListener("input", this.handleInput);
|
||||
this.el.addEventListener("keydown", this.handleKeyDown);
|
||||
|
||||
this.syncScrollContainer();
|
||||
this.syncExpandedSurfaces();
|
||||
this.surfaceObserver.observe(this.el, { childList: true, subtree: true });
|
||||
this.autoResize();
|
||||
window.requestAnimationFrame(() => this.scrollToBottom(true));
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.syncScrollContainer();
|
||||
this.syncExpandedSurfaces();
|
||||
this.autoResize();
|
||||
window.requestAnimationFrame(() => this.scrollToBottom());
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.surfaceObserver.disconnect();
|
||||
this.el.removeEventListener("input", this.handleInput);
|
||||
this.el.removeEventListener("keydown", this.handleKeyDown);
|
||||
|
||||
if (this.scrollContainer) {
|
||||
this.scrollContainer.removeEventListener("scroll", this.handleScroll);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1 +1,18 @@
|
||||
export const createHooks = (hooks) => hooks;
|
||||
import { AppShell } from "./app_shell.js";
|
||||
import { SidebarInteractions } from "./sidebar_interactions.js";
|
||||
import { SettingsSectionScroll, TagsSectionScroll } from "./section_scroll.js";
|
||||
import { ChatSurface } from "./chat_surface.js";
|
||||
import { MenuEditorTree } from "./menu_editor_tree.js";
|
||||
import { MonacoEditor } from "./monaco_editor.js";
|
||||
import { MonacoDiffEditor } from "./monaco_diff_editor.js";
|
||||
|
||||
export const Hooks = {
|
||||
AppShell,
|
||||
SidebarInteractions,
|
||||
SettingsSectionScroll,
|
||||
TagsSectionScroll,
|
||||
ChatSurface,
|
||||
MenuEditorTree,
|
||||
MonacoEditor,
|
||||
MonacoDiffEditor
|
||||
};
|
||||
|
||||
134
assets/js/hooks/menu_editor_tree.js
Normal file
134
assets/js/hooks/menu_editor_tree.js
Normal file
@@ -0,0 +1,134 @@
|
||||
export const MenuEditorTree = {
|
||||
mounted() {
|
||||
this.dragItemId = null;
|
||||
this.dragSourceEl = null;
|
||||
this.dropTargetEl = null;
|
||||
this.dropPosition = null;
|
||||
|
||||
this.clearDropTarget = () => {
|
||||
if (this.dropTargetEl) {
|
||||
this.dropTargetEl.classList.remove("is-drop-before", "is-drop-after", "is-drop-inside");
|
||||
}
|
||||
|
||||
this.dropTargetEl = null;
|
||||
this.dropPosition = null;
|
||||
};
|
||||
|
||||
this.setDropTarget = (row, position) => {
|
||||
if (this.dropTargetEl === row && this.dropPosition === position) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearDropTarget();
|
||||
this.dropTargetEl = row;
|
||||
this.dropPosition = position;
|
||||
row.classList.add(`is-drop-${position}`);
|
||||
};
|
||||
|
||||
this.handleDragStart = (event) => {
|
||||
const handle = event.target.closest("[data-menu-drag-handle='true']");
|
||||
const row = event.target.closest("[data-menu-item-id]");
|
||||
|
||||
if (!handle || !row || !this.el.contains(row)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragItemId = row.dataset.menuItemId || null;
|
||||
this.dragSourceEl = row;
|
||||
row.classList.add("is-dragging");
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", this.dragItemId || "");
|
||||
}
|
||||
};
|
||||
|
||||
this.handleDragOver = (event) => {
|
||||
const row = event.target.closest("[data-menu-item-id]");
|
||||
|
||||
if (!this.dragItemId || !row || !this.el.contains(row)) {
|
||||
this.clearDropTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
const targetItemId = row.dataset.menuItemId || "";
|
||||
|
||||
if (!targetItemId || targetItemId === this.dragItemId) {
|
||||
this.clearDropTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rect = row.getBoundingClientRect();
|
||||
const offsetY = event.clientY - rect.top;
|
||||
const allowInside = row.dataset.menuCanDropInside === "true";
|
||||
const insideBandTop = rect.height * 0.3;
|
||||
const insideBandBottom = rect.height * 0.7;
|
||||
|
||||
const position =
|
||||
allowInside && offsetY >= insideBandTop && offsetY <= insideBandBottom
|
||||
? "inside"
|
||||
: offsetY < rect.height / 2
|
||||
? "before"
|
||||
: "after";
|
||||
|
||||
this.setDropTarget(row, position);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
};
|
||||
|
||||
this.handleDrop = (event) => {
|
||||
const row = event.target.closest("[data-menu-item-id]");
|
||||
|
||||
if (!this.dragItemId || !row || !this.el.contains(row) || !this.dropPosition) {
|
||||
this.clearDropTarget();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.pushEvent("menu_editor_drop_item", {
|
||||
drag_item_id: this.dragItemId,
|
||||
target_item_id: row.dataset.menuItemId,
|
||||
position: this.dropPosition
|
||||
});
|
||||
|
||||
this.clearDropTarget();
|
||||
};
|
||||
|
||||
this.handleDragLeave = (event) => {
|
||||
const related = event.relatedTarget;
|
||||
|
||||
if (this.dropTargetEl && (!related || !this.dropTargetEl.contains(related))) {
|
||||
this.clearDropTarget();
|
||||
}
|
||||
};
|
||||
|
||||
this.handleDragEnd = () => {
|
||||
if (this.dragSourceEl) {
|
||||
this.dragSourceEl.classList.remove("is-dragging");
|
||||
}
|
||||
|
||||
this.dragItemId = null;
|
||||
this.dragSourceEl = null;
|
||||
this.clearDropTarget();
|
||||
};
|
||||
|
||||
this.el.addEventListener("dragstart", this.handleDragStart);
|
||||
this.el.addEventListener("dragover", this.handleDragOver);
|
||||
this.el.addEventListener("drop", this.handleDrop);
|
||||
this.el.addEventListener("dragleave", this.handleDragLeave);
|
||||
this.el.addEventListener("dragend", this.handleDragEnd);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.el.removeEventListener("dragstart", this.handleDragStart);
|
||||
this.el.removeEventListener("dragover", this.handleDragOver);
|
||||
this.el.removeEventListener("drop", this.handleDrop);
|
||||
this.el.removeEventListener("dragleave", this.handleDragLeave);
|
||||
this.el.removeEventListener("dragend", this.handleDragEnd);
|
||||
}
|
||||
};
|
||||
129
assets/js/hooks/monaco_diff_editor.js
Normal file
129
assets/js/hooks/monaco_diff_editor.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { loadMonaco, ensureMonacoTheme, diffModelPath } from "../monaco/services.js";
|
||||
|
||||
export const MonacoDiffEditor = {
|
||||
mounted() {
|
||||
this.host = this.el.querySelector(".monaco-diff-editor-instance");
|
||||
this.originalInput = this.el.querySelector(".monaco-diff-original");
|
||||
this.modifiedInput = this.el.querySelector(".monaco-diff-modified");
|
||||
this.filePath = this.el.dataset.monacoDiffFilePath || "working-tree";
|
||||
this.language = this.el.dataset.monacoDiffLanguage || "plaintext";
|
||||
this.viewStyle = this.el.dataset.monacoDiffViewStyle || "inline";
|
||||
this.wordWrap = this.el.dataset.monacoDiffWordWrap || "off";
|
||||
this.hideUnchanged = this.el.dataset.monacoDiffHideUnchanged === "true";
|
||||
|
||||
this.readValues = () => ({
|
||||
original: this.originalInput?.value || "",
|
||||
modified: this.modifiedInput?.value || ""
|
||||
});
|
||||
|
||||
this.applyDataset = () => {
|
||||
this.filePath = this.el.dataset.monacoDiffFilePath || "working-tree";
|
||||
this.language = this.el.dataset.monacoDiffLanguage || "plaintext";
|
||||
this.viewStyle = this.el.dataset.monacoDiffViewStyle || "inline";
|
||||
this.wordWrap = this.el.dataset.monacoDiffWordWrap || "off";
|
||||
this.hideUnchanged = this.el.dataset.monacoDiffHideUnchanged === "true";
|
||||
};
|
||||
|
||||
this.setModels = (monaco) => {
|
||||
const values = this.readValues();
|
||||
|
||||
this.originalModel?.dispose();
|
||||
this.modifiedModel?.dispose();
|
||||
|
||||
this.originalModel = monaco.editor.createModel(
|
||||
values.original,
|
||||
this.language,
|
||||
monaco.Uri.parse(diffModelPath(this.filePath, "original"))
|
||||
);
|
||||
|
||||
this.modifiedModel = monaco.editor.createModel(
|
||||
values.modified,
|
||||
this.language,
|
||||
monaco.Uri.parse(diffModelPath(this.filePath, "modified"))
|
||||
);
|
||||
|
||||
this.editor.setModel({ original: this.originalModel, modified: this.modifiedModel });
|
||||
this.lastFilePath = this.filePath;
|
||||
};
|
||||
|
||||
loadMonaco()
|
||||
.then((monaco) => {
|
||||
if (!this.host) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureMonacoTheme(monaco);
|
||||
|
||||
this.editor = monaco.editor.createDiffEditor(this.host, {
|
||||
theme: "bds-theme",
|
||||
automaticLayout: true,
|
||||
readOnly: true,
|
||||
renderSideBySide: this.viewStyle === "side-by-side",
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: "on",
|
||||
diffCodeLens: false,
|
||||
originalEditable: false,
|
||||
wordWrap: this.wordWrap,
|
||||
hideUnchangedRegions: { enabled: this.hideUnchanged },
|
||||
ignoreTrimWhitespace: false
|
||||
});
|
||||
|
||||
this.setModels(monaco);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load Monaco diff editor", error);
|
||||
});
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.host = this.el.querySelector(".monaco-diff-editor-instance");
|
||||
this.originalInput = this.el.querySelector(".monaco-diff-original");
|
||||
this.modifiedInput = this.el.querySelector(".monaco-diff-modified");
|
||||
this.applyDataset();
|
||||
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadMonaco().then((monaco) => {
|
||||
ensureMonacoTheme(monaco);
|
||||
monaco.editor.setTheme("bds-theme");
|
||||
|
||||
this.editor.updateOptions({
|
||||
renderSideBySide: this.viewStyle === "side-by-side",
|
||||
wordWrap: this.wordWrap,
|
||||
hideUnchangedRegions: { enabled: this.hideUnchanged }
|
||||
});
|
||||
|
||||
if (this.lastFilePath !== this.filePath) {
|
||||
this.setModels(monaco);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = this.readValues();
|
||||
|
||||
if (this.originalModel && this.originalModel.getLanguageId() !== this.language) {
|
||||
monaco.editor.setModelLanguage(this.originalModel, this.language);
|
||||
}
|
||||
|
||||
if (this.modifiedModel && this.modifiedModel.getLanguageId() !== this.language) {
|
||||
monaco.editor.setModelLanguage(this.modifiedModel, this.language);
|
||||
}
|
||||
|
||||
if (this.originalModel && this.originalModel.getValue() !== values.original) {
|
||||
this.originalModel.setValue(values.original);
|
||||
}
|
||||
|
||||
if (this.modifiedModel && this.modifiedModel.getValue() !== values.modified) {
|
||||
this.modifiedModel.setValue(values.modified);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.originalModel?.dispose();
|
||||
this.modifiedModel?.dispose();
|
||||
this.editor?.dispose();
|
||||
}
|
||||
};
|
||||
238
assets/js/hooks/monaco_editor.js
Normal file
238
assets/js/hooks/monaco_editor.js
Normal file
@@ -0,0 +1,238 @@
|
||||
import {
|
||||
loadMonaco,
|
||||
ensureMonacoTheme,
|
||||
registerMonacoEditor,
|
||||
unregisterMonacoEditor
|
||||
} from "../monaco/services.js";
|
||||
|
||||
export const MonacoEditor = {
|
||||
mounted() {
|
||||
this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea");
|
||||
this.host = this.el.querySelector(".monaco-editor-instance");
|
||||
this.language = this.el.dataset.monacoLanguage || "plaintext";
|
||||
this.wordWrap = this.el.dataset.monacoWordWrap || "off";
|
||||
this.editorId = this.el.dataset.monacoEditorId || "";
|
||||
this.insertEvent = this.el.dataset.monacoInsertEvent || "";
|
||||
this.syncTimer = null;
|
||||
this.isApplyingRemoteUpdate = false;
|
||||
this.lastKnownValue = this.textarea?.value || "";
|
||||
|
||||
this.syncEditorFromTextarea = () => {
|
||||
this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea");
|
||||
|
||||
if (!this.textarea || !this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.textarea.value || "";
|
||||
|
||||
if (this.editor.getValue() !== value) {
|
||||
this.isApplyingRemoteUpdate = true;
|
||||
this.editor.setValue(value);
|
||||
this.isApplyingRemoteUpdate = false;
|
||||
}
|
||||
|
||||
this.lastKnownValue = value;
|
||||
};
|
||||
|
||||
this.layoutEditorSoon = () => {
|
||||
window.requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor.layout();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.waitForMonacoVisibleSize = () =>
|
||||
new Promise((resolve) => {
|
||||
let settled = false;
|
||||
let attempts = 0;
|
||||
|
||||
const hasVisibleSize = () => {
|
||||
const rect = this.host?.getBoundingClientRect();
|
||||
return Boolean(rect && rect.width > 0 && rect.height > 0);
|
||||
};
|
||||
|
||||
const finish = () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
|
||||
settled = true;
|
||||
this.visibleSizeObserver?.disconnect();
|
||||
this.visibleSizeObserver = null;
|
||||
resolve();
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
if (hasVisibleSize() || attempts >= 20) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
attempts += 1;
|
||||
window.requestAnimationFrame(check);
|
||||
};
|
||||
|
||||
if (hasVisibleSize()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.ResizeObserver && this.host) {
|
||||
this.visibleSizeObserver = new ResizeObserver(() => {
|
||||
if (hasVisibleSize()) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
this.visibleSizeObserver.observe(this.host);
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(check);
|
||||
});
|
||||
|
||||
this.queueSync = () => {
|
||||
if (!this.textarea || !this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.clearTimeout(this.syncTimer);
|
||||
this.syncTimer = window.setTimeout(() => {
|
||||
if (!this.textarea || !this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.editor.getValue();
|
||||
|
||||
if (this.textarea.value === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastKnownValue = value;
|
||||
this.textarea.value = value;
|
||||
this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}, 120);
|
||||
};
|
||||
|
||||
this.handleInsert = ({ id, content }) => {
|
||||
if (!this.editor || !content || String(id) !== String(this.editorId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this.editor.getModel();
|
||||
const selection = this.editor.getSelection();
|
||||
|
||||
if (!model || !selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.editor.getValue();
|
||||
const start = model.getOffsetAt(selection.getStartPosition());
|
||||
const end = model.getOffsetAt(selection.getEndPosition());
|
||||
const before = value.slice(0, start);
|
||||
const after = value.slice(end);
|
||||
const separator = before !== "" && !before.endsWith("\n") ? "\n" : "";
|
||||
const suffix = after !== "" && !content.endsWith("\n") ? "\n" : "";
|
||||
const inserted = `${separator}${content}${suffix}`;
|
||||
this.editor.executeEdits("bds-insert-content", [
|
||||
{
|
||||
range: selection,
|
||||
text: inserted,
|
||||
forceMoveMarkers: true
|
||||
}
|
||||
]);
|
||||
this.editor.focus();
|
||||
};
|
||||
|
||||
loadMonaco()
|
||||
.then(async (monaco) => {
|
||||
if (!this.host || !this.textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.waitForMonacoVisibleSize();
|
||||
|
||||
ensureMonacoTheme(monaco);
|
||||
|
||||
this.editor = monaco.editor.create(this.host, {
|
||||
value: this.textarea.value || "",
|
||||
language: this.language,
|
||||
theme: "bds-theme",
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: this.wordWrap,
|
||||
lineNumbers: "on",
|
||||
lineNumbersMinChars: 3,
|
||||
fontSize: 14,
|
||||
fontFamily: "'Cascadia Code', 'Consolas', 'Courier New', monospace",
|
||||
padding: { top: 12, bottom: 12 },
|
||||
roundedSelection: false,
|
||||
renderLineHighlight: "line",
|
||||
formatOnPaste: true,
|
||||
cursorStyle: "line",
|
||||
cursorBlinking: "smooth",
|
||||
quickSuggestions: this.language === "markdown-with-macros" ? false : true,
|
||||
tabSize: 2,
|
||||
insertSpaces: true
|
||||
});
|
||||
|
||||
registerMonacoEditor(this.editorId || this.el.id, this.editor);
|
||||
monaco.editor.setTheme("bds-theme");
|
||||
this.syncEditorFromTextarea();
|
||||
this.layoutEditorSoon();
|
||||
|
||||
this.changeSubscription = this.editor.onDidChangeModelContent(() => {
|
||||
if (this.isApplyingRemoteUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.queueSync();
|
||||
});
|
||||
|
||||
if (this.insertEvent) {
|
||||
this.handleEvent(this.insertEvent, this.handleInsert);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load Monaco editor", error);
|
||||
});
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.textarea = document.getElementById(this.el.dataset.monacoInputId) || this.el.querySelector("textarea");
|
||||
this.host = this.el.querySelector(".monaco-editor-instance");
|
||||
this.language = this.el.dataset.monacoLanguage || this.language || "plaintext";
|
||||
this.wordWrap = this.el.dataset.monacoWordWrap || this.wordWrap || "off";
|
||||
|
||||
if (!this.editor || !this.textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadMonaco().then((monaco) => {
|
||||
ensureMonacoTheme(monaco);
|
||||
monaco.editor.setTheme("bds-theme");
|
||||
|
||||
if (this.editor.getModel()?.getLanguageId() !== this.language) {
|
||||
monaco.editor.setModelLanguage(this.editor.getModel(), this.language);
|
||||
}
|
||||
|
||||
this.editor.updateOptions({ wordWrap: this.wordWrap });
|
||||
});
|
||||
|
||||
this.syncEditorFromTextarea();
|
||||
this.layoutEditorSoon();
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
window.clearTimeout(this.syncTimer);
|
||||
this.visibleSizeObserver?.disconnect();
|
||||
this.changeSubscription?.dispose();
|
||||
unregisterMonacoEditor(this.editorId || this.el.id);
|
||||
this.editor?.dispose();
|
||||
}
|
||||
};
|
||||
31
assets/js/hooks/section_scroll.js
Normal file
31
assets/js/hooks/section_scroll.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const makeSectionScrollHook = (datasetKey) => ({
|
||||
mounted() {
|
||||
this.lastTargetId = null;
|
||||
this.scrollToSelectedSection();
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.scrollToSelectedSection();
|
||||
},
|
||||
|
||||
scrollToSelectedSection() {
|
||||
const targetId = this.el.dataset[datasetKey];
|
||||
|
||||
if (!targetId || targetId === this.lastTargetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastTargetId = targetId;
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
const target = document.getElementById(targetId);
|
||||
|
||||
if (target && this.el.contains(target)) {
|
||||
target.scrollIntoView({ block: "start", behavior: "smooth" });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const SettingsSectionScroll = makeSectionScrollHook("settingsScrollTarget");
|
||||
export const TagsSectionScroll = makeSectionScrollHook("tagsScrollTarget");
|
||||
24
assets/js/hooks/sidebar_interactions.js
Normal file
24
assets/js/hooks/sidebar_interactions.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const SidebarInteractions = {
|
||||
mounted() {
|
||||
this.handleDblClick = (event) => {
|
||||
const button = event.target.closest("[data-testid='sidebar-open-item']");
|
||||
|
||||
if (!button || !this.el.contains(button)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pushEvent("pin_sidebar_item", {
|
||||
route: button.dataset.route,
|
||||
id: button.dataset.itemId,
|
||||
title: button.dataset.openTitle || "",
|
||||
subtitle: button.dataset.openSubtitle || ""
|
||||
});
|
||||
};
|
||||
|
||||
this.el.addEventListener("dblclick", this.handleDblClick);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.el.removeEventListener("dblclick", this.handleDblClick);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user