mirror of
https://github.com/nn-ninja/n-brace.git
synced 2026-07-22 06:42:36 +00:00
some new features
This commit is contained in:
parent
71126d8f19
commit
42e2fed4e5
13 changed files with 1139 additions and 105 deletions
36
README.md
36
README.md
|
|
@ -1,19 +1,41 @@
|
|||
## Obsidian 3D Graph (Yomaru)
|
||||
|
||||
> This is a fork from the original https://github.com/AlexW00/obsidian-3d-graph
|
||||
>
|
||||
> ⚠️ This plugin is still under early development. Please open github issue if you have problems.
|
||||
>
|
||||
> ⚠️ This plugin is still under early development. Please open github issue if you have problems.
|
||||
|
||||
A 3D Graph for Obsidian!
|
||||
|
||||
## Installation
|
||||
see the demo: https://www.youtube.com/watch?v=HnqXH6z4WrY
|
||||
|
||||
### through BRAT
|
||||
## Installation
|
||||
|
||||
### Manual installation
|
||||
### through BRAT
|
||||
|
||||
## Showcase:
|
||||
1. install the BRAT plugin
|
||||
2. go to the plugin option, add beta plugin, copy and paste the link of this repo
|
||||
3. the plugin will automatically appear in the list of installed community plugins, enabled this plugin
|
||||
|
||||
https://www.youtube.com/watch?v=HnqXH6z4WrY
|
||||
### Manual installation
|
||||
|
||||
1. cd to `.obsidian/plugins`
|
||||
2. git clone this repo
|
||||
3. `cd obsidian-3d-graph && bun install && bun run build`
|
||||
4. there you go 🎉
|
||||
|
||||
## Development
|
||||
|
||||
1. cd to `.obsidian/plugins`
|
||||
2. git clone this repo
|
||||
3. `cd obsidian-3d-graph && bun install && bun run dev`
|
||||
4. there you go 🎉
|
||||
|
||||
for release, just run `bun release`
|
||||
|
||||
## Note
|
||||
|
||||
## Say thank you
|
||||
|
||||
If you are enjoying this plugin then please support my work and enthusiasm by buying me a coffee on https://www.buymeacoffee.com/yomaru.
|
||||
|
||||
<a href="https://www.buymeacoffee.com/yomaru" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
|
|
|||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -48,6 +48,7 @@
|
|||
"3d-force-graph": "^1.72.3",
|
||||
"d3": "^7.8.5",
|
||||
"observable-slim": "^0.1.6",
|
||||
"three": "^0.157.0"
|
||||
"three": "^0.157.0",
|
||||
"three-spritetext": "^1.8.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
298
src/typings/types-obsidian.d.ts
vendored
Normal file
298
src/typings/types-obsidian.d.ts
vendored
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import "obsidian";
|
||||
import { EphemeralState, SuggestModal, TFile, View, WorkspaceLeaf } from "obsidian";
|
||||
import { EmbeddedViewParent } from "@/views/leafView";
|
||||
|
||||
interface InternalPlugins {
|
||||
switcher: QuickSwitcherPlugin;
|
||||
"page-preview": InternalPlugin;
|
||||
graph: GraphPlugin;
|
||||
"global-search": GlobalSearchPlugin;
|
||||
}
|
||||
|
||||
interface GlobalSearchPlugin extends Plugin {
|
||||
instance: {
|
||||
getGlobalSearchQuery: () => string;
|
||||
openGlobalSearch: (query: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface OpenViewState {
|
||||
eState?: EphemeralState;
|
||||
state?: { mode: string };
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
declare class QuickSwitcherModal extends SuggestModal<TFile> {
|
||||
getSuggestions(query: string): TFile[] | Promise<TFile[]>;
|
||||
|
||||
renderSuggestion(value: TFile, el: HTMLElement): unknown;
|
||||
|
||||
onChooseSuggestion(item: TFile, evt: MouseEvent | KeyboardEvent): unknown;
|
||||
}
|
||||
|
||||
interface InternalPlugin {
|
||||
disable(): void;
|
||||
|
||||
enable(): void;
|
||||
|
||||
enabled: boolean;
|
||||
_loaded: boolean;
|
||||
instance: { name: string; id: string };
|
||||
}
|
||||
|
||||
interface GraphPlugin extends InternalPlugin {
|
||||
views: { localgraph: (leaf: WorkspaceLeaf) => GraphView };
|
||||
}
|
||||
|
||||
interface GraphView extends View {
|
||||
engine: typeof Object;
|
||||
renderer: { worker: { terminate(): void } };
|
||||
}
|
||||
|
||||
interface QuickSwitcherPlugin extends InternalPlugin {
|
||||
instance: {
|
||||
name: string;
|
||||
id: string;
|
||||
QuickSwitcherModal: typeof QuickSwitcherModal;
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
const i18next: {
|
||||
t(id: string): string;
|
||||
};
|
||||
|
||||
interface Window {
|
||||
activeWindow: Window;
|
||||
activeDocument: Document;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "obsidian" {
|
||||
interface App {
|
||||
commands: {
|
||||
listCommands(): Command[];
|
||||
findCommand(id: string): Command;
|
||||
removeCommand(id: string): void;
|
||||
executeCommandById(id: string): void;
|
||||
commands: Record<string, Command>;
|
||||
};
|
||||
internalPlugins: {
|
||||
plugins: InternalPlugins;
|
||||
getPluginById<T extends keyof InternalPlugins>(id: T): InternalPlugins[T];
|
||||
};
|
||||
plugins: {
|
||||
manifests: Record<string, PluginManifest>;
|
||||
plugins: Record<string, Plugin> & {
|
||||
["recent-files-obsidian"]: Plugin & {
|
||||
shouldAddFile(file: TFile): boolean;
|
||||
};
|
||||
};
|
||||
getPlugin(id: string): Plugin;
|
||||
getPlugin(id: "calendar"): CalendarPlugin;
|
||||
};
|
||||
dom: { appContainerEl: HTMLElement };
|
||||
viewRegistry: ViewRegistry;
|
||||
|
||||
openWithDefaultApp(path: string): void;
|
||||
}
|
||||
|
||||
interface ViewRegistry {
|
||||
typeByExtension: Record<string, string>; // file extensions to view types
|
||||
viewByType: Record<string, (leaf: WorkspaceLeaf) => View>; // file extensions to view types
|
||||
}
|
||||
|
||||
interface CalendarPlugin {
|
||||
view: View;
|
||||
}
|
||||
|
||||
interface WorkspaceParent {
|
||||
insertChild(index: number, child: WorkspaceItem, resize?: boolean): void;
|
||||
|
||||
replaceChild(index: number, child: WorkspaceItem, resize?: boolean): void;
|
||||
|
||||
removeChild(leaf: WorkspaceLeaf, resize?: boolean): void;
|
||||
|
||||
containerEl: HTMLElement;
|
||||
children: any;
|
||||
}
|
||||
|
||||
interface MarkdownEditView {
|
||||
editorEl: HTMLElement;
|
||||
}
|
||||
|
||||
class MarkdownPreviewRendererStatic extends MarkdownPreviewRenderer {
|
||||
static registerDomEvents(
|
||||
el: HTMLElement,
|
||||
handlerInstance: unknown,
|
||||
cb: (el: HTMLElement) => unknown
|
||||
): void;
|
||||
}
|
||||
|
||||
interface WorkspaceLeaf {
|
||||
openLinkText(linkText: string, path: string, state?: unknown): Promise<void>;
|
||||
|
||||
updateHeader(): void;
|
||||
|
||||
containerEl: HTMLDivElement;
|
||||
working: boolean;
|
||||
parentSplit: WorkspaceParent;
|
||||
activeTime: number;
|
||||
}
|
||||
|
||||
interface Workspace {
|
||||
recordHistory(leaf: WorkspaceLeaf, pushHistory: boolean): void;
|
||||
|
||||
iterateLeaves(
|
||||
callback: (item: WorkspaceLeaf) => boolean | void,
|
||||
item: WorkspaceItem | WorkspaceItem[]
|
||||
): boolean;
|
||||
|
||||
iterateLeaves(
|
||||
item: WorkspaceItem | WorkspaceItem[],
|
||||
callback: (item: WorkspaceLeaf) => boolean | void
|
||||
): boolean;
|
||||
|
||||
getDropLocation(event: MouseEvent): {
|
||||
target: WorkspaceItem;
|
||||
sidedock: boolean;
|
||||
};
|
||||
|
||||
recursiveGetTarget(event: MouseEvent, parent: WorkspaceParent): WorkspaceItem;
|
||||
|
||||
recordMostRecentOpenedFile(file: TFile): void;
|
||||
|
||||
onDragLeaf(event: MouseEvent, leaf: WorkspaceLeaf): void;
|
||||
|
||||
onLayoutChange(): void; // tell Obsidian leaves have been added/removed/etc.
|
||||
activeLeafEvents(): void;
|
||||
|
||||
floatingSplit: any;
|
||||
|
||||
pushUndoHistory(leaf: WorkspaceLeaf, id: string, e: any): void;
|
||||
}
|
||||
|
||||
interface Editor {
|
||||
getClickableTokenAt(pos: EditorPosition): {
|
||||
text: string;
|
||||
type: string;
|
||||
start: EditorPosition;
|
||||
end: EditorPosition;
|
||||
};
|
||||
}
|
||||
|
||||
interface WorkspaceItem {
|
||||
side?: "left" | "right";
|
||||
}
|
||||
|
||||
interface View {
|
||||
iconEl: HTMLElement;
|
||||
file: TFile;
|
||||
|
||||
setMode(mode: MarkdownSubView): Promise<void>;
|
||||
|
||||
followLinkUnderCursor(newLeaf: boolean): void;
|
||||
|
||||
modes: Record<string, MarkdownSubView>;
|
||||
|
||||
getMode(): string;
|
||||
|
||||
headerEl: HTMLElement;
|
||||
contentEl: HTMLElement;
|
||||
}
|
||||
|
||||
interface SearchView extends View {
|
||||
onKeyArrowRightInFocus(e: KeyboardEvent): void;
|
||||
|
||||
onKeyArrowLeftInFocus(e: KeyboardEvent): void;
|
||||
|
||||
onKeyArrowUpInFocus(e: KeyboardEvent): void;
|
||||
|
||||
onKeyArrowDownInFocus(e: KeyboardEvent): void;
|
||||
|
||||
onKeyEnterInFocus(e: KeyboardEvent): void;
|
||||
|
||||
onKeyShowMoreBefore(e: KeyboardEvent): void;
|
||||
|
||||
onKeyShowMoreAfter(e: KeyboardEvent): void;
|
||||
|
||||
dom: any;
|
||||
searchComponent: SearchComponent;
|
||||
headerDom: any;
|
||||
}
|
||||
|
||||
interface EmptyView extends View {
|
||||
actionListEl: HTMLElement;
|
||||
emptyTitleEl: HTMLElement;
|
||||
}
|
||||
|
||||
interface FileManager {
|
||||
createNewMarkdownFile(folder: TFolder, fileName: string): Promise<TFile>;
|
||||
}
|
||||
|
||||
enum PopoverState {
|
||||
Showing,
|
||||
Shown,
|
||||
Hiding,
|
||||
Hidden,
|
||||
}
|
||||
|
||||
interface Menu {
|
||||
items: MenuItem[];
|
||||
dom: HTMLElement;
|
||||
hideCallback: () => unknown;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
iconEl: HTMLElement;
|
||||
dom: HTMLElement;
|
||||
}
|
||||
|
||||
interface EphemeralState {
|
||||
focus?: boolean;
|
||||
subpath?: string;
|
||||
line?: number;
|
||||
startLoc?: Loc;
|
||||
endLoc?: Loc;
|
||||
scroll?: number;
|
||||
}
|
||||
|
||||
interface HoverParent {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface WorkspaceContainer {
|
||||
type: "window" | "split" | "tab";
|
||||
}
|
||||
|
||||
interface HoverPopover {
|
||||
parent: EmbeddedViewParent | null;
|
||||
targetEl: HTMLElement;
|
||||
hoverEl: HTMLElement;
|
||||
|
||||
position(pos?: MousePos): void;
|
||||
|
||||
hide(): void;
|
||||
|
||||
show(): void;
|
||||
|
||||
shouldShowSelf(): boolean;
|
||||
|
||||
timer: number;
|
||||
waitTime: number;
|
||||
|
||||
shouldShow(): boolean;
|
||||
|
||||
transition(): void;
|
||||
}
|
||||
|
||||
interface MousePos {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Plugin {
|
||||
registerGlobalCommand(command: Command): void;
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +76,8 @@ export class State<T> {
|
|||
}, this as Record<string, unknown>) as S;
|
||||
if (typeof subStateValue === "object") {
|
||||
// check if is like generic type S
|
||||
if (subStateValue instanceof type) {
|
||||
// @ts-ignore
|
||||
if (subStateValue instanceof type || Boolean(subStateValue?.__getTarget)) {
|
||||
// @ts-ignore
|
||||
return new State(subStateValue.__getTarget);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,11 +11,15 @@ import { eventBus } from "@/util/EventBus";
|
|||
import { GraphSettings } from "@/settings/GraphSettings";
|
||||
import { DagOrientation } from "@/settings/categories/DisplaySettings";
|
||||
import * as d3 from "d3";
|
||||
import * as THREE from "three";
|
||||
|
||||
const LINK_PARTICLE_MULTIPLIER = 2;
|
||||
const LINK_ARROW_WIDTH_MULTIPLIER = 5;
|
||||
const PARTICLE_FREQUECY = 4;
|
||||
|
||||
const FOCAL_FROM_CAMERA = 400;
|
||||
const DISTANCE_FROM_FOCAL = 300;
|
||||
|
||||
// Adapted from https://github.com/vasturiano/3d-force-graph/blob/master/example/highlight/index.html
|
||||
// D3.js 3D Force Graph
|
||||
|
||||
|
|
@ -36,13 +40,15 @@ export class ForceGraph {
|
|||
private readonly isLocalGraph: boolean;
|
||||
private graph: Graph;
|
||||
private readonly plugin: Graph3dPlugin;
|
||||
private myCube: THREE.Mesh;
|
||||
private nodeLabelEl: HTMLDivElement;
|
||||
|
||||
constructor(plugin: Graph3dPlugin, rootHtmlElement: HTMLElement, isLocalGraph: boolean) {
|
||||
this.rootHtmlElement = rootHtmlElement;
|
||||
this.isLocalGraph = isLocalGraph;
|
||||
this.plugin = plugin;
|
||||
|
||||
console.log("ForceGraph constructor", rootHtmlElement);
|
||||
// console.log("ForceGraph constructor", rootHtmlElement);
|
||||
|
||||
this.createGraph();
|
||||
this.initListeners();
|
||||
|
|
@ -67,15 +73,68 @@ export class ForceGraph {
|
|||
}
|
||||
|
||||
private createGraph() {
|
||||
const xDir = new THREE.Vector3(1, 0, 0);
|
||||
const yDir = new THREE.Vector3(0, 1, 0);
|
||||
const zDir = new THREE.Vector3(0, 0, 1);
|
||||
|
||||
const myCube = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(30, 30, 30),
|
||||
new THREE.MeshBasicMaterial({ color: 0xff0000 })
|
||||
);
|
||||
this.myCube = myCube;
|
||||
myCube.position.set(0, 0, -300);
|
||||
myCube.visible = false;
|
||||
|
||||
xDir.normalize();
|
||||
yDir.normalize();
|
||||
zDir.normalize();
|
||||
|
||||
const origin = new THREE.Vector3(0, 0, 0);
|
||||
const length = 100;
|
||||
|
||||
const xArrow = new THREE.ArrowHelper(xDir, origin, length, 0xff0000);
|
||||
const yArrow = new THREE.ArrowHelper(yDir, origin, length, 0x00ff00);
|
||||
const zArrow = new THREE.ArrowHelper(zDir, origin, length, 0x0000ff);
|
||||
|
||||
this.createInstance();
|
||||
this.createNodes();
|
||||
this.createLinks();
|
||||
// this.updateForce(settings.display.dagOrientation, settings.display.nodeRepulsion);
|
||||
this.instance.scene().add(xArrow).add(yArrow).add(zArrow).add(myCube);
|
||||
const camera = this.instance.camera() as THREE.PerspectiveCamera;
|
||||
const renderer = this.instance.renderer();
|
||||
function onZoom(event: WheelEvent) {
|
||||
// const delta = event.deltaY;
|
||||
// const zoomSpeed = 0.1;
|
||||
const distanceToCenter = camera.position.distanceTo(origin);
|
||||
// console.log(camera.position, distanceToCenter);
|
||||
camera.updateProjectionMatrix();
|
||||
xArrow.setLength(distanceToCenter / 10);
|
||||
yArrow.setLength(distanceToCenter / 10);
|
||||
zArrow.setLength(distanceToCenter / 10);
|
||||
}
|
||||
|
||||
renderer.domElement.addEventListener("wheel", onZoom);
|
||||
|
||||
this.instance.scene().onBeforeRender = (renderer, scene, camera, geometry, material, group) => {
|
||||
const cwd = new THREE.Vector3();
|
||||
camera.getWorldDirection(cwd);
|
||||
cwd.multiplyScalar(FOCAL_FROM_CAMERA);
|
||||
cwd.add(camera.position);
|
||||
myCube.position.set(cwd.x, cwd.y, cwd.z);
|
||||
myCube.setRotationFromQuaternion(camera.quaternion);
|
||||
};
|
||||
}
|
||||
|
||||
private createInstance() {
|
||||
const [width, height] = [this.rootHtmlElement.innerWidth, this.rootHtmlElement.innerHeight];
|
||||
const divEl = document.createElement("div");
|
||||
const nodeLabelEl = divEl.createDiv({
|
||||
cls: "node-label",
|
||||
text: "test",
|
||||
});
|
||||
nodeLabelEl.style.opacity = "0";
|
||||
this.nodeLabelEl = nodeLabelEl;
|
||||
|
||||
// set the divEl to have z-index 0
|
||||
divEl.style.zIndex = "0";
|
||||
const settings = this.plugin.getSettings();
|
||||
|
|
@ -88,9 +147,7 @@ export class ForceGraph {
|
|||
],
|
||||
})(this.rootHtmlElement)
|
||||
.graphData(this.getGraphData())
|
||||
// .nodeLabel(
|
||||
// (node: Node) => `<div class="node-label">${node.name}</div>`
|
||||
// )
|
||||
// .nodeLabel((node: Node) => `<div class="node-label">${node.name}</div>`)
|
||||
// @ts-ignore we need to return null or empty string because by default it will access the name of node, see https://github.com/vasturiano/3d-force-graph#node-styling
|
||||
.nodeLabel((node: Node) => null)
|
||||
.nodeRelSize(settings.display.nodeSize)
|
||||
|
|
@ -106,28 +163,35 @@ export class ForceGraph {
|
|||
// .d3Force("y", d3.forceY().strength(0.1));
|
||||
}
|
||||
|
||||
private createNodes = () => {
|
||||
private getNodeLabelText = (node: Node) => {
|
||||
const settings = this.plugin.getSettings();
|
||||
const fullPath = node.path;
|
||||
const fileNameWithExtension = node.name;
|
||||
const fullPathWithoutExtension = fullPath.substring(0, fullPath.lastIndexOf("."));
|
||||
const fileNameWithoutExtension = fileNameWithExtension.substring(
|
||||
0,
|
||||
fileNameWithExtension.lastIndexOf(".")
|
||||
);
|
||||
const text = !settings.display.showExtension
|
||||
? settings.display.showFullPath
|
||||
? fullPathWithoutExtension
|
||||
: fileNameWithoutExtension
|
||||
: settings.display.showFullPath
|
||||
? fullPath
|
||||
: fileNameWithExtension;
|
||||
return text;
|
||||
};
|
||||
|
||||
private createNodes = () => {
|
||||
this.instance
|
||||
.nodeColor((node: Node) => this.getNodeColor(node))
|
||||
// .nodeVisibility(this.doShowNode)
|
||||
.onNodeHover(this.onNodeHover)
|
||||
.nodeThreeObject((node: Node) => {
|
||||
const nodeEl = document.createElement("div");
|
||||
const fullPath = node.path;
|
||||
const fileNameWithExtension = node.name;
|
||||
const fullPathWithoutExtension = fullPath.substring(0, fullPath.lastIndexOf("."));
|
||||
const fileNameWithoutExtension = fileNameWithExtension.substring(
|
||||
0,
|
||||
fileNameWithExtension.lastIndexOf(".")
|
||||
);
|
||||
nodeEl.textContent = !settings.display.showExtension
|
||||
? settings.display.showFullPath
|
||||
? fullPathWithoutExtension
|
||||
: fileNameWithoutExtension
|
||||
: settings.display.showFullPath
|
||||
? fullPath
|
||||
: fileNameWithExtension;
|
||||
|
||||
const text = this.getNodeLabelText(node);
|
||||
nodeEl.textContent = text;
|
||||
// @ts-ignore
|
||||
nodeEl.style.color = node.color;
|
||||
nodeEl.className = "node-label";
|
||||
|
|
@ -137,7 +201,34 @@ export class ForceGraph {
|
|||
nodeEl.style.borderRadius = "4px";
|
||||
nodeEl.style.backgroundColor = rgba(0, 0, 0, 0.5);
|
||||
nodeEl.style.userSelect = "none";
|
||||
return new CSS2DObject(nodeEl);
|
||||
|
||||
// const sprite = new SpriteText(text);
|
||||
// sprite.color = "#8898aa";
|
||||
// sprite.textHeight = 12;
|
||||
// sprite.position.y = 8;
|
||||
// sprite.material.depthWrite = false;
|
||||
// sprite.visible
|
||||
|
||||
// return sprite;
|
||||
|
||||
const cssObject = new CSS2DObject(nodeEl);
|
||||
cssObject.onAfterRender = (renderer, scene, camera) => {
|
||||
// get the position of the node
|
||||
// @ts-ignore
|
||||
const obj = node.__threeObj as THREE.Object3D | undefined;
|
||||
const nodePosition = obj?.position as THREE.Vector3;
|
||||
// then get the distance between the node and this.myCube , console.log it
|
||||
const distance = nodePosition.distanceTo(this.myCube.position);
|
||||
// change the opacity of the nodeEl base on the distance
|
||||
// the higher the distance, the lower the opacity
|
||||
// when the distance is 300, the opacity is 0
|
||||
// console.log(distance);
|
||||
const normalizedDistance = Math.min(distance, DISTANCE_FROM_FOCAL) / DISTANCE_FROM_FOCAL;
|
||||
const easedValue = 0.5 - 0.5 * Math.cos(normalizedDistance * Math.PI);
|
||||
nodeEl.style.opacity = `${1 - easedValue}`;
|
||||
};
|
||||
|
||||
return cssObject;
|
||||
})
|
||||
.nodeThreeObjectExtend(true);
|
||||
};
|
||||
|
|
@ -150,7 +241,6 @@ export class ForceGraph {
|
|||
? settings.display.linkThickness * 1.5
|
||||
: settings.display.linkThickness
|
||||
)
|
||||
// .linkVisibility(this.doShowLink)
|
||||
.linkDirectionalParticles((link: Link) =>
|
||||
this.isHighlightedLink(link) ? PARTICLE_FREQUECY : 0
|
||||
)
|
||||
|
|
@ -205,9 +295,9 @@ export class ForceGraph {
|
|||
const settings = this.plugin.getSettings();
|
||||
if (this.isLocalGraph && this.plugin.openFileState.value) {
|
||||
this.graph = this.plugin.globalGraph.clone().getLocalGraph(this.plugin.openFileState.value);
|
||||
console.log(this.graph);
|
||||
// console.log(this.graph);
|
||||
} else {
|
||||
console.log(settings.filters.searchResult);
|
||||
// console.log(settings.filters.searchResult);
|
||||
this.graph = this.plugin.globalGraph.filter((node) => {
|
||||
return (
|
||||
(settings.filters.searchResult.length === 0 ||
|
||||
|
|
@ -224,7 +314,7 @@ export class ForceGraph {
|
|||
};
|
||||
|
||||
private refreshGraphData = () => {
|
||||
console.log("refresh graph data");
|
||||
// console.log("refresh graph data");
|
||||
this.instance.graphData(this.getGraphData());
|
||||
};
|
||||
|
||||
|
|
@ -288,6 +378,17 @@ export class ForceGraph {
|
|||
private onNodeHover = (node: Node | null) => {
|
||||
if ((!node && !this.highlightedNodes.size) || (node && this.hoveredNode === node)) return;
|
||||
|
||||
// set node label text
|
||||
if (node) {
|
||||
const text = this.getNodeLabelText(node);
|
||||
this.nodeLabelEl.textContent = text;
|
||||
// @ts-ignore
|
||||
this.nodeLabelEl.style.color = node.color;
|
||||
this.nodeLabelEl.style.opacity = "1";
|
||||
} else {
|
||||
this.nodeLabelEl.style.opacity = "0";
|
||||
}
|
||||
|
||||
this.clearHighlights();
|
||||
|
||||
if (node) {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export class Graph3dView extends ItemView {
|
|||
const settings = new GraphSettingsView(
|
||||
this.plugin.settingsState,
|
||||
this.plugin.theme,
|
||||
this.plugin.app
|
||||
this.plugin
|
||||
);
|
||||
viewContent.appendChild(settings);
|
||||
} else {
|
||||
|
|
|
|||
598
src/views/leafView.ts
Normal file
598
src/views/leafView.ts
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// Original code from https://github.com/nothingislost/obsidian-hover-editor/blob/9ec3449be9ab3433dc46c4c3acfde1da72ff0261/src/popover.ts
|
||||
// You can use this file as a basic leaf view create method in anywhere
|
||||
// Please rememeber if you want to use this file, you should patch the types-obsidian.d.ts file
|
||||
// And also monkey around the Obsidian original method.
|
||||
import {
|
||||
Component,
|
||||
EphemeralState,
|
||||
HoverPopover,
|
||||
MarkdownEditView,
|
||||
parseLinktext,
|
||||
Plugin,
|
||||
PopoverState,
|
||||
requireApiVersion,
|
||||
resolveSubpath,
|
||||
TFile,
|
||||
View,
|
||||
Workspace,
|
||||
WorkspaceLeaf,
|
||||
WorkspaceSplit,
|
||||
WorkspaceTabs,
|
||||
} from "obsidian";
|
||||
import type { OpenViewState } from "@/typings/types-obsidian";
|
||||
|
||||
const PREFIX = "graph-3d";
|
||||
|
||||
export interface EmbeddedViewParent {
|
||||
hoverPopover: EmbeddedView | null;
|
||||
containerEl?: HTMLElement;
|
||||
view?: View;
|
||||
dom?: HTMLElement;
|
||||
}
|
||||
const popovers = new WeakMap<Element, EmbeddedView>();
|
||||
type ConstructableWorkspaceSplit = new (
|
||||
ws: Workspace,
|
||||
dir: "horizontal" | "vertical"
|
||||
) => WorkspaceSplit;
|
||||
|
||||
export function isEmebeddedLeaf(leaf: WorkspaceLeaf) {
|
||||
// Work around missing enhance.js API by checking match condition instead of looking up parent
|
||||
return leaf.containerEl.matches(`.${PREFIX}-block.${PREFIX}-leaf-view .workspace-leaf`);
|
||||
}
|
||||
|
||||
function genId(size: number): string {
|
||||
const chars = [];
|
||||
for (let n = 0; n < size; n++) chars.push(((16 * Math.random()) | 0).toString(16));
|
||||
return chars.join("");
|
||||
}
|
||||
|
||||
function nosuper<T>(base: new (...args: unknown[]) => T): new () => T {
|
||||
const derived = function () {
|
||||
return Object.setPrototypeOf(new Component(), new.target.prototype);
|
||||
};
|
||||
derived.prototype = base.prototype;
|
||||
return Object.setPrototypeOf(derived, base);
|
||||
}
|
||||
|
||||
export const spawnLeafView = (
|
||||
plugin: Plugin,
|
||||
initiatingEl?: HTMLElement,
|
||||
leaf?: WorkspaceLeaf,
|
||||
onShowCallback?: () => unknown,
|
||||
props?: {
|
||||
hoverElClasses?: string[];
|
||||
containerElClasses?: string[];
|
||||
}
|
||||
): [WorkspaceLeaf, EmbeddedView] => {
|
||||
// When Obsidian doesn't set any leaf active, use leaf instead.
|
||||
let parent = app.workspace.activeLeaf as unknown as EmbeddedViewParent;
|
||||
if (!parent) parent = leaf as unknown as EmbeddedViewParent;
|
||||
|
||||
if (!initiatingEl) initiatingEl = parent?.containerEl;
|
||||
|
||||
const hoverPopover = new EmbeddedView(
|
||||
parent,
|
||||
initiatingEl!,
|
||||
plugin,
|
||||
undefined,
|
||||
onShowCallback,
|
||||
props
|
||||
);
|
||||
return [hoverPopover.attachLeaf(), hoverPopover];
|
||||
};
|
||||
|
||||
export class EmbeddedView extends nosuper(HoverPopover) {
|
||||
onTarget: boolean;
|
||||
setActive: (event: MouseEvent) => void;
|
||||
|
||||
lockedOut: boolean;
|
||||
abortController? = this.addChild(new Component());
|
||||
detaching = false;
|
||||
opening = false;
|
||||
|
||||
rootSplit: WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(
|
||||
window.app.workspace,
|
||||
"vertical"
|
||||
);
|
||||
|
||||
isPinned = true;
|
||||
|
||||
titleEl: HTMLElement;
|
||||
containerEl: HTMLElement;
|
||||
|
||||
// It is currently not useful.
|
||||
// leafInHoverEl: WorkspaceLeaf;
|
||||
|
||||
oldPopover = this.parent?.hoverPopover;
|
||||
document: Document = this.targetEl?.ownerDocument ?? window.activeDocument ?? window.document;
|
||||
|
||||
id = genId(8);
|
||||
// @ts-ignore
|
||||
bounce?: NodeJS.Timeout;
|
||||
boundOnZoomOut: () => void;
|
||||
|
||||
originalPath: string; // these are kept to avoid adopting targets w/a different link
|
||||
originalLinkText: string;
|
||||
static activePopover?: EmbeddedView;
|
||||
|
||||
static activeWindows() {
|
||||
const windows: Window[] = [window];
|
||||
const { floatingSplit } = app.workspace;
|
||||
if (floatingSplit) {
|
||||
for (const split of floatingSplit.children) {
|
||||
if (split.win) windows.push(split.win);
|
||||
}
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
|
||||
static containerForDocument(doc: Document) {
|
||||
if (doc !== document && app.workspace.floatingSplit)
|
||||
for (const container of app.workspace.floatingSplit.children) {
|
||||
if (container.doc === doc) return container;
|
||||
}
|
||||
return app.workspace.rootSplit;
|
||||
}
|
||||
|
||||
static activePopovers() {
|
||||
return this.activeWindows().flatMap(this.popoversForWindow);
|
||||
}
|
||||
|
||||
static popoversForWindow(win?: Window) {
|
||||
return (
|
||||
Array.prototype.slice.call(
|
||||
win?.document?.body.querySelectorAll(`.${PREFIX}-leaf-view`) ?? []
|
||||
) as HTMLElement[]
|
||||
)
|
||||
.map((el) => popovers.get(el)!)
|
||||
.filter((he) => he);
|
||||
}
|
||||
|
||||
static forLeaf(leaf: WorkspaceLeaf | undefined) {
|
||||
// leaf can be null such as when right clicking on an internal link
|
||||
const el = leaf && document.body.matchParent.call(leaf.containerEl, `.${PREFIX}-leaf-view`); // work around matchParent race condition
|
||||
return el ? popovers.get(el) : undefined;
|
||||
}
|
||||
|
||||
static iteratePopoverLeaves(ws: Workspace, cb: (leaf: WorkspaceLeaf) => boolean | void) {
|
||||
for (const popover of this.activePopovers()) {
|
||||
if (popover.rootSplit && ws.iterateLeaves(cb, popover.rootSplit)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
hoverEl: HTMLElement = this.document.defaultView!.createDiv({
|
||||
cls: `${PREFIX}-block ${PREFIX}-leaf-view`,
|
||||
attr: { id: `${PREFIX}-` + this.id },
|
||||
});
|
||||
|
||||
constructor(
|
||||
parent: EmbeddedViewParent,
|
||||
public targetEl: HTMLElement,
|
||||
public plugin: Plugin,
|
||||
waitTime?: number,
|
||||
public onShowCallback?: () => unknown,
|
||||
props?: {
|
||||
hoverElClasses?: string[];
|
||||
containerElClasses?: string[];
|
||||
}
|
||||
) {
|
||||
//
|
||||
super();
|
||||
|
||||
if (waitTime === undefined) {
|
||||
waitTime = 300;
|
||||
}
|
||||
this.onTarget = true;
|
||||
|
||||
this.parent = parent;
|
||||
this.waitTime = waitTime;
|
||||
this.state = PopoverState.Showing;
|
||||
|
||||
this.abortController!.load();
|
||||
this.show();
|
||||
this.onShow();
|
||||
|
||||
this.setActive = this._setActive.bind(this);
|
||||
// if (hoverEl) {
|
||||
// hoverEl.addEventListener("mousedown", this.setActive);
|
||||
// }
|
||||
// custom logic begin
|
||||
popovers.set(this.hoverEl, this);
|
||||
if (props?.hoverElClasses) {
|
||||
this.hoverEl.addClass(...props.hoverElClasses);
|
||||
}
|
||||
this.containerEl = this.hoverEl.createDiv({ cls: props?.containerElClasses });
|
||||
this.buildWindowControls();
|
||||
this.setInitialDimensions();
|
||||
}
|
||||
|
||||
_setActive(evt: MouseEvent) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
// @ts-ignore
|
||||
this.plugin.app.workspace.setActiveLeaf(this.leaves()[0], {
|
||||
focus: true,
|
||||
});
|
||||
}
|
||||
|
||||
getDefaultMode() {
|
||||
// return this.parent?.view?.getMode ? this.parent.view.getMode() : "source";
|
||||
return "source";
|
||||
}
|
||||
|
||||
updateLeaves() {
|
||||
if (this.onTarget && this.targetEl && !this.document.contains(this.targetEl)) {
|
||||
this.onTarget = false;
|
||||
this.transition();
|
||||
}
|
||||
let leafCount = 0;
|
||||
this.plugin.app.workspace.iterateLeaves((leaf) => {
|
||||
leafCount++;
|
||||
}, this.rootSplit);
|
||||
|
||||
if (leafCount === 0) {
|
||||
this.hide(); // close if we have no leaves
|
||||
}
|
||||
this.hoverEl.setAttribute("data-leaf-count", leafCount.toString());
|
||||
}
|
||||
|
||||
leaves() {
|
||||
const leaves: WorkspaceLeaf[] = [];
|
||||
this.plugin.app.workspace.iterateLeaves((leaf) => {
|
||||
leaves.push(leaf);
|
||||
}, this.rootSplit);
|
||||
return leaves;
|
||||
}
|
||||
|
||||
setInitialDimensions() {
|
||||
this.hoverEl.style.height = "auto";
|
||||
this.hoverEl.style.width = "100%";
|
||||
}
|
||||
|
||||
transition() {
|
||||
if (this.shouldShow()) {
|
||||
if (this.state === PopoverState.Hiding) {
|
||||
this.state = PopoverState.Shown;
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
} else {
|
||||
if (this.state === PopoverState.Showing) {
|
||||
this.hide();
|
||||
} else {
|
||||
if (this.state === PopoverState.Shown) {
|
||||
this.state = PopoverState.Hiding;
|
||||
this.timer = window.setTimeout(() => {
|
||||
if (this.shouldShow()) {
|
||||
this.transition();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}, this.waitTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildWindowControls() {
|
||||
this.titleEl = this.document.defaultView!.createDiv("popover-titlebar");
|
||||
this.titleEl.createDiv("popover-title");
|
||||
|
||||
this.containerEl.prepend(this.titleEl);
|
||||
}
|
||||
|
||||
attachLeaf(): WorkspaceLeaf {
|
||||
this.rootSplit.getRoot = () =>
|
||||
this.plugin.app.workspace[this.document === document ? "rootSplit" : "floatingSplit"]!;
|
||||
this.rootSplit.getContainer = () => EmbeddedView.containerForDocument(this.document);
|
||||
|
||||
this.titleEl.insertAdjacentElement("afterend", this.rootSplit.containerEl);
|
||||
const leaf = this.plugin.app.workspace.createLeafInParent(this.rootSplit, 0);
|
||||
|
||||
this.updateLeaves();
|
||||
return leaf;
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
super.onload();
|
||||
this.registerEvent(this.plugin.app.workspace.on("layout-change", this.updateLeaves, this));
|
||||
this.registerEvent(
|
||||
app.workspace.on("layout-change", () => {
|
||||
// Ensure that top-level items in a popover are not tabbed
|
||||
// @ts-ignore
|
||||
this.rootSplit.children.forEach((item: any, index: any) => {
|
||||
if (item instanceof WorkspaceTabs) {
|
||||
this.rootSplit.replaceChild(index, item.children[0]);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
// Once we've been open for closeDelay, use the closeDelay as a hiding timeout
|
||||
const closeDelay = 600;
|
||||
setTimeout(() => (this.waitTime = closeDelay), closeDelay);
|
||||
|
||||
this.oldPopover?.hide();
|
||||
this.oldPopover = null;
|
||||
|
||||
this.hoverEl.toggleClass("is-new", true);
|
||||
|
||||
this.document.body.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
this.hoverEl.toggleClass("is-new", false);
|
||||
},
|
||||
{ once: true, capture: true }
|
||||
);
|
||||
|
||||
if (this.parent) {
|
||||
this.parent.hoverPopover = this;
|
||||
}
|
||||
|
||||
// Remove original view header;
|
||||
const viewHeaderEl = this.hoverEl.querySelector(".view-header");
|
||||
viewHeaderEl?.remove();
|
||||
|
||||
const sizer = this.hoverEl.querySelector(".workspace-leaf");
|
||||
if (sizer) this.hoverEl.appendChild(sizer);
|
||||
|
||||
// Remove original inline tilte;
|
||||
const inlineTitle = this.hoverEl.querySelector(".inline-title");
|
||||
if (inlineTitle) inlineTitle.remove();
|
||||
|
||||
this.onShowCallback?.();
|
||||
this.onShowCallback = undefined; // only call it once
|
||||
}
|
||||
|
||||
detect(el: HTMLElement) {
|
||||
// TODO: may not be needed? the mouseover/out handers handle most detection use cases
|
||||
const { targetEl } = this;
|
||||
|
||||
if (targetEl) {
|
||||
this.onTarget = el === targetEl || targetEl.contains(el);
|
||||
}
|
||||
}
|
||||
|
||||
shouldShow() {
|
||||
return this.shouldShowSelf() || this.shouldShowChild();
|
||||
}
|
||||
|
||||
shouldShowChild(): boolean {
|
||||
return EmbeddedView.activePopovers().some((popover) => {
|
||||
if (popover !== this && popover.targetEl && this.hoverEl.contains(popover.targetEl)) {
|
||||
return popover.shouldShow();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
shouldShowSelf() {
|
||||
// Don't let obsidian show() us if we've already started closing
|
||||
// return !this.detaching && (this.onTarget || this.onHover);
|
||||
return (
|
||||
!this.detaching &&
|
||||
!!(
|
||||
this.onTarget ||
|
||||
this.state == PopoverState.Shown ||
|
||||
this.document.querySelector(
|
||||
`body>.modal-container, body > #he${this.id} ~ .menu, body > #he${this.id} ~ .suggestion-container`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
show() {
|
||||
// native obsidian logic start
|
||||
// if (!this.targetEl || this.document.body.contains(this.targetEl)) {
|
||||
this.state = PopoverState.Shown;
|
||||
this.timer = 0;
|
||||
|
||||
this.targetEl.appendChild(this.hoverEl);
|
||||
this.onShow();
|
||||
app.workspace.onLayoutChange();
|
||||
|
||||
// initializingHoverPopovers.remove(this);
|
||||
// activeHoverPopovers.push(this);
|
||||
// initializePopoverChecker();
|
||||
this.load();
|
||||
// }
|
||||
// native obsidian logic end
|
||||
|
||||
// if this is an image view, set the dimensions to the natural dimensions of the image
|
||||
// an interactjs reflow will be triggered to constrain the image to the viewport if it's
|
||||
// too large
|
||||
if (this.hoverEl.dataset.imgHeight && this.hoverEl.dataset.imgWidth) {
|
||||
this.hoverEl.style.height =
|
||||
parseFloat(this.hoverEl.dataset.imgHeight) + this.titleEl.offsetHeight + "px";
|
||||
this.hoverEl.style.width = parseFloat(this.hoverEl.dataset.imgWidth) + "px";
|
||||
}
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.oldPopover = null;
|
||||
if (this.parent?.hoverPopover === this) {
|
||||
this.parent.hoverPopover = null;
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.onTarget = false;
|
||||
this.detaching = true;
|
||||
// Once we reach this point, we're committed to closing
|
||||
|
||||
// in case we didn't ever call show()
|
||||
|
||||
// A timer might be pending to call show() for the first time, make sure
|
||||
// it doesn't bring us back up after we close
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
}
|
||||
|
||||
// Hide our HTML element immediately, even if our leaves might not be
|
||||
// detachable yet. This makes things more responsive and improves the
|
||||
// odds of not showing an empty popup that's just going to disappear
|
||||
// momentarily.
|
||||
this.hoverEl.hide();
|
||||
|
||||
// If a file load is in progress, we need to wait until it's finished before
|
||||
// detaching leaves. Because we set .detaching, The in-progress openFile()
|
||||
// will call us again when it finishes.
|
||||
if (this.opening) return;
|
||||
|
||||
// Leave this code here to observe the state of the leaves
|
||||
const leaves = this.leaves();
|
||||
if (leaves.length) {
|
||||
// Detach all leaves before we unload the popover and remove it from the DOM.
|
||||
// Each leaf.detach() will trigger layout-changed and the updateLeaves()
|
||||
// method will then call hide() again when the last one is gone.
|
||||
// leaves[0].detach();
|
||||
// leaves[0].detach();
|
||||
this.targetEl.empty();
|
||||
} else {
|
||||
this.parent = null;
|
||||
this.abortController?.unload();
|
||||
this.abortController = undefined;
|
||||
return this.nativeHide();
|
||||
}
|
||||
}
|
||||
|
||||
nativeHide() {
|
||||
const { hoverEl, targetEl } = this;
|
||||
this.state = PopoverState.Hidden;
|
||||
hoverEl.detach();
|
||||
|
||||
if (targetEl) {
|
||||
const parent = targetEl.matchParent(`.${PREFIX}-leaf-view`);
|
||||
if (parent) popovers.get(parent)?.transition();
|
||||
}
|
||||
|
||||
this.onHide();
|
||||
this.unload();
|
||||
}
|
||||
|
||||
resolveLink(linkText: string, sourcePath: string): TFile | null {
|
||||
const link = parseLinktext(linkText);
|
||||
const tFile = link
|
||||
? this.plugin.app.metadataCache.getFirstLinkpathDest(link.path, sourcePath)
|
||||
: null;
|
||||
return tFile;
|
||||
}
|
||||
|
||||
async openLink(
|
||||
linkText: string,
|
||||
sourcePath: string,
|
||||
eState?: EphemeralState,
|
||||
createInLeaf?: WorkspaceLeaf
|
||||
) {
|
||||
let file = this.resolveLink(linkText, sourcePath);
|
||||
const link = parseLinktext(linkText);
|
||||
if (!file && createInLeaf) {
|
||||
const folder = this.plugin.app.fileManager.getNewFileParent(sourcePath);
|
||||
file = await this.plugin.app.fileManager.createNewMarkdownFile(folder, link.path);
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
// this.displayCreateFileAction(linkText, sourcePath, eState);
|
||||
return;
|
||||
}
|
||||
const { viewRegistry } = this.plugin.app;
|
||||
const viewType = viewRegistry.typeByExtension[file.extension];
|
||||
if (!viewType || !viewRegistry.viewByType[viewType]) {
|
||||
// this.displayOpenFileAction(file);
|
||||
return;
|
||||
}
|
||||
|
||||
eState = Object.assign(this.buildEphemeralState(file, link), eState);
|
||||
const parentMode = this.getDefaultMode();
|
||||
const state = this.buildState(parentMode, eState);
|
||||
const leaf = await this.openFile(file, state, createInLeaf);
|
||||
const leafViewType = leaf?.view?.getViewType();
|
||||
// console.log(leaf);
|
||||
if (leafViewType === "image") {
|
||||
// TODO: temporary workaround to prevent image popover from disappearing immediately when using live preview
|
||||
if (
|
||||
this.parent?.hasOwnProperty("editorEl") &&
|
||||
(this.parent as unknown as MarkdownEditView).editorEl!.hasClass("is-live-preview")
|
||||
) {
|
||||
this.waitTime = 3000;
|
||||
}
|
||||
const img = leaf!.view.contentEl.querySelector("img")!;
|
||||
this.hoverEl.dataset.imgHeight = String(img.naturalHeight);
|
||||
this.hoverEl.dataset.imgWidth = String(img.naturalWidth);
|
||||
this.hoverEl.dataset.imgRatio = String(img.naturalWidth / img.naturalHeight);
|
||||
} else if (leafViewType === "pdf") {
|
||||
this.hoverEl.style.height = "800px";
|
||||
this.hoverEl.style.width = "600px";
|
||||
}
|
||||
if (state.state?.mode === "source") {
|
||||
this.whenShown(() => {
|
||||
// Not sure why this is needed, but without it we get issue #186
|
||||
if (requireApiVersion("1.0")) (leaf?.view as any)?.editMode?.reinit?.();
|
||||
leaf?.view?.setEphemeralState(state.eState);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
whenShown(callback: () => any) {
|
||||
// invoke callback once the popover is visible
|
||||
if (this.detaching) return;
|
||||
const existingCallback = this.onShowCallback;
|
||||
this.onShowCallback = () => {
|
||||
if (this.detaching) return;
|
||||
callback();
|
||||
if (typeof existingCallback === "function") existingCallback();
|
||||
};
|
||||
if (this.state === PopoverState.Shown) {
|
||||
this.onShowCallback();
|
||||
this.onShowCallback = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async openFile(file: TFile, openState?: OpenViewState, useLeaf?: WorkspaceLeaf) {
|
||||
if (this.detaching) return;
|
||||
const leaf = useLeaf ?? this.attachLeaf();
|
||||
this.opening = true;
|
||||
|
||||
try {
|
||||
await leaf.openFile(file, openState);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.opening = false;
|
||||
if (this.detaching) this.hide();
|
||||
}
|
||||
this.plugin.app.workspace.setActiveLeaf(leaf);
|
||||
|
||||
return leaf;
|
||||
}
|
||||
|
||||
buildState(parentMode: string, eState?: EphemeralState) {
|
||||
return {
|
||||
active: false, // Don't let Obsidian force focus if we have autofocus off
|
||||
state: { mode: "source" }, // Don't set any state for the view, because this leaf is stayed on another view.
|
||||
eState: eState,
|
||||
};
|
||||
}
|
||||
|
||||
buildEphemeralState(
|
||||
file: TFile,
|
||||
link?: {
|
||||
path: string;
|
||||
subpath: string;
|
||||
}
|
||||
) {
|
||||
const cache = this.plugin.app.metadataCache.getFileCache(file);
|
||||
const subpath = cache ? resolveSubpath(cache, link?.subpath || "") : undefined;
|
||||
const eState: EphemeralState = { subpath: link?.subpath };
|
||||
if (subpath) {
|
||||
eState.line = subpath.start.line;
|
||||
eState.startLoc = subpath.start;
|
||||
eState.endLoc = subpath.end || undefined;
|
||||
}
|
||||
return eState;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,26 +3,28 @@ import { DisplaySettingsView } from "@/views/settings/categories/DisplaySettings
|
|||
import { FilterSettings } from "@/settings/categories/FilterSettings";
|
||||
import { GroupSettings } from "@/settings/categories/GroupSettings";
|
||||
import { DisplaySettings } from "@/settings/categories/DisplaySettings";
|
||||
import { App, ExtraButtonComponent } from "obsidian";
|
||||
import { ExtraButtonComponent, WorkspaceLeaf } from "obsidian";
|
||||
import { State, StateChange } from "@/util/State";
|
||||
import { GroupSettingsView } from "@/views/settings/categories/GroupSettingsView";
|
||||
import { FilterSettingsView } from "@/views/settings/categories/FilterSettingsView";
|
||||
import { GraphSettings } from "@/settings/GraphSettings";
|
||||
import { ObsidianTheme } from "@/util/ObsidianTheme";
|
||||
import { eventBus } from "@/util/EventBus";
|
||||
import Graph3dPlugin from "@/main";
|
||||
|
||||
export class GraphSettingsView extends HTMLDivElement {
|
||||
private app: App;
|
||||
private settingsButton: ExtraButtonComponent;
|
||||
private graphControls: HTMLDivElement;
|
||||
private readonly settingsState: State<GraphSettings>;
|
||||
private readonly theme: ObsidianTheme;
|
||||
private readonly plugin: Graph3dPlugin;
|
||||
searchLeaf: WorkspaceLeaf;
|
||||
|
||||
constructor(settingsState: State<GraphSettings>, theme: ObsidianTheme, app: App) {
|
||||
constructor(settingsState: State<GraphSettings>, theme: ObsidianTheme, plugin: Graph3dPlugin) {
|
||||
super();
|
||||
this.settingsState = settingsState;
|
||||
this.theme = theme;
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private isCollapsedState = new State(true);
|
||||
|
|
@ -40,23 +42,25 @@ export class GraphSettingsView extends HTMLDivElement {
|
|||
this.graphControls = this.createDiv({ cls: "graph-controls" });
|
||||
|
||||
this.appendGraphControlsItems(this.graphControls.createDiv({ cls: "control-buttons" }));
|
||||
this.appendSetting(
|
||||
this.appendSettingGroup(
|
||||
this.settingsState.createSubState("value.filters", FilterSettings),
|
||||
"Filters",
|
||||
(...args) => FilterSettingsView(...args, this.app)
|
||||
(...args) => FilterSettingsView(...args, this.plugin)
|
||||
);
|
||||
this.appendSetting(
|
||||
this.appendSettingGroup(
|
||||
this.settingsState.createSubState("value.groups", GroupSettings),
|
||||
"Groups",
|
||||
(...args) => GroupSettingsView(...args, this.theme)
|
||||
);
|
||||
this.appendSetting(
|
||||
this.appendSettingGroup(
|
||||
this.settingsState.createSubState("value.display", DisplaySettings),
|
||||
"Display",
|
||||
DisplaySettingsView
|
||||
);
|
||||
this.initListeners();
|
||||
this.toggleCollapsed(this.isCollapsedState.value);
|
||||
|
||||
// init search view
|
||||
}
|
||||
|
||||
private initListeners() {
|
||||
|
|
@ -111,14 +115,18 @@ export class GraphSettingsView extends HTMLDivElement {
|
|||
}
|
||||
|
||||
private appendMinimizeButton(containerEl: HTMLElement) {
|
||||
// on close
|
||||
new ExtraButtonComponent(containerEl)
|
||||
.setIcon("x")
|
||||
.setTooltip("Close")
|
||||
.onClick(() => (this.isCollapsedState.value = true));
|
||||
.onClick(() => {
|
||||
this.isCollapsedState.value = true;
|
||||
console.log("graph setting is closed");
|
||||
});
|
||||
}
|
||||
|
||||
// utility function to append a setting
|
||||
private appendSetting<S>(
|
||||
private appendSettingGroup<S>(
|
||||
setting: S,
|
||||
title: string,
|
||||
view: (setting: S, containerEl: HTMLElement) => void
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ import { ObsidianTheme } from "@/util/ObsidianTheme";
|
|||
import { State } from "@/util/State";
|
||||
import { GroupSettingsView } from "@/views/settings/categories/GroupSettingsView";
|
||||
|
||||
const getRandomColor = () => {
|
||||
return "#" + Math.floor(Math.random() * 16777215).toString(16);
|
||||
};
|
||||
|
||||
export const addNodeGroupButton = (
|
||||
groupSettings: State<GroupSettings>,
|
||||
containerEl: HTMLElement,
|
||||
|
|
@ -20,7 +24,7 @@ export const addNodeGroupButton = (
|
|||
.setClass("mod-cta")
|
||||
.setButtonText("Add Group")
|
||||
.onClick(() => {
|
||||
groupSettings.value.groups.push(new NodeGroup("", theme.textMuted));
|
||||
groupSettings.value.groups.push(new NodeGroup("", getRandomColor()));
|
||||
containerEl.empty();
|
||||
GroupSettingsView(groupSettings, containerEl, theme);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,69 +1,68 @@
|
|||
import { App, Notice, Setting, Plugin } from "obsidian";
|
||||
import { SearchView, Setting, TFile } from "obsidian";
|
||||
import { FilterSettings } from "@/settings/categories/FilterSettings";
|
||||
import { State } from "@/util/State";
|
||||
import { spawnLeafView } from "@/views/leafView";
|
||||
import Graph3dPlugin from "@/main";
|
||||
|
||||
interface GlobalSearchPlugin extends Plugin {
|
||||
instance: {
|
||||
getGlobalSearchQuery: () => string;
|
||||
openGlobalSearch: (query: string) => void;
|
||||
};
|
||||
}
|
||||
const DomLookUpTime = 300;
|
||||
|
||||
export const FilterSettingsView = (
|
||||
export const FilterSettingsView = async (
|
||||
filterSettings: State<FilterSettings>,
|
||||
containerEl: HTMLElement,
|
||||
app: App
|
||||
plugin: Graph3dPlugin
|
||||
) => {
|
||||
const instance = (app.internalPlugins.plugins["global-search"] as GlobalSearchPlugin).instance;
|
||||
// add search setting
|
||||
new Setting(containerEl)
|
||||
.setClass("mod-search-setting")
|
||||
.addSearch((search) => {
|
||||
// initialize search with current search query
|
||||
search.inputEl.value = filterSettings.value.searchQuery;
|
||||
filterSettings.onChange((change) => {
|
||||
if (change.currentPath === "searchQuery") {
|
||||
search.inputEl.value = change.newValue as string;
|
||||
}
|
||||
});
|
||||
search.setPlaceholder("Read only");
|
||||
search.inputEl.onclick = (e) => {
|
||||
new Notice("This search is delegated to the global search plugin");
|
||||
const currentSearch = instance.getGlobalSearchQuery();
|
||||
instance.openGlobalSearch(currentSearch);
|
||||
};
|
||||
search.clearButtonEl.onclick = (e) => {
|
||||
filterSettings.value.searchQuery = "";
|
||||
filterSettings.value.searchResult = [];
|
||||
};
|
||||
search.inputEl.readOnly = true;
|
||||
return search;
|
||||
})
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Search").onClick(async (e) => {
|
||||
const currentSearch = instance.getGlobalSearchQuery();
|
||||
instance.openGlobalSearch(currentSearch);
|
||||
const searchLeaf = app.workspace.getLeavesOfType("search")[0];
|
||||
if (searchLeaf) {
|
||||
filterSettings.value.searchQuery = instance.getGlobalSearchQuery();
|
||||
const search = await searchLeaf.open(searchLeaf.view);
|
||||
const rawSearchResult = await new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
resolve(search.dom.resultDomLookup);
|
||||
}, 300)
|
||||
);
|
||||
// @ts-ignore
|
||||
const files = Array.from(rawSearchResult.keys());
|
||||
console.log(files);
|
||||
// @ts-ignore
|
||||
filterSettings.value.searchResult = files.map((f) => f.path as string);
|
||||
console.log(filterSettings.value.searchResult);
|
||||
} else {
|
||||
new Notice("Please open your search leaf first");
|
||||
}
|
||||
});
|
||||
});
|
||||
const searchEl = containerEl.createDiv({
|
||||
// cls :
|
||||
});
|
||||
const [searchLeaf] = spawnLeafView(plugin, searchEl);
|
||||
|
||||
await searchLeaf.setViewState({
|
||||
type: "search",
|
||||
});
|
||||
|
||||
// add searchEl to containerEl
|
||||
containerEl.appendChild(searchEl);
|
||||
|
||||
const element = searchLeaf.containerEl.querySelector(
|
||||
".workspace-leaf-content[data-type='search']"
|
||||
) as HTMLDivElement;
|
||||
// element.style.removeProperty("overflow");
|
||||
const searchRowEl = element.querySelector(".search-row") as HTMLDivElement;
|
||||
searchRowEl.style.margin = "0px";
|
||||
console.log(element);
|
||||
// move the element to the containerEl
|
||||
containerEl.appendChild(searchRowEl);
|
||||
const inputEl = searchRowEl.getElementsByTagName("input")[0]!;
|
||||
const settingIconEl = searchRowEl.querySelector(
|
||||
".clickable-icon[aria-label='Search settings']"
|
||||
) as HTMLDivElement;
|
||||
const matchCaseIconEl = searchRowEl.querySelector(
|
||||
"div.search-input-container > div[aria-label='Match case'] "
|
||||
) as HTMLDivElement;
|
||||
const clearButtonEl = searchRowEl.querySelector(".search-input-clear-button") as HTMLDivElement;
|
||||
settingIconEl?.remove();
|
||||
matchCaseIconEl?.remove();
|
||||
console.log(settingIconEl);
|
||||
inputEl.value = filterSettings.value.searchQuery;
|
||||
inputEl.onkeydown = async (e) => {
|
||||
const currentView = searchLeaf.view as SearchView;
|
||||
const rawSearchResult = await new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
resolve(currentView.dom.resultDomLookup);
|
||||
}, DomLookUpTime)
|
||||
);
|
||||
// @ts-ignore
|
||||
const files = Array.from(rawSearchResult.keys()) as TFile[];
|
||||
|
||||
filterSettings.value.searchResult = files.map((f) => f.path);
|
||||
filterSettings.value.searchQuery = inputEl.value;
|
||||
};
|
||||
|
||||
clearButtonEl.onclick = async () => {
|
||||
filterSettings.value.searchQuery = "";
|
||||
filterSettings.value.searchResult = [];
|
||||
};
|
||||
|
||||
// add show attachments setting
|
||||
new Setting(containerEl).setName("Show Attachments").addToggle((toggle) => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export const AddNodeGroupItem = (
|
|||
});
|
||||
|
||||
addColorPicker(groupEl, group.value.color, (value) => {
|
||||
console.log("color changed", value);
|
||||
group.value.color = value;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const addNodeGroups = (groupSettings: State<GroupSettings>, containerEl:
|
|||
cls: "graph-color-groups-container",
|
||||
});
|
||||
groupSettings.value.groups.forEach((group, index) => {
|
||||
console.log(groupSettings.value.groups[0]);
|
||||
const groupState = groupSettings.createSubState(`value.groups.${index}`, NodeGroup);
|
||||
AddNodeGroupItem(groupState, nodeGroupContainerEl, () => {
|
||||
groupSettings.value.groups.splice(index, 1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue