├── Releases.md ├── global.d.ts ├── versions.json ├── .gitattributes ├── .gitignore ├── demos └── flow-demo.mp4 ├── src ├── basics │ ├── types │ │ ├── TransactionRange.ts │ │ ├── command.ts │ │ └── settings.ts │ ├── flow │ │ ├── PortalType.ts │ │ ├── flowCommands.ts │ │ ├── FlowEditorHover.tsx │ │ ├── markdownPost.tsx │ │ ├── patchWorkspaceForFlow.ts │ │ └── flowEditor.ts │ ├── menus │ │ ├── inlineStylerView │ │ │ ├── styles │ │ │ │ ├── index.ts │ │ │ │ └── default.ts │ │ │ ├── Mark.tsx │ │ │ └── inlineStyler.tsx │ │ ├── registerMenus.ts │ │ ├── obsidianSyntax.ts │ │ ├── StickerMenu.tsx │ │ └── MakeMenu │ │ │ └── MakeMenu.tsx │ ├── utils │ │ └── utils.ts │ ├── schemas │ │ └── settings.ts │ ├── ui │ │ ├── UICollapse.tsx │ │ └── UINote.tsx │ ├── codemirror │ │ ├── flowStateFields.ts │ │ ├── placeholder.ts │ │ └── flowViewUpdates.ts │ ├── enactor │ │ └── enactor.ts │ ├── cmExtensions.ts │ ├── codemirror.ts │ └── makemods │ │ └── replaceMobileMainMenu.tsx ├── shared │ ├── types │ │ ├── focus.ts │ │ ├── blink.tsx │ │ ├── Warning.ts │ │ ├── ui.ts │ │ ├── emojis.ts │ │ ├── spaceFragment.ts │ │ ├── context.ts │ │ ├── Pos.ts │ │ ├── makemd.ts │ │ ├── spaceInfo.ts │ │ ├── afile.ts │ │ ├── caches.ts │ │ ├── persister.ts │ │ ├── path.ts │ │ ├── predicate.ts │ │ ├── commands.ts │ │ ├── spaceDef.ts │ │ ├── kits.ts │ │ ├── actions.ts │ │ ├── mdb.ts │ │ ├── mframe.ts │ │ ├── menu.ts │ │ ├── api.ts │ │ ├── settings.ts │ │ ├── PathState.ts │ │ ├── frameExec.ts │ │ ├── metadata.ts │ │ ├── superstate.ts │ │ ├── indexMap.ts │ │ └── uiManager.ts │ ├── schemas │ │ ├── builtin.ts │ │ ├── context.ts │ │ ├── predicate.tsx │ │ └── fields.ts │ ├── utils │ │ ├── paths.ts │ │ ├── uuid.js │ │ ├── json.ts │ │ ├── dom.ts │ │ ├── sticker.ts │ │ ├── makemd │ │ │ ├── schema.ts │ │ │ ├── embed.ts │ │ │ ├── fragment.ts │ │ │ └── inlineTable.ts │ │ ├── stickers.ts │ │ ├── openPathInElement.ts │ │ ├── obsidian.ts │ │ ├── sanitizers.ts │ │ ├── inputManager.ts │ │ ├── dispatchers │ │ │ └── dispatcher.ts │ │ ├── array.ts │ │ ├── color.ts │ │ └── uri.ts │ └── components │ │ ├── PathSticker.tsx │ │ └── StickerModal.tsx ├── css │ ├── Obsidian │ │ └── Mods.css │ ├── Menus │ │ ├── ColorPicker.css │ │ ├── MakeMenu.css │ │ ├── StickerMenu.css │ │ ├── InlineMenu.css │ │ └── MainMenu.css │ ├── Editor │ │ ├── Frames │ │ │ ├── Insert.css │ │ │ ├── Slides.css │ │ │ └── Page.css │ │ ├── Flow │ │ │ ├── FlowState.css │ │ │ └── Properties.css │ │ ├── Context │ │ │ └── ContextList.css │ │ └── Properties │ │ │ └── DatePicker.css │ ├── SpaceViewer │ │ ├── Text.css │ │ ├── Frame.css │ │ ├── Layout.css │ │ └── Nodes.css │ ├── Panels │ │ ├── Navigator │ │ │ ├── EverView.css │ │ │ ├── Focuses.css │ │ │ └── FileTree.css │ │ ├── ContextBuilder.css │ │ └── SpaceEditor.css │ ├── UI │ │ └── Buttons.css │ ├── DefaultVibe.css │ └── Modal │ │ └── Modal.css └── types │ └── obsidian.d.ts ├── manifest.json ├── .eslintrc.js ├── LICENSE ├── tsconfig.json ├── README.md ├── package.json └── esbuild.config.mjs /Releases.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.css"; 2 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.1.0": "0.1.0" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | package-lock.json 4 | .DS_Store 5 | .makemd 6 | .trash -------------------------------------------------------------------------------- /demos/flow-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Make-md/Obsidian-Basics/HEAD/demos/flow-demo.mp4 -------------------------------------------------------------------------------- /src/basics/types/TransactionRange.ts: -------------------------------------------------------------------------------- 1 | 2 | export type TransactionRange = { 3 | from: number; 4 | to: number; 5 | }; 6 | -------------------------------------------------------------------------------- /src/shared/types/focus.ts: -------------------------------------------------------------------------------- 1 | export type Focus = { 2 | name: string; 3 | paths: string[]; 4 | sticker: string; 5 | } -------------------------------------------------------------------------------- /src/css/Obsidian/Mods.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .mobile-toolbar-options-container { 4 | border-top: 1px solid var(--mk-ui-divider); 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/schemas/builtin.ts: -------------------------------------------------------------------------------- 1 | export const builtinSpacePathPrefix = "spaces://$"; 2 | export const tagsSpacePath = "spaces://$tags"; 3 | -------------------------------------------------------------------------------- /src/shared/types/blink.tsx: -------------------------------------------------------------------------------- 1 | export enum BlinkMode { 2 | Search, 3 | Blink, 4 | Open, 5 | OpenSpaces, 6 | Image, 7 | Command, 8 | } 9 | -------------------------------------------------------------------------------- /src/basics/flow/PortalType.ts: -------------------------------------------------------------------------------- 1 | export type PortalType = "none" | 2 | "doc" | 3 | "block" | 4 | "foldernote" | 5 | "flow" | 6 | "context"; 7 | -------------------------------------------------------------------------------- /src/shared/types/Warning.ts: -------------------------------------------------------------------------------- 1 | export type Warning = { 2 | id: string; 3 | message: string; 4 | description: string; 5 | command: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/shared/utils/paths.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | export const removeTrailingSlashFromFolder = (path: string) => path == "/" 6 | ? path 7 | : path.slice(-1) == "/" 8 | ? path.substring(0, path.length - 1) 9 | : path; 10 | -------------------------------------------------------------------------------- /src/shared/utils/uuid.js: -------------------------------------------------------------------------------- 1 | export function genId() { 2 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => 3 | ( 4 | c ^ 5 | (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))) 6 | ).toString(16) 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/types/ui.ts: -------------------------------------------------------------------------------- 1 | export enum ScreenType { Phone, Desktop, Tablet } 2 | export enum InteractionType { Touch, Mouse, Controller, Voice } 3 | export type Sticker = { 4 | type: string; 5 | name: string; 6 | value: string; 7 | html: string; 8 | keywords: string; 9 | }; 10 | -------------------------------------------------------------------------------- /src/shared/schemas/context.ts: -------------------------------------------------------------------------------- 1 | import { SpaceTableSchema } from "shared/types/mdb"; 2 | 3 | export const defaultContextSchemaID = "files"; 4 | export const defaultContextDBSchema: SpaceTableSchema = { 5 | id: defaultContextSchemaID, 6 | name: "Items", 7 | type: "db", 8 | primary: "true", 9 | }; 10 | -------------------------------------------------------------------------------- /src/shared/types/emojis.ts: -------------------------------------------------------------------------------- 1 | export type EmojiData = Record< 2 | string, 3 | { 4 | n: [string, string]; 5 | u: string; 6 | v?: string[]; 7 | }[] 8 | >; 9 | 10 | export type Emoji = { 11 | label: string; 12 | desc: string; 13 | unicode: string; 14 | variants?: string[]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/shared/types/spaceFragment.ts: -------------------------------------------------------------------------------- 1 | 2 | export type SpaceFragmentType = "context" | 'frame' | 'action'; 3 | 4 | export type SpaceFragmentSchema = { 5 | id: string; 6 | name: string; 7 | sticker?: string; 8 | frameType?: string; 9 | type: SpaceFragmentType; 10 | path: string; 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/shared/types/context.ts: -------------------------------------------------------------------------------- 1 | export const PathPropertyName = "File" 2 | export const PathPropertyCreated = "Created" 3 | 4 | export type ContextDefType = 'tag' 5 | export type ContextDef = { 6 | type: ContextDefType, 7 | value: string 8 | } 9 | 10 | export type ContextLookup = { 11 | field: string; 12 | property: string; 13 | } -------------------------------------------------------------------------------- /src/basics/menus/inlineStylerView/styles/index.ts: -------------------------------------------------------------------------------- 1 | import defaultStyles from "./default"; 2 | 3 | export type InlineStyle = { 4 | label: string; 5 | value: string; 6 | insertOffset: number; 7 | cursorOffset?: number; 8 | icon: string; 9 | mark?: string; 10 | }; 11 | 12 | export function resolveStyles() { 13 | return defaultStyles; 14 | } 15 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "make-basics", 3 | "name": "Basics", 4 | "version": "0.1.1", 5 | "minAppVersion": "0.16.0", 6 | "description": "Editable embeds and inline styling and other basic improvements to the markdown editor to your workflow.", 7 | "author": "MAKE.md", 8 | "authorUrl": "https://www.make.md", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/types/Pos.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Pos = { x: number; y: number; z?: number; }; 3 | export type Size = { width: number; height: number; }; 4 | export type Rect = { x: number; y: number; width: number; height: number; }; 5 | 6 | export type Anchors = "top" | "bottom" | "left" | "right" | 'center' 7 | export type Edges = "top" | "bottom" | "left" | "right" | 'inside' -------------------------------------------------------------------------------- /src/shared/utils/json.ts: -------------------------------------------------------------------------------- 1 | 2 | export const safelyParseJSON = (json: string) => { 3 | // This function cannot be optimised, it's best to 4 | // keep it small! 5 | let parsed; 6 | try { 7 | parsed = JSON.parse(json); 8 | } catch (e) { 9 | // 10 | // Oh well, but whatever... 11 | } 12 | 13 | return parsed; // Could be undefined! 14 | }; 15 | -------------------------------------------------------------------------------- /src/shared/types/makemd.ts: -------------------------------------------------------------------------------- 1 | import { App, WorkspaceLeaf } from "obsidian"; 2 | import { ISuperstate } from "shared/types/superstate"; 3 | 4 | export interface IMakeMDPlugin { 5 | app: App; 6 | superstate: ISuperstate; 7 | openPath: ( 8 | leaf: WorkspaceLeaf, 9 | path: string, 10 | flow?: boolean 11 | ) => Promise; 12 | } -------------------------------------------------------------------------------- /src/shared/types/spaceInfo.ts: -------------------------------------------------------------------------------- 1 | 2 | export type FilesystemSpaceInfo = SpaceInfo & { 3 | folderPath: string; 4 | dbPath: string; 5 | framePath: string; 6 | commandsPath: string; 7 | }; 8 | 9 | export type SpaceInfo = { 10 | name: string; 11 | path: string; 12 | isRemote: boolean; 13 | readOnly: boolean; 14 | defPath: string; 15 | notePath: string; 16 | }; 17 | -------------------------------------------------------------------------------- /src/basics/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const compareByField = 2 | (field: string, dir: boolean) => 3 | (_a: Record, _b: Record) => { 4 | const a = dir ? _a : _b; 5 | const b = dir ? _b : _a; 6 | if (a[field] < b[field]) { 7 | return -1; 8 | } 9 | if (a[field] > b[field]) { 10 | return 1; 11 | } 12 | return 0; 13 | }; 14 | -------------------------------------------------------------------------------- /src/shared/utils/dom.ts: -------------------------------------------------------------------------------- 1 | 2 | export function selectElementContents(el: Element) { 3 | if (!el) return; 4 | const range = document.createRange(); 5 | range.selectNodeContents(el); 6 | const sel = window.getSelection(); 7 | sel.removeAllRanges(); 8 | sel.addRange(range); 9 | } 10 | 11 | export const windowFromDocument = (doc: Document): Window => { 12 | return doc.defaultView || window; 13 | } -------------------------------------------------------------------------------- /src/shared/types/afile.ts: -------------------------------------------------------------------------------- 1 | import { PathLabel } from "./caches"; 2 | 3 | export type AFile = { 4 | path: string; 5 | name: string; 6 | filename: string; 7 | parent: string; 8 | isFolder: boolean; 9 | extension?: string; 10 | ctime?: number; 11 | mtime?: number; 12 | size?: number; 13 | } 14 | 15 | export type VaultItem = { 16 | path: string; 17 | parent: string; 18 | created: string; 19 | folder: string; 20 | rank?: string; 21 | } & PathLabel; 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/shared/types/caches.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export type PathCache = { 5 | [key: string]: any; 6 | metadata: Record; 7 | ctime: number; 8 | label: PathLabel; 9 | contentTypes: string[]; 10 | tags: string[]; 11 | type: string; 12 | subtype: string; 13 | parent: string; 14 | readOnly: boolean; 15 | }; 16 | 17 | export type PathLabel = { 18 | name: string; 19 | sticker: string; 20 | color: string; 21 | thumbnail?: string; 22 | preview?: string; 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /src/shared/utils/sticker.ts: -------------------------------------------------------------------------------- 1 | import { ISuperstate as Superstate } from "shared/types/superstate"; 2 | 3 | export const savePathSticker = async ( 4 | superstate: Superstate, 5 | path: string, 6 | sticker: string 7 | ) => { 8 | superstate.spaceManager.saveLabel(path, superstate.settings.fmKeySticker, sticker); 9 | };export const removeIconsForPaths = (superstate: Superstate, paths: string[]) => { 10 | paths.forEach((path) => { 11 | savePathSticker(superstate, path, ""); 12 | }); 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /src/css/Menus/ColorPicker.css: -------------------------------------------------------------------------------- 1 | 2 | .mk-ui-color-picker { 3 | display: flex; 4 | flex-direction: column; 5 | gap: 4px; 6 | } 7 | .mk-ui-color-picker-selector { 8 | border-bottom: thin solid var(--mk-ui-divider); 9 | } 10 | 11 | .mk-ui-color-picker-palette { 12 | padding: 12px; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | .mk-ui-color-picker .mk-color { 17 | margin: 4px; 18 | } 19 | .mk-ui-color-picker-palette > div { 20 | display: flex; 21 | flex-direction: row; 22 | gap: 4px; 23 | } -------------------------------------------------------------------------------- /src/css/Menus/MakeMenu.css: -------------------------------------------------------------------------------- 1 | .mk-slash-item { 2 | display: flex; 3 | align-items: center; 4 | } 5 | .mk-slash-icon { 6 | display: flex; 7 | margin-right: 8px; 8 | } 9 | .mk-slash-icon svg { 10 | width: 16px; 11 | height: 16px; 12 | } 13 | .cm-focused .cm-active.mk-placeholder:before { 14 | content: attr(data-ph); 15 | color: var(--mk-ui-text-tertiary); 16 | position: absolute; 17 | } 18 | 19 | .mk-floweditor .cm-active.mk-placeholder:before { 20 | content: attr(data-ph); 21 | color: var(--mk-ui-text-tertiary); 22 | position: absolute; 23 | } -------------------------------------------------------------------------------- /src/css/Editor/Frames/Insert.css: -------------------------------------------------------------------------------- 1 | 2 | .mk-frame-insert { 3 | width: 100%; 4 | bottom: 0px; 5 | display: flex; 6 | position: fixed; 7 | height: 30px; 8 | border-radius: 4px; 9 | margin-top: 4px; 10 | margin-left: 4px; 11 | --max-width: var(--file-line-width); 12 | max-width: calc(min(100%, var(--max-width))) !important; 13 | margin-left: calc((max(100%, var(--max-width)) - var(--max-width)) /2) !important; 14 | 15 | } 16 | 17 | .mk-frame-insert:hover { 18 | background: var(--mk-ui-background-hover) !important; 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/shared/utils/makemd/schema.ts: -------------------------------------------------------------------------------- 1 | import { SpaceTableSchema } from "shared/types/mdb"; 2 | import { FrameSchema } from "shared/types/mframe"; 3 | import { safelyParseJSON } from "shared/utils/json"; 4 | 5 | 6 | export const frameSchemaToTableSchema = (frameSchema: FrameSchema) => { 7 | return { 8 | ...frameSchema, 9 | def: JSON.stringify(frameSchema.def) 10 | }; 11 | }; 12 | export const mdbSchemaToFrameSchema = (schema: SpaceTableSchema): FrameSchema => { 13 | if (!schema) return null; 14 | return { 15 | ...schema, 16 | def: safelyParseJSON(schema.def) 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/types/persister.ts: -------------------------------------------------------------------------------- 1 | import { DBRow } from "shared/types/mdb"; 2 | 3 | 4 | export abstract class LocalCachePersister { 5 | public abstract initialize(): Promise; 6 | public abstract isInitialized(): boolean; 7 | public abstract unload(): void; 8 | public abstract store(path: string, cache: string, type: string): Promise; 9 | public abstract reset(): void; 10 | public abstract remove(path: string, type: string): Promise; 11 | public abstract cleanType(type: string): void; 12 | public abstract loadAll(type: string): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/types/path.ts: -------------------------------------------------------------------------------- 1 | import { SpaceFragmentType } from "./spaceFragment"; 2 | 3 | export type PathRefTypes = SpaceFragmentType | 'block' | 'heading' | "unknown"; 4 | 5 | export type URI = { 6 | basePath: string; 7 | scheme: string; 8 | path: string; 9 | authority: string; 10 | 11 | fullPath: string; 12 | alias?: string; 13 | ref?: string; 14 | refStr?: string; 15 | refType?: PathRefTypes; 16 | query?: {[key: string]: string} 17 | isRemote?: boolean; 18 | trailSlash: boolean; 19 | };export type TargetLocation = "split" | "overview" | "window" | "tab" | "left" | "right" | 'system' | 'hover' | boolean; 20 | 21 | -------------------------------------------------------------------------------- /src/basics/types/command.ts: -------------------------------------------------------------------------------- 1 | import MakeBasicsPlugin from "main"; 2 | 3 | import { Editor, TFile } from "obsidian"; 4 | 5 | export enum CommandType { 6 | None, 7 | Command, 8 | Section 9 | } 10 | 11 | export type Command = { 12 | label: string; 13 | value: string; 14 | offset?: [number, number]; 15 | icon: string; 16 | type?: CommandType, 17 | onSelect?: ( 18 | _evt: any, 19 | plugin: MakeBasicsPlugin, 20 | file: TFile, 21 | editor: Editor, 22 | start: { line: number; ch: number }, 23 | startCh: number, 24 | end: { line: number; ch: number }, 25 | onComplete: () => void 26 | ) => void 27 | } 28 | -------------------------------------------------------------------------------- /src/basics/schemas/settings.ts: -------------------------------------------------------------------------------- 1 | import { MakeBasicsSettings } from "basics/types/settings"; 2 | 3 | export const BasicDefaultSettings: MakeBasicsSettings = { 4 | flowMenuEnabled: true, 5 | markSans: false, 6 | makeMenuPlaceholder: true, 7 | mobileMakeBar: false, 8 | mobileSidepanel: false, 9 | inlineStyler: true, 10 | inlineStylerColors: false, 11 | editorFlow: true, 12 | internalLinkClickFlow: true, 13 | internalLinkSticker: true, 14 | editorFlowStyle: "minimal", 15 | menuTriggerChar: "/", 16 | inlineStickerMenu: true, 17 | emojiTriggerChar: ":", 18 | flowState: false, 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /src/basics/ui/UICollapse.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { uiIconSet } from "shared/assets/icons"; 3 | export const UICollapse = (props: { 4 | collapsed: boolean; 5 | onToggle?: (collapsed: boolean, e: React.MouseEvent) => void; 6 | }) => { 7 | return ( 8 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/basics/types/settings.ts: -------------------------------------------------------------------------------- 1 | 2 | export type DeleteFileOption = "trash" | "permanent" | "system-trash"; 3 | export type InlineContextLayout = "horizontal" | "vertical"; 4 | 5 | export interface MakeBasicsSettings { 6 | 7 | markSans: boolean; 8 | flowMenuEnabled: boolean; 9 | makeMenuPlaceholder: boolean; 10 | flowState: boolean; 11 | inlineStyler: boolean; 12 | mobileMakeBar: boolean; 13 | mobileSidepanel: boolean; 14 | inlineStylerColors: boolean; 15 | editorFlow: boolean; 16 | internalLinkClickFlow: boolean; 17 | internalLinkSticker: boolean; 18 | editorFlowStyle: string; 19 | menuTriggerChar: string; 20 | inlineStickerMenu: boolean; 21 | emojiTriggerChar: string; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/css/Editor/Frames/Slides.css: -------------------------------------------------------------------------------- 1 | .mk-frame-slides { 2 | display: flex 3 | } 4 | .mk-frame-slide, .mk-frame-slide-active { 5 | display: flex; 6 | flex-direction: column 7 | } 8 | .mk-frame-slide span:first-child { 9 | border-left: thin solid var(--mk-ui-divider); 10 | font-size: 8px; 11 | } 12 | .mk-frame-slide-active span:first-child { 13 | border-left: thin solid var(--mk-ui-divider); 14 | font-size: 8px; 15 | } 16 | .mk-frame-slide span:last-child { 17 | width: 20px; 18 | height: 20px; 19 | background: var(--background-primary-alt) 20 | } 21 | 22 | .mk-frame-slide-active span:last-child { 23 | width: 20px; 24 | height: 20px; 25 | background: var(--background-secondary) 26 | } -------------------------------------------------------------------------------- /src/shared/types/predicate.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export type Filter = { 4 | field: string; 5 | fn: string; 6 | value: string; 7 | fType: string; 8 | }; 9 | 10 | export type Predicate = { 11 | view: string; 12 | 13 | listView: string; 14 | listItem: string; 15 | listGroup: string; 16 | listViewProps: Record; 17 | listItemProps: Record; 18 | listGroupProps: Record; 19 | filters: Filter[]; 20 | sort: Sort[]; 21 | groupBy: string[]; 22 | 23 | colsOrder: string[]; 24 | colsHidden: string[]; 25 | colsSize: Record; 26 | colsCalc: Record; 27 | }; 28 | 29 | export type Sort = { 30 | field: string; 31 | fn: string; 32 | }; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react-hooks/recommended" 11 | ], 12 | "overrides": [ 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": "latest", 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "react", 21 | "@typescript-eslint" 22 | ], 23 | "rules": { 24 | "react/prop-types": "off", 25 | "@typescript-eslint/ban-ts-comment": "off", 26 | "no-empty": 'off', 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/basics/menus/registerMenus.ts: -------------------------------------------------------------------------------- 1 | import MakeBasicsPlugin from "main"; 2 | import MakeMenu from "./MakeMenu/MakeMenu"; 3 | import StickerMenu from "./StickerMenu"; 4 | import { loadStylerIntoContainer } from "./inlineStylerView/InlineMenu"; 5 | 6 | export const registerEditorMenus = (plugin: MakeBasicsPlugin) => { 7 | if (plugin.settings.flowMenuEnabled) 8 | { 9 | plugin.registerEditorSuggest(new MakeMenu(plugin.app, plugin)); 10 | } 11 | if (plugin.settings.inlineStickerMenu) 12 | {plugin.registerEditorSuggest(new StickerMenu(plugin.app, plugin));} 13 | if (plugin.isTouchScreen() && plugin.settings.mobileMakeBar && plugin.settings.inlineStyler) 14 | loadStylerIntoContainer(plugin.app.mobileToolbar.containerEl, plugin); 15 | } -------------------------------------------------------------------------------- /src/css/SpaceViewer/Text.css: -------------------------------------------------------------------------------- 1 | .mk-t-h1 { 2 | --font-text-size: var(--h1-size); 3 | --text-normal: var(--h1-color); 4 | --font-weight: var(--h1-weight); 5 | } 6 | 7 | .mk-t-h2 { 8 | --font-text-size: var(--h2-size); 9 | --text-normal: var(--h2-color); 10 | --font-weight: var(--h2-weight); 11 | } 12 | 13 | .mk-t-h3 { 14 | --font-text-size: var(--h3-size); 15 | --text-normal: var(--h3-color); 16 | --font-weight: var(--h3-weight); 17 | } 18 | 19 | .mk-t-h4 { 20 | --font-text-size: var(--h4-size); 21 | --text-normal: var(--h4-color); 22 | --font-weight: var(--h4-weight); 23 | } 24 | 25 | .mk-t-h5 { 26 | --font-text-size: var(--h5-size); 27 | --text-normal: var(--h5-color); 28 | --font-weight: var(--h5-weight); 29 | } 30 | 31 | .mk-t-p { 32 | } -------------------------------------------------------------------------------- /src/shared/schemas/predicate.tsx: -------------------------------------------------------------------------------- 1 | import { Predicate } from "shared/types/predicate"; 2 | 3 | export const defaultPredicate: Predicate = { 4 | view: "list", 5 | filters: [], 6 | listView: "", 7 | listItem: "", 8 | listGroup: "", 9 | listGroupProps: {}, 10 | listViewProps: {}, 11 | listItemProps: {}, 12 | sort: [], 13 | groupBy: [], 14 | colsOrder: [], 15 | colsHidden: [], 16 | colsSize: {}, 17 | colsCalc: {}, 18 | }; 19 | 20 | export const defaultTablePredicate: Predicate = { 21 | view: "table", 22 | filters: [], 23 | listView: "", 24 | listItem: "", 25 | listGroup: "", 26 | listGroupProps: {}, 27 | listViewProps: {}, 28 | listItemProps: {}, 29 | sort: [], 30 | groupBy: [], 31 | colsOrder: [], 32 | colsHidden: [], 33 | colsSize: {}, 34 | colsCalc: {}, 35 | }; 36 | -------------------------------------------------------------------------------- /src/basics/codemirror/flowStateFields.ts: -------------------------------------------------------------------------------- 1 | import { Annotation, StateField } from "@codemirror/state"; 2 | import { PortalType } from "../flow/PortalType"; 3 | 4 | export const portalTypeAnnotation = Annotation.define(); 5 | export const flowIDAnnotation = Annotation.define(); 6 | export const flowIDStateField = StateField.define({ 7 | create: () => undefined, 8 | update(value, tr) { 9 | if (tr.annotation(flowIDAnnotation)) return tr.annotation(flowIDAnnotation); 10 | return value; 11 | }, 12 | }); 13 | 14 | export const flowTypeStateField = StateField.define({ 15 | create: (state) => "none", 16 | update(value, tr) { 17 | if (tr.annotation(portalTypeAnnotation)) 18 | return tr.annotation(portalTypeAnnotation); 19 | return value; 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/basics/menus/obsidianSyntax.ts: -------------------------------------------------------------------------------- 1 | export type oMark = { 2 | mark: string; 3 | formatting: string; 4 | formatChar: string; 5 | altFormatting?: string; 6 | }; 7 | export const oMarks: oMark[] = [ 8 | { 9 | mark: "em", 10 | formatting: "formatting-em", 11 | altFormatting: "em_formatting_formatting-strong", 12 | formatChar: "*", 13 | }, 14 | { 15 | mark: "strong", 16 | formatting: "formatting-strong", 17 | formatChar: "**", 18 | }, 19 | { 20 | mark: "strikethrough", 21 | formatting: "formatting-strikethrough", 22 | formatChar: "~~", 23 | }, 24 | { 25 | mark: "inline-code", 26 | formatting: "formatting-code", 27 | formatChar: "`", 28 | }, 29 | ]; 30 | 31 | export type oBlock = { 32 | block: string; 33 | formatting: string; 34 | blockChar: string; 35 | }; 36 | -------------------------------------------------------------------------------- /src/css/Panels/Navigator/EverView.css: -------------------------------------------------------------------------------- 1 | .mk-ever-view { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | } 6 | .mk-ever-view-header { 7 | display: flex; 8 | 9 | padding: 8px 12px; 10 | gap: 4px; 11 | } 12 | .mk-ever-view-header-title { 13 | display: flex; 14 | flex-direction: column; 15 | flex: 1; 16 | } 17 | .mk-ever-view-title { 18 | font-size: 16px; 19 | font-weight: 500; 20 | color: var(--mk-ui-text-primary); 21 | } 22 | 23 | .mk-ever-view-filters { 24 | padding: 4px 8px; 25 | } 26 | 27 | .mk-ever-view-contents { 28 | flex: 1; 29 | overflow: auto; 30 | } 31 | 32 | .mk-ever-view-count { 33 | font-size: 12px; 34 | color: var(--mk-ui-text-secondary); 35 | } 36 | .mk-ever-view-filter { 37 | padding: 8px; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/basics/menus/inlineStylerView/styles/default.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | label: "bold", 4 | value: `****`, 5 | insertOffset: 2, 6 | icon: "mk-mark-strong", 7 | mark: "strong", 8 | }, 9 | { 10 | label: "italics", 11 | value: "**", 12 | insertOffset: 1, 13 | icon: "mk-mark-em", 14 | mark: "em", 15 | }, 16 | { 17 | label: "strikethrough", 18 | value: "~~~~", 19 | insertOffset: 2, 20 | icon: "mk-mark-strikethrough", 21 | mark: "strikethrough", 22 | }, 23 | { 24 | label: "code", 25 | value: "``", 26 | insertOffset: 1, 27 | icon: "mk-mark-code", 28 | mark: "inline-code", 29 | }, 30 | { 31 | label: "link", 32 | value: "[]()", 33 | insertOffset: 1, 34 | cursorOffset: 2, 35 | icon: "mk-mark-link", 36 | }, 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /src/shared/types/commands.ts: -------------------------------------------------------------------------------- 1 | import { SpaceProperty } from "./mdb"; 2 | 3 | export type Library = { 4 | name: string; 5 | commands: Command[]; 6 | } 7 | 8 | export type CommandSchema = { 9 | id: string; 10 | name: string; 11 | type: string; 12 | //used for type definition 13 | //used for view options including filter, order and group 14 | predicate?: string; 15 | primary?: string; 16 | def?: { 17 | icon?: string 18 | type?: string 19 | description?: string 20 | } 21 | } 22 | 23 | export type CommandWithPath = { 24 | scheme: string; 25 | path: string; 26 | } & Command; 27 | 28 | export type Command = { 29 | schema: CommandSchema; 30 | 31 | fields: SpaceProperty[]; 32 | code?: any; 33 | codeType?: string; 34 | } 35 | 36 | export type CommandResult = { 37 | result: any; 38 | error: any; 39 | } -------------------------------------------------------------------------------- /src/shared/types/spaceDef.ts: -------------------------------------------------------------------------------- 1 | import { Filter } from "./predicate"; 2 | 3 | export type SpaceSort = { 4 | field: string; 5 | asc: boolean; 6 | group: boolean; 7 | recursive: boolean; 8 | }; 9 | 10 | export type SpaceDefFilter = { 11 | type: string; 12 | fType: string; 13 | } & Filter; 14 | export type SpaceDefGroup = { 15 | type: 'any' | 'all'; 16 | trueFalse: boolean; 17 | filters: SpaceDefFilter[]; 18 | }; 19 | export type SpaceType = 'folder' | 'tag' | 'vault' | 'default' | 'unknown'; 20 | 21 | 22 | export type SpaceDefinition = { 23 | contexts?: string[]; 24 | sort?: SpaceSort; 25 | filters?: SpaceDefGroup[]; 26 | links?: string[]; 27 | tags?: string[]; 28 | template?: string; 29 | templateName?: string; 30 | recursive?: string; 31 | defaultSticker?: string; 32 | defaultColor?: string; 33 | readMode?: boolean; 34 | }; 35 | -------------------------------------------------------------------------------- /src/shared/utils/stickers.ts: -------------------------------------------------------------------------------- 1 | 2 | export const emojiFromString = (emoji: string) => { 3 | let html; 4 | try { 5 | html = unifiedToNative(emoji); 6 | } 7 | catch { 8 | html = emoji; 9 | } 10 | return html; 11 | }; 12 | 13 | export function parseStickerString(input: string): [string, string] { 14 | if (!input) { 15 | return ["", ""]; 16 | } 17 | const match = input.match(/^(.*?)\s*\/\/\s*(.*)$/); 18 | if (match) { 19 | return [match[1], match[2]]; 20 | } else { 21 | return ["", input]; 22 | } 23 | } 24 | export const unifiedToNative = (unified: string) => { 25 | const unicodes = unified.split("-"); 26 | const codePoints = unicodes.map((u) => `0x${u}`); 27 | // @ts-ignore 28 | return String.fromCodePoint(...codePoints); 29 | }; 30 | export const nativeToUnified = (native: string) => native.codePointAt(0).toString(16); 31 | 32 | -------------------------------------------------------------------------------- /src/shared/utils/openPathInElement.ts: -------------------------------------------------------------------------------- 1 | import { App, WorkspaceLeaf } from "obsidian"; 2 | import { FlowEditor, FlowEditorParent } from "../FlowEditor"; 3 | 4 | 5 | 6 | 7 | export const openPathInElement = ( 8 | app: App, 9 | parentLeaf: WorkspaceLeaf, 10 | initiatingEl?: HTMLElement, 11 | fileName?: string, 12 | onShowCallback?: (leaf: FlowEditor) => Promise 13 | ) => { 14 | const parent = (parentLeaf ?? app.workspace.getLeaf()) as unknown as FlowEditorParent; 15 | if (!initiatingEl) initiatingEl = parent.containerEl; 16 | const hoverPopover = new FlowEditor( 17 | parent, 18 | initiatingEl!, 19 | app, 20 | undefined, 21 | onShowCallback 22 | ); 23 | 24 | // plugin.attachPortal(hoverPopover); 25 | if (fileName) 26 | hoverPopover.titleEl.textContent = fileName.substring( 27 | 0, 28 | fileName.lastIndexOf(".") 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/basics/menus/inlineStylerView/Mark.tsx: -------------------------------------------------------------------------------- 1 | import MakeBasicsPlugin from "main"; 2 | import React from "react"; 3 | import { uiIconSet } from "shared/assets/icons"; 4 | import i18n from "shared/i18n"; 5 | import { InlineStyle } from "./styles"; 6 | export const Mark = (props: { 7 | plugin: MakeBasicsPlugin; 8 | i: number; 9 | style: InlineStyle; 10 | active: boolean; 11 | toggleMarkAction: (e: React.MouseEvent, s: InlineStyle) => void; 12 | }) => { 13 | const { i, style, active, toggleMarkAction } = props; 14 | 15 | return ( 16 |
)[style.label] 21 | : undefined 22 | } 23 | className={`mk-mark ${style.mark && active ? "mk-mark-active" : ""}`} 24 | dangerouslySetInnerHTML={{ 25 | __html: uiIconSet[`${style.icon}`], 26 | }} 27 | onMouseDown={(e) => toggleMarkAction(e, style)} 28 | >
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/css/Editor/Flow/FlowState.css: -------------------------------------------------------------------------------- 1 | 2 | .is-phone.mk-flow-state .workspace-split { 3 | padding-top: 0; 4 | } 5 | .mk-flow-state 6 | .workspace-tab-header-container-inner { 7 | display: none; 8 | } 9 | .mk-flow-state .workspace-ribbon { 10 | display: none; 11 | } 12 | body:not(.is-mobile).mk-flow-state .workspace-split.mod-left-split .workspace-sidedock-vault-profile { 13 | transform: translateY(-100%); 14 | transition: transform 300ms linear; 15 | } 16 | body.mk-flow-state { 17 | --tab-container-background: var(--background-primary) !important; 18 | --titlebar-background-focused: var(--background-primary) !important; 19 | 20 | } 21 | 22 | .mk-flow-state .workspace-tabs .workspace-leaf { 23 | background: var(--background-primary) !important; 24 | } 25 | 26 | .is-phone.mk-flow-state .view-header { 27 | /* display: none; */ 28 | } 29 | 30 | .mk-flow-state .view-header { 31 | transform: translateY(-100%); 32 | max-height: 0; 33 | transition: transform 300ms linear; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/shared/types/kits.ts: -------------------------------------------------------------------------------- 1 | import { SpaceDefinition } from "shared/types/spaceDef"; 2 | import { SpaceTables } from "./mdb"; 3 | import { MDBFrame } from "./mframe"; 4 | 5 | export type Kit = { 6 | id: string, 7 | name: string, 8 | colors: {[key: string]: string}, 9 | frames: MDBFrame[], 10 | } 11 | 12 | export type Note = { 13 | name: string; 14 | properties: Record; 15 | content: string; 16 | } 17 | export type Assets = { 18 | name: string; 19 | path: string; 20 | payload?: string; 21 | type?: string 22 | } 23 | 24 | export type SpaceKit = { 25 | name: string; 26 | path: string; 27 | definition: SpaceDefinition; 28 | properties: Record; 29 | context: SpaceTables; 30 | frames: SpaceTables; 31 | children: SpaceKit[]; 32 | notes: Note[]; 33 | assets: Assets[]; 34 | templates: TemplateKit[]; 35 | content: string; 36 | } 37 | 38 | export type TemplateKit = { 39 | name: string; 40 | type: string; 41 | content: string | SpaceKit; 42 | } -------------------------------------------------------------------------------- /src/basics/codemirror/placeholder.ts: -------------------------------------------------------------------------------- 1 | import { RangeSetBuilder, StateField } from "@codemirror/state"; 2 | import { Decoration, DecorationSet, EditorView } from "@codemirror/view"; 3 | import MakeBasicsPlugin from "main"; 4 | import i18n from "shared/i18n"; 5 | const placeholderLine = (plugin: MakeBasicsPlugin) => Decoration.line({ 6 | attributes: { "data-ph": i18n.labels.placeholder.replace('${1}', plugin.settings.menuTriggerChar) }, 7 | class: "mk-placeholder", 8 | }); 9 | 10 | export const placeholderExtension = (plugin: MakeBasicsPlugin) => StateField.define({ 11 | create() { 12 | return Decoration.none; 13 | }, 14 | update(value, tr) { 15 | const builder = new RangeSetBuilder(); 16 | const currentLine = tr.state.doc.lineAt(tr.state.selection.main.head); 17 | 18 | if (currentLine?.length == 0) 19 | builder.add(currentLine.from, currentLine.from, placeholderLine(plugin)); 20 | const dec = builder.finish(); 21 | return dec; 22 | }, 23 | provide: (f) => EditorView.decorations.from(f), 24 | }); 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 JP Cen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/shared/utils/makemd/embed.ts: -------------------------------------------------------------------------------- 1 | 2 | import { SpaceState } from "shared/types/PathState"; 3 | 4 | export const framePathForSpace = (space: SpaceState, schema: string) => { 5 | if (space.type == 'folder') { 6 | return `${space.path}/#*${schema}` 7 | } 8 | if (space.type == 'vault') { 9 | return `/#*${schema}` 10 | } 11 | return `${space.path}/#*${schema}` 12 | } 13 | 14 | export const actionPathForSpace = (space: SpaceState, schema: string) => { 15 | if (space.type == 'folder') { 16 | return `${space.path}/#;${schema}` 17 | } 18 | if (space.type == 'vault') { 19 | return `/#;${schema}` 20 | } 21 | return `${space.path}/#;${schema}` 22 | } 23 | 24 | export const contextPathForSpace = (space: SpaceState, schema: string) => { 25 | if (space.type == 'folder') { 26 | return `${space.path}/#^${schema}` 27 | } 28 | if (space.type == 'vault') { 29 | return `/#^${schema}` 30 | } 31 | return `${space.path}/#^${schema}` 32 | } 33 | 34 | export const contextViewEmbedStringFromContext = (space: SpaceState, schema: string) => `![![${framePathForSpace(space, schema)}]]` 35 | 36 | export const contextEmbedStringFromContext = (space: SpaceState, schema: string) => `![![${contextPathForSpace(space, schema)}]]` 37 | 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "esModuleInterop": true, 5 | "baseUrl": "src", 6 | "inlineSourceMap": true, 7 | "inlineSources": true, 8 | "isolatedModules": true, 9 | "module": "ESNext", 10 | "target": "es6", 11 | "allowJs": true, 12 | "alwaysStrict": true, 13 | "noImplicitAny": true, 14 | "moduleResolution": "node", 15 | "importHelpers": true, 16 | "removeComments": true, 17 | "allowSyntheticDefaultImports": true, 18 | "lib": ["dom", "es5", "scripthost", "es2020", "es2022"], 19 | "skipLibCheck": true, 20 | // "paths": { 21 | // "react": ["node_modules/preact/compat/"], 22 | // "react-dom": ["node_modules/preact/compat/"] 23 | // } 24 | }, 25 | "include": [ 26 | "**/*.ts", 27 | "**/*.tsx", 28 | "src/adapters/obsidian/ui/navigator/NavigatorView.tsx", 29 | "src/core/utils/autosizer.js", 30 | "src/core/utils/libs/detectElementResize.js", 31 | "src/core/utils/sqljs.ts", 32 | "src/core/react/components/UI/Menus/menu/Suggestions.jsx", 33 | "src/shared/utils/uuid.js" 34 | , "src/adapters/text/removemd.js", "src/core/utils/color/gradientParser.js", "src/core/react/components/SpaceEditor/ts.worker.js" ] 35 | } 36 | -------------------------------------------------------------------------------- /src/shared/types/actions.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Command, CommandWithPath } from "./commands"; 3 | import { URI } from "./path"; 4 | import { ISuperstate } from "./superstate"; 5 | 6 | 7 | 8 | export interface CLIAdapter { 9 | manager: ICLIManager; 10 | scheme: string; 11 | commandForAction: (action: string) => Command; 12 | runCommand: (command: string, instance: ActionInstance) => Promise; 13 | allCommands: () => CommandWithPath[]; 14 | 15 | } 16 | 17 | export interface ICLIManager { 18 | builtinCommands: Command[]; 19 | mainTerminal: CLIAdapter; 20 | terminals: CLIAdapter[]; 21 | superstate: ISuperstate; 22 | terminalForURI(uri: URI): CLIAdapter | null; 23 | commandForAction(action: string): Command | null; 24 | runCommand(action: string, instance: ActionInstance): Promise | void; 25 | allCommands(): CommandWithPath[]; 26 | } 27 | export type ActionTree = { 28 | action: string; 29 | result?: string; 30 | linked?: { [key: string]: string }; 31 | props: { [key: string]: any }; 32 | propsValue: { [key: string]: any }; 33 | children: ActionTree[]; 34 | }; 35 | 36 | export type ActionInstance = { 37 | props: { [key: string]: any }; 38 | instanceProps: { [key: string]: any }; 39 | result?: any; 40 | iterations: number; 41 | error?: any; 42 | }; 43 | -------------------------------------------------------------------------------- /src/shared/utils/obsidian.ts: -------------------------------------------------------------------------------- 1 | import { App } from "obsidian"; 2 | 3 | 4 | 5 | 6 | export const getLineRangeFromRef = ( 7 | path: string, 8 | ref: string | undefined, 9 | app: App 10 | ): [number | undefined, number | undefined] => { 11 | if (!ref) { 12 | return [undefined, undefined]; 13 | } 14 | const cache = app.metadataCache.getCache(path); 15 | if (!cache) return [undefined, undefined]; 16 | const headings = cache.headings; 17 | const blocks = cache.blocks; 18 | const sections = cache.sections; 19 | if (blocks && ref.charAt(0) == "^" && blocks[ref.substring(1)]) { 20 | return [ 21 | blocks[ref.substring(1)].position.start.line + 1, 22 | blocks[ref.substring(1)].position.end.line + 1, 23 | ]; 24 | } 25 | const heading = headings?.find((f) => f.heading.replace("#", " ") == ref); 26 | 27 | if (heading) { 28 | const index = headings.findIndex((f) => f.heading == heading.heading); 29 | const level = headings[index]?.level; 30 | const nextIndex = headings.findIndex( 31 | (f, i) => i > index && f.level <= level 32 | ); 33 | 34 | const start = heading.position.start.line + 2; 35 | if (index < headings.length - 1 && nextIndex != -1) { 36 | return [start, headings[nextIndex].position.end.line]; 37 | } 38 | return [start, sections[sections.length - 1].position.end.line + 1]; 39 | } 40 | return [undefined, undefined]; 41 | }; 42 | -------------------------------------------------------------------------------- /src/shared/utils/sanitizers.ts: -------------------------------------------------------------------------------- 1 | 2 | export const sanitizeSQLStatement = (name: string) => { 3 | try { 4 | return name?.replace(/'/g, `''`) 5 | } catch(e) { 6 | console.log(e, name); 7 | return '' 8 | } 9 | };export const sanitizeColumnName = (name: string): string => { 10 | if (name?.charAt(0) == "_") { 11 | return sanitizeColumnName(name.substring(1)); 12 | } 13 | return name?.replace(/"/g, ``); 14 | }; 15 | export const sanitizeTableName = (name: string) => { 16 | return name?.replace(/[^a-z0-9+]+/gi, ""); 17 | }; 18 | const folderReservedRe = /^[+\$#^]+/; 19 | const illegalRe = /[\/\?<>\\:\*\|":]/g; 20 | const controlRe = /[\x00-\x1f\x80-\x9f]/g; 21 | const reservedRe = /^\.+$/; 22 | const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; 23 | 24 | export const sanitizeFolderName = (name: string) => { 25 | const replacement = ""; 26 | return name 27 | .replace(folderReservedRe, replacement) 28 | .replace(illegalRe, replacement) 29 | .replace(controlRe, replacement) 30 | .replace(reservedRe, replacement) 31 | .replace(windowsReservedRe, replacement); 32 | }; 33 | export const sanitizeFileName = (name: string) => { 34 | const replacement = ""; 35 | return name 36 | .replace(illegalRe, replacement) 37 | .replace(controlRe, replacement) 38 | .replace(reservedRe, replacement) 39 | .replace(windowsReservedRe, replacement); 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /src/shared/types/mdb.ts: -------------------------------------------------------------------------------- 1 | 2 | export type DBRow = Record; 3 | export type DBRows = DBRow[]; 4 | export type DBTable = { 5 | uniques: string[]; 6 | cols: string[]; 7 | rows: DBRows; 8 | }; 9 | 10 | export enum ContextSchemaType { 11 | SpaceType = 0, 12 | ContextType = 1, 13 | FrameType = 2, 14 | TableType = 3, 15 | CommandType = 4, 16 | } 17 | 18 | export type SpaceTableColumn = SpaceProperty & { table?: string }; 19 | 20 | export type DBTables = Record; 21 | 22 | export type MDB = { 23 | schemas: SpaceTableSchema[]; 24 | fields: SpaceProperty[]; 25 | tables: {[key: string]: DBTable }; 26 | }; 27 | 28 | export type SpaceTable = { 29 | schema: SpaceTableSchema; 30 | cols: SpaceProperty[]; 31 | rows: DBRows; 32 | }; 33 | export type SpaceTables = Record; 34 | export type SpaceTableSchema = { 35 | id: string; 36 | name: string; 37 | type: string; 38 | //used for type definition 39 | def?: string; 40 | //used for view options including filter, order and group 41 | predicate?: string; 42 | primary?: string; 43 | }; 44 | 45 | export type SpaceProperty = { 46 | name: string; 47 | //schema that the fields in 48 | schemaId?: string; 49 | type: string; 50 | //metadata for field 51 | value?: string; 52 | hidden?: string; 53 | //styling for field 54 | attrs?: string; 55 | //constraints at the db level 56 | unique?: string; 57 | primary?: string; 58 | }; 59 | -------------------------------------------------------------------------------- /src/css/Menus/StickerMenu.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: emoji; 3 | 4 | src: local("Apple Color Emoji"), local("Android Emoji"), local("Segoe UI"), 5 | local(EmojiSymbols), local(Symbola); 6 | /* Emoji unicode blocks */ 7 | unicode-range: U+1F300-1F5FF, U+1F600-1F64F, U+1F680-1F6FF, U+2600-26FF; 8 | } 9 | 10 | .mk-sticker-menu .suggestion { 11 | width: 240px; 12 | height: 240px; 13 | display: flex; 14 | flex-wrap: wrap; 15 | align-content: flex-start; 16 | flex-direction: row; 17 | 18 | } 19 | .mk-sticker-modal { 20 | display: flex; 21 | flex-wrap: wrap; 22 | } 23 | .mk-sticker-modal .suggestion-item { 24 | width: 30px; 25 | height: 30px; 26 | display: flex; 27 | font-size: 20px; 28 | gap: 4px; 29 | align-items: center; 30 | padding: 0; 31 | text-align: center; 32 | justify-content: center; 33 | font-family: emoji; 34 | } 35 | .mk-sticker-filter { 36 | border: none; 37 | background: none; 38 | border-bottom: thin solid var(--mk-ui-border); 39 | width: 100%; 40 | padding: 8px 12px; 41 | } 42 | .mk-sticker-menu .suggestion-item:hover { 43 | background: var(--mk-ui-background-hover); 44 | } 45 | .mk-image-modal { 46 | display: flex; 47 | flex-wrap: wrap; 48 | } 49 | 50 | .mk-sticker { 51 | display: flex; 52 | height: var(--icon-size); 53 | align-items: center; 54 | } 55 | .mk-sticker svg { 56 | height: var(--icon-size); 57 | width: var(--icon-size); 58 | stroke-width: var(--icon-stroke); 59 | } -------------------------------------------------------------------------------- /src/css/UI/Buttons.css: -------------------------------------------------------------------------------- 1 | .is-phone .mk-inline-button { 2 | width: unset !important; 3 | } 4 | 5 | .is-tablet .mk-inline-button { 6 | padding: unset !important; 7 | } 8 | 9 | .mk-inline-button { 10 | display: inline-flex; 11 | align-items: center; 12 | justify-content: center; 13 | padding: 4px; 14 | border-radius: var(--clickable-icon-radius); 15 | box-shadow: unset !important; 16 | } 17 | 18 | .mk-inline-button:not(:hover) { 19 | background: unset !important; 20 | } 21 | 22 | .mk-inline-button { 23 | font-size: 13px; 24 | 25 | display: flex; 26 | align-items: center; 27 | gap: 10px; 28 | width: auto !important; 29 | } 30 | .mk-inline-button { 31 | background: none !important; 32 | box-shadow: none !important; 33 | color: var(--mk-ui-text-tertiary) !important; 34 | padding: 0 4px !important; 35 | display: flex; 36 | gap: 4px; 37 | height: 20px; 38 | border: none !important; 39 | } 40 | 41 | .mk-inline-button svg { 42 | color: var(--mk-ui-text-tertiary) !important; 43 | } 44 | body:not(.is-mobile) .mk-inline-button:hover svg, 45 | body:not(.is-mobile) .mk-inline-button:hover { 46 | color: var(--mk-ui-text-primary) !important; 47 | } 48 | 49 | 50 | 51 | .mk-inline-button { 52 | background: none; 53 | border: 0; 54 | box-shadow: none; 55 | margin: 0; 56 | height: 24px; 57 | width: 24px; 58 | padding: 0 !important; 59 | 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basics for Obsidian 2 | Basics transforms your editor experience to enhance your note-taking by streamlining the process of viewing, editing, and styling your notes. Basics offers a range of powerful features, including: 3 | 4 | ## Inline Styler 5 | Inline styler allows you to apply different styling to your text quickly. 6 | ### Opening the Flow Styler 7 | 1. Select the text in your note that you want to style 8 | 2. Use the styler to select the formating you want to apply 9 | 10 | ## Make Menu 11 | Make menu allows you to quickly create formatted blocks as well as notes. 12 | ### Creating Blocks with Flow Menu 13 | 1. Create a new line and type the hotkey ('/' by default) to open the Flow Menu 14 | 2. Select/search for the block you want to create 15 | 16 | ## Flow Block 17 | Flow blocks makes your embedded notes editable. 18 | ### Creating a Flow Block from Flow Menu 19 | 1. Create a new line in your notes where you want your Flow Block to be 20 | 2. Open the Make Menu by typing your trigger key (/ by default) 21 | 3. Select Flow Block 22 | 4. Find the Note you want to connect 23 | 24 | ### Opening a Flow Block from Inline Link 25 | 1. Hover over the inline link you want to open flow block with 26 | 2. Based on your inline flow block settings 27 | 1. If hover to open is on: Hold shift and click the link 28 | 2. If hover to open is off: Click Open Flow from the menu 29 | 30 | ### Opening a Flow Block from Inline Embeds 31 | 1. Hover over the inline embed you want to open flow block with 32 | 2. Click the toggle flow button 33 | -------------------------------------------------------------------------------- /src/basics/enactor/enactor.ts: -------------------------------------------------------------------------------- 1 | import { Root } from "react-dom/client"; 2 | import { SelectOption } from "shared/types/menu"; 3 | import { URI } from "shared/types/path"; 4 | import { SpaceFragmentSchema } from "shared/types/spaceFragment"; 5 | 6 | export interface Enactor { 7 | name: string; 8 | load(): void; 9 | convertSpaceFragmentToMarkdown( 10 | spaceFragment: SpaceFragmentSchema, 11 | onReturn: (markdown: string) => void 12 | ): void; 13 | selectLink(e: React.MouseEvent, onSelect: (path: string) => void): void; 14 | selectSpace(e: React.MouseEvent, onSelect: (path: string) => void): void; 15 | pathExists(path: string): Promise; 16 | selectImage(e: React.MouseEvent, onSelect: (path: string) => void): void; 17 | isSpace(path: string): boolean; 18 | loadExtensions(firstLoad: boolean): void; 19 | spaceNotePath(path: string): string | null; 20 | parentPath(path: string): string; 21 | spaceFolderPath(path: string): string; 22 | createNote(parent: string, name: string, content?: string) : Promise; 23 | createRoot(el: Element | DocumentFragment) : Root; 24 | notify(message: string) : void; 25 | uriByString(uri: string, source?: string) : URI; 26 | spaceFragmentSchema(uri: string) : Promise; 27 | resolvePath(path: string, source?: string) : string; 28 | saveSettings() : void; 29 | openMenu(ev: React.MouseEvent, options: SelectOption[]) : void; 30 | openPath(path: string, source?: HTMLElement) : void; 31 | addActiveStateListener(listener: () => void) : void; 32 | removeActiveStateListener(listener: () => void) : void; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/shared/types/mframe.ts: -------------------------------------------------------------------------------- 1 | import { SpaceProperty, SpaceTableSchema } from "./mdb"; 2 | 3 | export type MFrame = { 4 | id: string; 5 | schemaId: string; 6 | name: string; 7 | type: string; 8 | parentId?: string; 9 | props?: string; 10 | actions?: string; 11 | ref?: string; 12 | rank: string; 13 | styles?: string; 14 | contexts?: string; 15 | } 16 | 17 | export type FrameTreeProp = Record; 18 | export type FrameSchema = { 19 | id: string; 20 | name: string; 21 | type: string; 22 | //used for type definition 23 | //used for view options including filter, order and group 24 | predicate?: string; 25 | primary?: string; 26 | def?: { 27 | icon?: string 28 | context?: string 29 | db?: string 30 | type?: string 31 | id?: string 32 | } 33 | } 34 | export type FrameRoot = { 35 | id?: string; 36 | def: {[key: string]: string}; 37 | children?: FrameRoot[]; 38 | node: FrameNode; 39 | } 40 | 41 | export type FrameNode = { 42 | styles?: FrameTreeProp; 43 | name: string; 44 | //key value for a context and a path 45 | contexts?: FrameTreeProp; 46 | props?: FrameTreeProp; 47 | types?: FrameTreeProp; 48 | propsValue?: FrameTreeProp; 49 | propsAttrs?: FrameTreeProp; 50 | actions?: FrameTreeProp; 51 | 52 | parentId?: string; 53 | ref?: string; 54 | type: string; 55 | rank: number; 56 | schemaId: string; 57 | icon?: string; 58 | id: string; 59 | }; 60 | 61 | export type MDBFrame = { 62 | schema: SpaceTableSchema; 63 | cols: SpaceProperty[]; 64 | rows: MFrame[]; 65 | }; 66 | 67 | export type MDBFrames = { [key: string] : MDBFrame} 68 | 69 | -------------------------------------------------------------------------------- /src/shared/utils/makemd/fragment.ts: -------------------------------------------------------------------------------- 1 | import { mdbSchemaToFrameSchema } from "shared/utils/makemd/schema"; 2 | 3 | 4 | import { SpaceFragmentSchema } from "shared/types/spaceFragment"; 5 | import { ISuperstate } from "shared/types/superstate"; 6 | 7 | export const uriToSpaceFragmentSchema = async ( 8 | superstate: ISuperstate, 9 | path: string 10 | ): Promise => { 11 | const uri = superstate.spaceManager.uriByString(path); 12 | if (uri.refType == "context") { 13 | const schema = superstate.contextsIndex 14 | .get(uri.basePath) 15 | ?.schemas.find((s) => s.id == uri.ref); 16 | if (schema) { 17 | return { 18 | id: schema.id, 19 | name: schema.name, 20 | type: "context", 21 | path: uri.basePath, 22 | }; 23 | } 24 | } 25 | if (uri.refType == "frame") { 26 | return superstate.spaceManager.readFrame(uri.basePath, uri.ref).then((s) => { 27 | 28 | const schema = s?.schema; 29 | if (schema) { 30 | const frameSchema = mdbSchemaToFrameSchema(schema); 31 | return { 32 | id: schema.id, 33 | name: frameSchema.name, 34 | sticker: frameSchema.def?.icon, 35 | type: "frame", 36 | frameType: frameSchema.type, 37 | path: uri.basePath, 38 | }; 39 | } 40 | return null; 41 | }); 42 | } 43 | if (uri.refType == "action") { 44 | const schema = superstate.actionsIndex 45 | .get(uri.path) 46 | ?.find((s) => s.schema.id == uri.ref)?.schema; 47 | if (schema) { 48 | return { 49 | id: schema.id, 50 | name: schema.name, 51 | sticker: schema.def?.icon, 52 | type: "action", 53 | path: uri.basePath, 54 | }; 55 | } 56 | } 57 | return null; 58 | }; 59 | -------------------------------------------------------------------------------- /src/shared/schemas/fields.ts: -------------------------------------------------------------------------------- 1 | import { defaultContextSchemaID } from "shared/schemas/context"; 2 | import { PathPropertyName } from "shared/types/context"; 3 | import { DBTable, SpaceProperty } from "shared/types/mdb"; 4 | import { SpaceInfo } from "shared/types/spaceInfo"; 5 | 6 | 7 | 8 | export const fieldSchema = { 9 | uniques: ["name,schemaId"], 10 | cols: [ 11 | "name", 12 | "schemaId", 13 | "type", 14 | "value", 15 | "attrs", 16 | "hidden", 17 | "unique", 18 | "primary", 19 | ], 20 | }; 21 | 22 | export const defaultContextFields: DBTable = { 23 | ...fieldSchema, 24 | rows: [ 25 | { 26 | name: PathPropertyName, 27 | schemaId: defaultContextSchemaID, 28 | type: "file", 29 | primary: "true", 30 | hidden: "", 31 | unique: "", 32 | attrs: "", 33 | value: "", 34 | }, 35 | { 36 | name: "Created", 37 | schemaId: defaultContextSchemaID, 38 | type: "fileprop", 39 | value: PathPropertyName + ".ctime", 40 | hidden: "", 41 | unique: "", 42 | attrs: "", 43 | primary: "true", 44 | }, 45 | ] as SpaceProperty[], 46 | }; 47 | 48 | export const defaultFieldsForContext = (space: SpaceInfo) => { 49 | return defaultContextFields; 50 | }; 51 | 52 | export const defaultTableFields: SpaceProperty[] = [ 53 | { 54 | name: "Name", 55 | schemaId: "", 56 | type: "text", 57 | primary: "true", 58 | }, 59 | ]; 60 | 61 | export const defaultTagFields: DBTable = { 62 | ...fieldSchema, 63 | rows: [ 64 | { 65 | name: PathPropertyName, 66 | schemaId: defaultContextSchemaID, 67 | type: "file", 68 | primary: "true", 69 | hidden: "", 70 | unique: "", 71 | attrs: "", 72 | value: "", 73 | }, 74 | ], 75 | }; 76 | -------------------------------------------------------------------------------- /src/css/Panels/ContextBuilder.css: -------------------------------------------------------------------------------- 1 | 2 | .mk-property-editor-context-title { 3 | display: flex; 4 | font-size: 13px; 5 | font-weight: var(--font-normal); 6 | padding: 8px 4px 7 | } 8 | .mk-property-editor-context-title span { 9 | flex: 1; 10 | } 11 | .mk-property-editor-context-tag { 12 | display: flex; 13 | gap: 4px; 14 | align-items: center; 15 | } 16 | .mk-property-editor { 17 | padding: 0; 18 | display: flex; 19 | flex-direction: column; 20 | height: 100%; 21 | gap: 8px; 22 | } 23 | .mk-property-editor-property { 24 | display: flex; 25 | padding: 8px; 26 | font-size: 13px; 27 | color: var(--mk-ui-text-secondary); 28 | gap: 6px; 29 | align-items: center; 30 | background: var(--background-modifier-cover); 31 | border-radius: 6px; 32 | } 33 | .mk-property-editor-new:hover { 34 | color: var(--mk-ui-text-primary); 35 | } 36 | .mk-property-editor-new span { 37 | font-size: 12px 38 | } 39 | .mk-property-editor-new { 40 | color: var(--mk-ui-text-tertiary); 41 | display: flex; 42 | flex-direction:column; 43 | padding: 8px 8px; 44 | border: thin solid var(--mk-ui-divider); 45 | border-radius: 8px; 46 | } 47 | 48 | 49 | .mk-property-editor-context-title { 50 | font-size: 15px; 51 | font-weight: var(--font-semibold); 52 | line-height: var(--line-height-tight); 53 | padding:8px 4px; 54 | display: flex; 55 | align-items: center; 56 | gap: 8px; 57 | } 58 | .mk-property-editor-list { 59 | display: flex; 60 | flex-direction: column; 61 | gap: 4px; 62 | } 63 | 64 | .mk-property-editor-context-title span, .mk-property-editor-list span { 65 | font-size: 12px; 66 | color: var(--mk-ui-text-tertiary); 67 | flex: 1; 68 | } 69 | -------------------------------------------------------------------------------- /src/css/SpaceViewer/Frame.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | .mk-f-edit { 9 | z-index: 28; 10 | /* transition: all 0.2s ease; */ 11 | outline: thin solid var(--mk-ui-border-accent); 12 | } 13 | 14 | .mk-f-editable:not(.mk-f-edit):hover { 15 | z-index: 28; 16 | outline: 2px solid var(--mk-ui-border-accent); 17 | outline-offset: -2px; 18 | } 19 | 20 | 21 | 22 | .mk-f-disabled { 23 | opacity: 0.5; 24 | 25 | } 26 | 27 | 28 | .mk-frame-edit[data-type="column"] { 29 | align-items: flex-start; 30 | } 31 | .mk-frame-edit[data-type="new"] { 32 | min-width: 50px; 33 | } 34 | 35 | .mk-frame-edit.mk-selected { 36 | background: var(--mk-ui-background-active); 37 | border-radius: 4px; 38 | } 39 | 40 | 41 | 42 | .mk-frame-edit.mk-layout-row { 43 | overflow: clip; 44 | overflow-clip-margin: 28px; 45 | } 46 | 47 | .mk-frame,.mk-frame-edit { 48 | --line-count: 1; 49 | /* transition: all 0.2s ease; */ 50 | } 51 | 52 | .mk-f-root-label { 53 | position: absolute; 54 | padding: 4px; 55 | border-radius: 4px; 56 | font-size: 13px; 57 | z-index: 29; 58 | top: -22px; 59 | background: var(--mk-background-blur); 60 | display: none; 61 | } 62 | .mk-f-root:hover > .mk-f-root-label { 63 | display: flex; 64 | } 65 | 66 | .mk-frame[data-type="listItem"]:empty, .mk-frame[data-type="frame"]:empty, .mk-frame[data-type="icon"]:empty, .mk-frame[data-type="text"]:empty, .mk-frame[data-type="image"]:empty, 67 | .mk-frame-edit[data-type="icon"]:empty, .mk-frame-edit[data-type="text"]:empty, .mk-frame-edit[data-type="image"]:empty { 68 | display: none; 69 | } 70 | 71 | .mk-frame.mk-icon-size-s { 72 | --icon-size: 18px 73 | } 74 | .mk-frame.mk-icon-size-m { 75 | --icon-size: 24px 76 | } 77 | .mk-frame.mk-icon-size-l { 78 | --icon-size: 48px 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/basics/cmExtensions.ts: -------------------------------------------------------------------------------- 1 | import { editBlockExtensions } from "../shared/utils/codemirror/selectiveEditor"; 2 | import { 3 | flowEditorInfo, 4 | internalLinkHover, 5 | internalLinkToggle, 6 | preloadFlowEditor 7 | } from "./codemirror/flowEditor"; 8 | import { flowIDStateField, flowTypeStateField } from "./codemirror/flowStateFields"; 9 | import { flowViewUpdates } from "./codemirror/flowViewUpdates"; 10 | import { placeholderExtension } from "./codemirror/placeholder"; 11 | import { cursorTooltip } from "./menus/inlineStylerView/inlineStyler"; 12 | import { toggleMarkExtension } from "./menus/inlineStylerView/marks"; 13 | 14 | import { Extension } from '@codemirror/state'; 15 | 16 | import MakeBasicsPlugin from "main"; 17 | import { tooltips } from "./tooltip"; 18 | 19 | export const cmExtensions = (plugin: MakeBasicsPlugin, mobile: boolean) => { 20 | const extensions : Extension[] = [...editBlockExtensions()]; 21 | 22 | extensions.push( 23 | ...[toggleMarkExtension, tooltips({ parent: document.body })] 24 | ); 25 | if (!mobile && plugin.settings.inlineStyler) { 26 | extensions.push(cursorTooltip(plugin)); 27 | } 28 | 29 | if (plugin.settings.flowMenuEnabled && plugin.settings.makeMenuPlaceholder) extensions.push(placeholderExtension(plugin)); 30 | if (plugin.settings.editorFlow) { 31 | extensions.push( 32 | flowTypeStateField, 33 | 34 | preloadFlowEditor, 35 | ); 36 | 37 | extensions.push( 38 | 39 | flowEditorInfo, 40 | flowIDStateField, 41 | flowViewUpdates(plugin) 42 | ); 43 | if (plugin.settings.internalLinkClickFlow) { 44 | extensions.push(internalLinkToggle); 45 | } else { 46 | extensions.push(internalLinkHover(plugin)); 47 | } 48 | } 49 | 50 | 51 | 52 | return extensions; 53 | }; 54 | -------------------------------------------------------------------------------- /src/basics/flow/flowCommands.ts: -------------------------------------------------------------------------------- 1 | import MakeBasicsPlugin from "main"; 2 | import i18n from "shared/i18n"; 3 | 4 | export const loadFlowCommands = (plugin: MakeBasicsPlugin) => { 5 | // this.addCommand({ 6 | // id: 'mk-prev', 7 | // name: "Mod Up", 8 | // callback: () => { 9 | // const cm = getActiveCM(this); 10 | // if (cm) { 11 | // const value = cm.state.field(flowEditorInfo, false); 12 | // const currPosition = cm.state.selection.main; 13 | // const sod = cursorDocStart({state: cm.state, dispatch: cm.dispatch}); 14 | // } 15 | // }, 16 | // hotkeys: [{ 17 | // modifiers: ["Mod"], 18 | // key: "ArrowUp", 19 | // },] 20 | // }) 21 | // this.addCommand({ 22 | // id: 'mk-next', 23 | // name: "Mod Down", 24 | // callback: () => { 25 | // const cm = getActiveCM(this); 26 | // if (cm) { 27 | // const value = cm.state.field(flowEditorInfo, false); 28 | // const currPosition = cm.state.selection.main; 29 | // if (cm.state.selection.main.to == cm.state.doc.length) { 30 | // alert('hello') 31 | // } else { 32 | // cursorDocEnd({state: cm.state, dispatch: cm.dispatch}); 33 | // } 34 | 35 | // } 36 | // }, 37 | // hotkeys: [{ 38 | // modifiers: ["Mod"], 39 | // key: "ArrowDown", 40 | // },] 41 | // }) 42 | 43 | 44 | plugin.addCommand({ 45 | id: "mk-open-flow", 46 | name: i18n.commandPalette.openFlow, 47 | callback: () => plugin.openFlow(), 48 | }); 49 | 50 | plugin.addCommand({ 51 | id: "mk-close-flow", 52 | name: i18n.commandPalette.closeFlow, 53 | callback: () => plugin.closeFlow(), 54 | }); 55 | } -------------------------------------------------------------------------------- /src/shared/utils/inputManager.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export class InputManager { 4 | private events: { [key: string]: Function[] } = {}; 5 | constructor() { 6 | this.addListeners(); 7 | } 8 | 9 | on(eventName: string, listener: Function) { 10 | if (!this.events[eventName]) { 11 | this.events[eventName] = []; 12 | } 13 | 14 | this.events[eventName].push(listener); 15 | } 16 | 17 | off(eventName: string, listener: Function) { 18 | const listeners = this.events[eventName]; 19 | if (listeners) { 20 | this.events[eventName] = listeners.filter(l => l !== listener); 21 | } 22 | } 23 | 24 | emit(eventName: string, data: any) { 25 | const listeners = this.events[eventName]; 26 | if (listeners) { 27 | let propagate = false; 28 | listeners.slice().reverse().forEach(listener => { 29 | if (propagate) return; 30 | propagate = listener(data) 31 | }); 32 | } 33 | } 34 | 35 | addListeners() { 36 | window.addEventListener('mousedown', this.handleMouseEvent, true); 37 | window.addEventListener('click', this.handleMouseEvent, true); 38 | window.addEventListener('contextmenu', this.handleMouseEvent, true); 39 | window.addEventListener('keydown', this.handleKeyEvent); 40 | window.addEventListener('keyup', this.handleKeyEvent); 41 | } 42 | 43 | removeListeners() { 44 | window.removeEventListener('mousedown', this.handleMouseEvent); 45 | window.removeEventListener('click', this.handleMouseEvent); 46 | window.removeEventListener('contextmenu', this.handleMouseEvent); 47 | window.removeEventListener('keydown', this.handleKeyEvent); 48 | window.removeEventListener('keyup', this.handleKeyEvent); 49 | } 50 | handleMouseEvent = (event: MouseEvent) => { 51 | this.emit(event.type, event); 52 | } 53 | 54 | handleKeyEvent = (event: KeyboardEvent) => { 55 | this.emit(event.type, event); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /src/css/Editor/Context/ContextList.css: -------------------------------------------------------------------------------- 1 | 2 | .mk-editor-context-selector { 3 | font-size: 14px; 4 | display: flex; 5 | padding: 8px; 6 | gap: 8px; 7 | border-top: thin solid var(--mk-ui-divider); 8 | flex-direction: column; 9 | } 10 | .mk-editor-context { 11 | overflow-x: scroll; 12 | } 13 | .mk-editor-context-groups { 14 | display: flex; 15 | --icon-size: 14px; 16 | padding: 0; 17 | z-index: var(--layer-popover); 18 | background: var(--mk-ui-background); 19 | max-height: unset; 20 | -webkit-app-region: no-drag; 21 | padding: 6px 10px; 22 | border: 1px solid var(--mk-ui-border); 23 | background-color: var(--mk-ui-background-menu); 24 | user-select: none; 25 | border-radius: 8px; 26 | align-items: center; 27 | height: 50px; 28 | white-space: nowrap; 29 | gap: 8px; 30 | } 31 | 32 | .mk-editor-context-group-select { 33 | background: rgba(var(--mono-rgb-100), 0.025); 34 | border-radius: 4px; 35 | overflow: hidden; 36 | display: flex; 37 | align-items: center; 38 | gap: 2px; 39 | white-space: nowrap; 40 | } 41 | 42 | .mk-editor-context-group .mk-path-context-field { 43 | width: auto; 44 | max-width: unset; 45 | min-width: auto; 46 | background: white; 47 | padding: 2px 8px; 48 | gap: 2px; 49 | } 50 | .mk-editor-context-groups span { 51 | flex: 1; 52 | } 53 | 54 | .mk-editor-context-group { 55 | display: flex; 56 | gap: 8px; 57 | align-items: center; 58 | padding: 0 8px; 59 | border: thin solid var(--mk-ui-divider); 60 | border-radius: 4px; 61 | height: 32px; 62 | background: var(--mk-ui-background); 63 | 64 | } 65 | .mk-editor-context-properties { 66 | display: flex; 67 | flex-direction: column; 68 | padding: 8px; 69 | background: var(--mk-ui-background-contrast); 70 | border-radius: 4px; 71 | 72 | } 73 | .mk-editor-context-properties > div { 74 | gap: 4px; 75 | display: flex; 76 | flex-wrap: wrap; 77 | } -------------------------------------------------------------------------------- /src/basics/codemirror/flowViewUpdates.ts: -------------------------------------------------------------------------------- 1 | import { EditorView, ViewUpdate } from "@codemirror/view"; 2 | import { 3 | flowIDStateField, 4 | flowTypeStateField, 5 | portalTypeAnnotation, 6 | } from "basics/codemirror/flowStateFields"; 7 | import MakeBasicsPlugin from "main"; 8 | import { MarkdownView } from "obsidian"; 9 | import { cacheFlowEditorHeight, flowEditorInfo } from "./flowEditor"; 10 | 11 | //flow view editor viewupdates 12 | 13 | export const flowViewUpdates = (plugin: MakeBasicsPlugin) => EditorView.updateListener.of((v: ViewUpdate) => { 14 | if (v.heightChanged) { 15 | plugin.app.workspace.iterateRootLeaves((leaf) => { 16 | const cm = (leaf.view as MarkdownView).editor?.cm as EditorView; 17 | 18 | if ( 19 | cm && 20 | v.view.dom == cm.dom && 21 | cm.state.field(flowTypeStateField, false) 22 | ) { 23 | if ( 24 | leaf.containerEl.parentElement?.hasClass("workspace-tab-container") 25 | ) { 26 | if (cm.state.field(flowTypeStateField, false) != "doc") { 27 | cm.dispatch({ 28 | annotations: portalTypeAnnotation.of("doc"), 29 | }); 30 | } 31 | } 32 | } 33 | }); 34 | } 35 | if (v.heightChanged) { 36 | const flowID = v.state.field(flowIDStateField, false); 37 | if (flowID) { 38 | plugin.app.workspace.iterateLeaves((leaf) => { 39 | const cm = (leaf.view as MarkdownView).editor?.cm as EditorView; 40 | if (cm) { 41 | const stateField = cm.state.field(flowEditorInfo, false); 42 | if (stateField) { 43 | if (stateField.find((f) => f.id == flowID)) { 44 | cm.dispatch({ 45 | annotations: cacheFlowEditorHeight.of([ 46 | flowID, 47 | v.view.contentHeight, 48 | ]), 49 | }); 50 | } 51 | } 52 | } 53 | }, plugin.app.workspace["rootSplit"]!); 54 | } 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basics", 3 | "version": "0.1.1", 4 | "description": "basics", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "preview": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs preview", 10 | "demo": "node esbuild.config.mjs demo", 11 | "version": "node version-bump.mjs && git add manifest.json versions.json" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/common-tags": "^1.8.1", 18 | "@types/he": "^1.2.0", 19 | "@types/node": "^14.14.37", 20 | "@types/react": "^18.2.57", 21 | "@types/react-dom": "^18.2.19", 22 | "@types/rrule": "^2.2.9", 23 | "@types/sql.js": "^1.4.4", 24 | "@typescript-eslint/eslint-plugin": "^5.57.1", 25 | "@typescript-eslint/parser": "^5.57.1", 26 | "builtin-modules": "3.3.0", 27 | "cross-env": "^7", 28 | "dotenv": "^16.0.3", 29 | "esbuild": "^0.14.47", 30 | "esbuild-envfile-plugin": "^1.0.3", 31 | "esbuild-plugin-copy": "^1.6.0", 32 | "esbuild-plugin-wat": "^0.2.7", 33 | "eslint": "^8.37.0", 34 | "eslint-plugin-react": "^7.32.2", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "find-cache-dir": "^4.0.0", 37 | "obsidian": "^1.1.1", 38 | "prop-types": "^15.7.2", 39 | "terser": "^5.20.0", 40 | "tslib": "^2.2.0", 41 | "typescript": "^4.2.4" 42 | }, 43 | "dependencies": { 44 | "@codemirror/basic-setup": "^0.20.0", 45 | "@codemirror/highlight": "^0.19.8", 46 | "@codemirror/lang-javascript": "^0.20.1", 47 | "@codemirror/language": "^6.10.2", 48 | "@codemirror/lint": "^0.20.3", 49 | "@lezer/common": "^1.0.1", 50 | "@lezer/highlight": "^1.2.0", 51 | "classnames": "^2.3.2", 52 | "common-tags": "^1.8.2", 53 | "date-fns": "^2.29.3", 54 | "he": "^1.2.0", 55 | "monkey-around": "^2.3.0", 56 | "range-analyzer": "^0.1.1-alpha.2", 57 | "react": "^18.2.0", 58 | "react-dom": "^18.2.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/css/Panels/Navigator/Focuses.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .tag-container { 5 | flex: 1; 6 | } 7 | 8 | .mk-button-blink { 9 | padding: 8px 12px !important; 10 | } 11 | 12 | 13 | .mk-search { 14 | display: flex; 15 | padding: 8px 8px; 16 | margin: 4px 12px; 17 | margin-left: 0px; 18 | } 19 | 20 | .mk-search svg { 21 | width: 16px; 22 | height: 16px; 23 | } 24 | 25 | .mk-waypoint { 26 | display: flex; 27 | } 28 | .mk-waypoint-new { 29 | display: flex; 30 | align-items: center; 31 | color: var(--mk-ui-text-secondary); 32 | opacity: .5 33 | } 34 | .mk-waypoint-new:hover { 35 | opacity: 1; 36 | } 37 | 38 | .mk-focuses .mk-focuses-item { 39 | display: flex; 40 | justify-content: center; 41 | } 42 | 43 | .is-mobile .mk-focuses { 44 | box-shadow: var(--mk-shadow-card); 45 | padding-top: 12px; 46 | } 47 | .is-mobile .mk-focuses > div { 48 | gap: 8px; 49 | overflow: initial; 50 | } 51 | .mk-focuses { 52 | display: flex; 53 | overflow: auto; 54 | padding: 12px 12px 4px 12px; 55 | } 56 | 57 | .mk-focuses-inner { 58 | flex-wrap: wrap; 59 | gap: 4px; 60 | display: flex; 61 | justify-content: center; 62 | } 63 | 64 | .mk-sidebar-expanded { 65 | flex-grow: 1; 66 | } 67 | 68 | .mk-waypoint:hover .mk-focuses-item:not(.mk-active) { 69 | background: var(--nav-item-background-hover); 70 | } 71 | .mk-focuses-item { 72 | color: var(--mk-ui-text-secondary); 73 | height: 30px; 74 | min-width: 30px; 75 | padding: 6px; 76 | border-radius: 50%; 77 | display: flex; 78 | align-items: center; 79 | gap: 4px; 80 | text-align: center; 81 | color: var(--icon-color); 82 | 83 | transition: transform 250ms ease; 84 | } 85 | .mk-focuses-item svg, .mk-waypoint-new svg { 86 | width: var(--icon-size); 87 | height: var(--icon-size); 88 | } 89 | 90 | .is-mobile .mk-focuses-item { 91 | width: 40px; 92 | height: 40px; 93 | border-radius: 20px; 94 | } 95 | 96 | 97 | .mk-focuses-item.mk-active { 98 | --label-color: var(--mk-ui-background-selected); 99 | color: var(--text-n); 100 | background-color: var(--label-color); 101 | opacity: 1; 102 | } 103 | 104 | -------------------------------------------------------------------------------- /src/shared/types/menu.ts: -------------------------------------------------------------------------------- 1 | import { Rect } from "./Pos"; 2 | import { IUIManager } from "./uiManager"; 3 | 4 | export type SelectMenuProps = { 5 | ui: IUIManager; 6 | multi?: boolean; 7 | value: string[]; 8 | editable: boolean; 9 | options: SelectOption[]; 10 | addKeyword?: string; 11 | defaultOptions?: SelectOption[]; 12 | saveOptions?: (options: string[], value: string[], isNew?: boolean) => void; 13 | removeOption?: (option: string) => void; 14 | placeholder?: string; 15 | detail?: boolean; 16 | searchable?: boolean; 17 | sections?: SelectSection[]; 18 | showAll?: boolean; 19 | showSections?: boolean; 20 | previewComponent?: React.ReactNode; 21 | onMoreOption?: (e: React.MouseEvent, option: string) => void; 22 | onHover?: (option: any) => void; 23 | onHide?: () => void; 24 | isDisclosure?: boolean; 25 | wrapperClass?: string; 26 | onSelectSection?: (section: string) => void; 27 | }; 28 | 29 | export enum SelectOptionType { 30 | Section = -2, 31 | Separator = -1, 32 | Option = 0, 33 | Disclosure = 1, 34 | Input = 2, 35 | Radio = 3, 36 | Toggle = 4, 37 | Custom = 5, 38 | Submenu = 6, 39 | } 40 | 41 | export type SelectSection = { 42 | name: string; 43 | value: string; 44 | }; 45 | //Overloaded component that handles menu selection 46 | export type SelectOption = { 47 | id?: number; 48 | name: string; 49 | fragment?: React.FC<{ 50 | hide: () => void; 51 | onSubmenu?: ( 52 | openSubmenu: (offset: Rect, onHide: () => void) => MenuObject 53 | ) => void; 54 | }>; 55 | value?: any; 56 | color?: string; 57 | section?: string; 58 | description?: string; 59 | icon?: string; 60 | sortable?: boolean; 61 | removeable?: boolean; 62 | disabled?: boolean; 63 | type?: SelectOptionType; 64 | onToggle?: () => void; 65 | onReorder?: (value: string, newValue: string) => void; 66 | onClick?: (ev: React.MouseEvent) => void; 67 | onSubmenu?: (offset: Rect, onHide: () => void) => MenuObject; 68 | onValueChange?: (value: string) => void; 69 | onMoreOptions?: (e: React.MouseEvent) => void; 70 | }; 71 | export type MenuObject = { 72 | hide: (suppress?: boolean) => void; 73 | update: (props: any) => void; 74 | }; 75 | -------------------------------------------------------------------------------- /src/basics/menus/inlineStylerView/inlineStyler.tsx: -------------------------------------------------------------------------------- 1 | import { EditorState, StateField } from "@codemirror/state"; 2 | import { EditorView } from "@codemirror/view"; 3 | import { InlineMenuComponent } from "basics/menus/inlineStylerView/InlineMenu"; 4 | import { oMarks } from "basics/menus/obsidianSyntax"; 5 | import { Tooltip, showTooltip } from "basics/tooltip"; 6 | import MakeBasicsPlugin from "main"; 7 | import React from "react"; 8 | import { expandRange, rangeIsMark } from "./marks"; 9 | 10 | const cursorTooltipField = (plugin: MakeBasicsPlugin) => 11 | StateField.define({ 12 | create: getCursorTooltips(plugin), 13 | 14 | update(tooltips, tr) { 15 | if (!tr.docChanged && !tr.selection) return tooltips; 16 | return getCursorTooltips(plugin)(tr.state); 17 | }, 18 | 19 | provide: (f) => showTooltip.computeN([f], (state) => state.field(f)), 20 | }); 21 | 22 | const getCursorTooltips = 23 | (plugin: MakeBasicsPlugin) => 24 | (state: EditorState): readonly Tooltip[] => { 25 | return state.selection.ranges 26 | .filter((range) => !range.empty) 27 | .map((range) => { 28 | const expandedRange = expandRange(range, state); 29 | const line = state.doc.lineAt(range.head); 30 | const activeMarks = oMarks 31 | .map((f) => (rangeIsMark(state, f, expandedRange) ? f.mark : "")) 32 | .filter((f) => f != ""); 33 | return { 34 | pos: Math.min(range.head, range.anchor), 35 | above: true, 36 | strictSide: true, 37 | arrow: false, 38 | create: (view: EditorView) => { 39 | const dom = document.createElement("div"); 40 | dom.className = "cm-tooltip-cursor"; 41 | const reactElement = plugin.enactor.createRoot(dom); 42 | reactElement.render( 43 | <> 44 | 50 | 51 | ); 52 | return { dom }; 53 | }, 54 | }; 55 | }); 56 | }; 57 | 58 | export function cursorTooltip(plugin: MakeBasicsPlugin) { 59 | return cursorTooltipField(plugin); 60 | } 61 | -------------------------------------------------------------------------------- /src/basics/codemirror.ts: -------------------------------------------------------------------------------- 1 | import { syntaxTree } from "@codemirror/language"; 2 | import { EditorState } from "@codemirror/state"; 3 | import { EditorView } from "@codemirror/view"; 4 | import { SyntaxNodeRef } from "@lezer/common"; 5 | import { TransactionRange } from "basics/types/TransactionRange"; 6 | import MakeBasicsPlugin from "main"; 7 | import { MarkdownView } from "obsidian"; 8 | 9 | export const getActiveCM = (plugin: MakeBasicsPlugin): EditorView | undefined => { 10 | let rcm: EditorView; 11 | plugin.app.workspace.iterateLeaves((leaf) => { 12 | const cm = (leaf.view as MarkdownView).editor?.cm; 13 | if (cm?.hasFocus) { 14 | rcm = cm; 15 | return true; 16 | } 17 | }, plugin.app.workspace["rootSplit"]!); 18 | return rcm; 19 | }; 20 | 21 | 22 | 23 | export const getActiveMarkdownView = (plugin: MakeBasicsPlugin): MarkdownView | undefined => { 24 | let rv: MarkdownView; 25 | plugin.app.workspace.iterateLeaves((leaf) => { 26 | const cm = (leaf.view as MarkdownView).editor?.cm; 27 | if (cm?.hasFocus) { 28 | rv = leaf.view as MarkdownView; 29 | return true; 30 | } 31 | }, plugin.app.workspace["rootSplit"]!); 32 | return rv; 33 | }; 34 | 35 | 36 | //optimize with resolve later... 37 | export function iterateTreeAtPos( 38 | pos: number, 39 | state: EditorState, 40 | iterateFns: { 41 | enter(node: SyntaxNodeRef): boolean | void; 42 | leave?(node: SyntaxNodeRef): void; 43 | } 44 | ) { 45 | syntaxTree(state).iterate({ ...iterateFns, from: pos, to: pos }); 46 | } 47 | 48 | export function iterateTreeInSelection( 49 | selection: TransactionRange, 50 | state: EditorState, 51 | iterateFns: { 52 | enter(node: SyntaxNodeRef): boolean | void; 53 | leave?(node: SyntaxNodeRef): void; 54 | } 55 | ) { 56 | syntaxTree(state).iterate({ 57 | ...iterateFns, 58 | from: selection.from, 59 | to: selection.to, 60 | }); 61 | } 62 | 63 | export function iterateTreeInDocument( 64 | state: EditorState, 65 | iterateFns: { 66 | enter(node: SyntaxNodeRef): boolean | void; 67 | leave?(node: SyntaxNodeRef): void; 68 | } 69 | ) { 70 | syntaxTree(state).iterate({ ...iterateFns }); 71 | } 72 | 73 | export function checkRangeOverlap( 74 | range1: [number, number], 75 | range2: [number, number] 76 | ) { 77 | return range1[0] <= range2[1] && range2[0] <= range1[1]; 78 | } 79 | -------------------------------------------------------------------------------- /src/css/Editor/Flow/Properties.css: -------------------------------------------------------------------------------- 1 | .mk-props-contexts { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 8px; 5 | position: relative; 6 | margin-bottom: 12px; 7 | width: 100%; 8 | align-items: flex-start; 9 | } 10 | .mk-props-value { 11 | display: flex; 12 | flex-direction: column;; 13 | gap: 8px; 14 | align-items: flex-start; 15 | } 16 | .mk-props-list { 17 | display: flex; 18 | flex-wrap: wrap; 19 | gap: 8px; 20 | width: 100%; 21 | align-items: center; 22 | } 23 | 24 | .mk-props-list .mk-path { 25 | width: 100px; 26 | } 27 | .mk-props-contexts .mk-path-context-row { 28 | width: 100%; 29 | } 30 | 31 | .mk-props-contexts .mk-divider { 32 | border-bottom: thin solid var(--mk-ui-divider); 33 | width: 100%; 34 | height: 1px; 35 | } 36 | 37 | .mk-props-contexts-space { 38 | display: flex; 39 | flex-direction: column; 40 | align-items: flex-start; 41 | gap: 4px; 42 | } 43 | .mk-props-contexts .mk-cell-boolean { 44 | padding: 0px; 45 | } 46 | .mk-props-contexts-space-list { 47 | display: flex; 48 | flex-wrap: wrap; 49 | gap: 4px; 50 | } 51 | .mk-props-contexts-space-name { 52 | display: flex; 53 | background: var(--tag-background); 54 | color: var(--tag-color); 55 | align-items: center; 56 | padding: 2px 8px; 57 | font-size: 12px; 58 | gap: 4px; 59 | border-radius: 12px; 60 | border: var(--tag-border-width) solid var(--tag-border-color); 61 | } 62 | .mk-props-contexts-space-add { 63 | display: flex; 64 | align-items: center; 65 | padding: 4px; 66 | border-radius: 4px; 67 | } 68 | .mk-props-contexts-space-add:hover { 69 | background: var(--mk-ui-background-hover) 70 | } 71 | 72 | .mk-props-contexts-space-name .mk-path-icon { 73 | --icon-container-size: 16px; 74 | --icon-size: 16px; 75 | } 76 | .mk-props-contexts-space-props { 77 | margin-left: 8px; 78 | } 79 | .mk-props-pill { 80 | display: flex; 81 | align-items: center; 82 | gap: 4px; 83 | padding: 4px 8px; 84 | border-radius: 12px; 85 | background: var(--mk-ui-background-contrast); 86 | color: var(--mk-ui-text-tertiary); 87 | font-size: 12px; 88 | 89 | } 90 | .mk-props-contexts-space-name:hover { 91 | opacity: 1 !important; 92 | } 93 | .mk-fold { 94 | position: absolute; 95 | left: -24px; 96 | top: 6px; 97 | } -------------------------------------------------------------------------------- /src/shared/utils/dispatchers/dispatcher.ts: -------------------------------------------------------------------------------- 1 | export interface EventTypeToPayload { 2 | [eventType: string]: any; 3 | } 4 | 5 | type EventListener = (event: T) => void | Promise; 6 | 7 | interface Listener { 8 | callback: EventListener; 9 | priority: number; 10 | once: boolean; 11 | context?: any; 12 | } 13 | 14 | 15 | export class EventDispatcher { 16 | private listeners: Map[]>; 17 | 18 | constructor() { 19 | this.listeners = new Map(); 20 | } 21 | 22 | addListener(eventType: K, listener: EventListener, priority = 0, context?: any): void { 23 | const newListener: Listener = { callback: listener, priority, once: false, context }; 24 | const listeners = this.listeners.get(eventType) || []; 25 | listeners.push(newListener); 26 | listeners.sort((a, b) => b.priority - a.priority); 27 | this.listeners.set(eventType, listeners); 28 | } 29 | 30 | addOnceListener(eventType: K, listener: EventListener, priority = 0, context?: any): void { 31 | const newListener: Listener = { callback: listener, priority, once: true, context }; 32 | const listeners = this.listeners.get(eventType) || []; 33 | listeners.push(newListener); 34 | listeners.sort((a, b) => b.priority - a.priority); 35 | this.listeners.set(eventType, listeners); 36 | } 37 | 38 | removeListener(eventType: K, listener: EventListener): void { 39 | const listeners = this.listeners.get(eventType); 40 | if (listeners) { 41 | this.listeners.set(eventType, listeners.filter(l => l.callback !== listener)); 42 | } 43 | } 44 | 45 | async dispatchEvent(eventType: K, event: T[K]): Promise { 46 | const listeners = this.listeners.get(eventType); 47 | if (listeners) { 48 | for (const listener of listeners) { 49 | try { 50 | await listener.callback.call(listener.context, event); 51 | } catch (error) { 52 | console.error(`Error in listener for event '${String(eventType)}':`, error); 53 | } 54 | 55 | if (listener.once) { 56 | this.removeListener(eventType, listener.callback); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/shared/utils/array.ts: -------------------------------------------------------------------------------- 1 | export const insert = (arr: any[], index: number, newItem: any) => !index || index <= 0 ? [ 2 | newItem, 3 | ...arr, 4 | ] : [ 5 | ...arr.slice(0, index), 6 | newItem, 7 | ...arr.slice(index), 8 | ]; 9 | 10 | export const insertMulti = (arr: any[], index: number, newItem: any[]) => !index || index <= 0 ? [ 11 | ...newItem, 12 | ...arr, 13 | ] : [ 14 | ...arr.slice(0, index), 15 | ...newItem, 16 | ...arr.slice(index), 17 | ]; 18 | 19 | export const uniq = (a: any[]) => [...new Set(a)]; 20 | export const uniqCaseInsensitive = (a: string[]) => [ 21 | ...new Map(a.map((s) => [s.toLowerCase(), s])).values(), 22 | ]; 23 | export const uniqueNameFromString = (name: string, cols: string[]) => { 24 | let newName = name; 25 | if (cols.includes(newName)) { 26 | let append = 1; 27 | while (cols.includes(newName)) { 28 | newName = name + append.toString(); 29 | append += 1; 30 | } 31 | } 32 | return newName; 33 | }; 34 | export const onlyUniqueProp = 35 | (prop: string) => (value: any, index: number, self: any[]) => { 36 | return self.findIndex((v) => value[prop] == v[prop]) === index; 37 | }; 38 | 39 | export const onlyUniquePropCaseInsensitive = 40 | (prop: string) => (value: any, index: number, self: any[]) => { 41 | return ( 42 | self.findIndex( 43 | (v) => value[prop].toLowerCase() == v[prop].toLowerCase() 44 | ) === index 45 | ); 46 | }; 47 | 48 | 49 | export const orderStringArrayByArray = (array: string[], order: string[]) =>{ 50 | 51 | return array.sort( function (a, b) { 52 | const A = order.indexOf(a), B = order.indexOf(b); 53 | 54 | if (A > B) { 55 | if (A != -1 && B == -1) { 56 | return -1 57 | } 58 | return 1; 59 | } else { 60 | if (B != -1 && A == -1) { 61 | return 1 62 | } 63 | return -1; 64 | } 65 | 66 | }); 67 | 68 | }; 69 | 70 | 71 | export const orderArrayByArrayWithKey = (array: any[], order: string[], key: string) =>{ 72 | 73 | return array.sort( function (a, b) { 74 | const A = order.indexOf(a[key]), B = order.indexOf(b[key]); 75 | 76 | if (A > B) { 77 | if (A != -1 && B == -1) { 78 | return -1 79 | } 80 | return 1; 81 | } else { 82 | if (B != -1 && A == -1) { 83 | return 1 84 | } 85 | return -1; 86 | } 87 | 88 | }); 89 | 90 | }; -------------------------------------------------------------------------------- /src/css/Menus/InlineMenu.css: -------------------------------------------------------------------------------- 1 | body:not(.is-mobile) .mk-style-menu { 2 | margin-left: -80px; 3 | } 4 | 5 | .mk-style-menu .mk-divider { 6 | border-left: thin solid var(--mk-ui-divider); 7 | width: 1px; 8 | height: 24px; 9 | } 10 | 11 | .mk-style-menu { 12 | display: flex; 13 | padding: 4px; 14 | margin-top: -50px; 15 | align-items: center; 16 | gap: 4px; 17 | border: 1px solid var(--background-modifier-border-hover); 18 | background-color: var(--background-secondary); 19 | border-radius: var(--radius-m); 20 | box-shadow: var(--shadow-s); 21 | z-index: var(--layer-menu); 22 | } 23 | 24 | .mk-style-toolbar svg, .mk-style-menu svg { 25 | width: 16px; 26 | height: 16px; 27 | } 28 | 29 | .mk-style-toolbar .mk-mark, .mk-style-menu .mk-mark { 30 | width: 28px; 31 | height: 28px; 32 | padding: 6px; 33 | } 34 | 35 | .mk-style-toolbar { 36 | --mobile-toolbar-height: 48px; 37 | border-radius: 0; 38 | width: 100%; 39 | margin-top: 0; 40 | overflow-x: auto; 41 | justify-content: center; 42 | height: 100%; 43 | display: flex; 44 | gap: 12px; 45 | align-items: center; 46 | } 47 | 48 | .mk-style-menu .mk-mark { 49 | margin: 4px; 50 | border-radius: 4px; 51 | display: flex; 52 | } 53 | 54 | 55 | .mk-mobile-styler .mobile-toolbar-options-list { 56 | display: none; 57 | } 58 | .mk-mobile-styler .mobile-toolbar-floating-options { 59 | display: none; 60 | } 61 | 62 | .mk-mark div { 63 | display: flex; 64 | } 65 | .mk-mark-group { 66 | display: flex; 67 | } 68 | .mk-mark-dropdown { 69 | padding: 2px; 70 | border-radius: 4px; 71 | display: flex; 72 | align-items: center; 73 | } 74 | 75 | .mk-mark-dropdown svg { 76 | width: 12px; 77 | height: 12px; 78 | transform: rotate(90deg) 79 | } 80 | 81 | .mk-style-menu .mk-mark:hover, .mk-style-menu .mk-mark-dropdown:hover { 82 | background: var(--mk-ui-background-hover); 83 | } 84 | 85 | .mk-style-menu .mk-mark-active { 86 | background: var(--mk-ui-background-hover); 87 | } 88 | 89 | .mk-style-menu svg { 90 | color: var(--mk-ui-text-secondary); 91 | } 92 | 93 | 94 | .mk-color { 95 | width: 24px; 96 | height: 24px; 97 | border-radius: 12px; 98 | margin: 8px; 99 | border: thin solid var(--mk-ui-border); 100 | } 101 | .mk-color:hover { 102 | opacity: 0.8; 103 | } 104 | 105 | mark { 106 | color: unset; 107 | border-radius: 2px; 108 | margin: 0px 2px; 109 | padding: 0px 2px; 110 | } 111 | -------------------------------------------------------------------------------- /src/shared/types/api.ts: -------------------------------------------------------------------------------- 1 | import { FrameContexts } from "shared/types/frameExec"; 2 | import { PathLabel } from "./caches"; 3 | import { DBRow, SpaceProperty } from "./mdb"; 4 | import { TargetLocation } from "./path"; 5 | 6 | export interface IAPI { 7 | frame: { 8 | update: (property: string, value: string, path: string, saveState: (state: any) => void) => void; 9 | }; 10 | properties: { 11 | color: (property: SpaceProperty, value: string) => string; 12 | sticker: (property: SpaceProperty) => string; 13 | value: (type: string, value: string) => string; 14 | }; 15 | path: { 16 | label: (path: string) => PathLabel | undefined; 17 | open: (path: string, target?: TargetLocation) => void; 18 | create: (name: string, space: string, type: string, content?: Promise | string) => void; 19 | setProperty: (path: string, property: string, value: Promise | string) => void; 20 | contextMenu: (e: React.MouseEvent, path: string) => void; 21 | }; 22 | commands: { 23 | run: (action: string, parameters?: { [key: string]: any }, contexts?: FrameContexts) => void; 24 | formula: (formula: string, parameters: { [key: string]: any }, contexts?: FrameContexts) => void; 25 | }; 26 | buttonCommand: (action: string, parameters: { [key: string]: any }, contexts: FrameContexts, saveState: (state: any) => void) => void; 27 | table: { 28 | select: (path: string, table: string) => Promise; 29 | update: (path: string, table: string, index: number, row: DBRow) => void; 30 | insert: (path: string, schema: string, row: DBRow) => Promise; 31 | create: (path: string, table: string, properties: SpaceProperty[]) => void; 32 | open: (space: string, table: string, index: number, target?: TargetLocation) => Promise; 33 | contextMenu: (e: React.MouseEvent, space: string, table: string, index: number) => Promise; 34 | }; 35 | context: { 36 | select: (path: string, table: string) => Promise; 37 | update: (path: string, file: string, field: string, value: string) => void; 38 | insert: (path: string, schema: string, name: string, row: DBRow) => Promise; 39 | }; 40 | date: { 41 | parse: (date: string) => Date; 42 | daysInMonth: (date: Date) => number; 43 | format: (date: Date, format?: string) => string; 44 | component: (date: Date, component: string) => number | undefined; 45 | offset: (date: Date, offset: number, type: string) => Date; 46 | now: () => Date; 47 | range: (start: Date, end: Date, format?: string) => string[]; 48 | }; 49 | } -------------------------------------------------------------------------------- /src/shared/types/settings.ts: -------------------------------------------------------------------------------- 1 | import { MakeBasicsSettings } from "basics/types/settings"; 2 | 3 | export type DeleteFileOption = "trash" | "permanent" | "system-trash"; 4 | export type InlineContextLayout = "horizontal" | "vertical"; 5 | 6 | export interface MakeMDSettings { 7 | defaultInitialization: boolean; 8 | filePreviewOnHover: boolean; 9 | blinkEnabled: boolean; 10 | spacesEnabled: boolean; 11 | navigatorEnabled: boolean; 12 | spacesDisablePatch: boolean; 13 | spacesPerformance: boolean; 14 | spaceRowHeight: number; 15 | mobileSpaceRowHeight: number; 16 | spacesStickers: boolean; 17 | banners: boolean; 18 | bannerHeight: number; 19 | spaceViewEnabled: boolean; 20 | sidebarTabs: boolean; 21 | showRibbon: boolean; 22 | deleteFileOption: DeleteFileOption; 23 | autoOpenFileContext: boolean; 24 | expandFolderOnClick: boolean; 25 | expandedSpaces: string[]; 26 | contextEnabled: boolean; 27 | saveAllContextToFrontmatter: boolean; 28 | activeView: string; 29 | currentWaypoint: number; 30 | activeSpace: string; 31 | hideFrontmatter: boolean; 32 | spacesFolder: string; 33 | spacesMDBInHidden: boolean; 34 | autoAddContextsToSubtags: boolean; 35 | folderNoteInsideFolder: boolean; 36 | folderNoteName: string; 37 | enableFolderNote: boolean; 38 | folderIndentationLines: boolean; 39 | revealActiveFile: boolean; 40 | hiddenFiles: string[]; 41 | skipFolders: string[]; 42 | skipFolderNames: string[]; 43 | hiddenExtensions: string[]; 44 | newFileLocation: string; 45 | newFileFolderPath: string; 46 | inlineContext: boolean; 47 | inlineContextProperties: boolean; 48 | imageThumbnails: boolean; 49 | inlineBacklinks: boolean; 50 | defaultDateFormat: string; 51 | defaultTimeFormat: string; 52 | inlineBacklinksExpanded: boolean; 53 | inlineContextExpanded: boolean; 54 | inlineContextSectionsExpanded: boolean; 55 | inlineContextNameLayout: InlineContextLayout; 56 | spacesUseAlias: boolean, 57 | spaceSubFolder: string, 58 | suppressedWarnings: string[], 59 | fmKeyAlias: string; 60 | fmKeyBanner: string; 61 | fmKeyBannerOffset: string; 62 | fmKeyColor: string; 63 | fmKeySticker: string; 64 | openSpacesOnLaunch: boolean; 65 | spacesRightSplit: boolean; 66 | indexSVG: boolean; 67 | readableLineWidth: boolean; 68 | syncFormulaToFrontmatter: boolean; 69 | releaseNotesPrompt: number; 70 | firstLaunch: boolean; 71 | enableDefaultSpaces: boolean; 72 | showSpacePinIcon: boolean; 73 | experimental: boolean; 74 | systemName: string; 75 | defaultSpaceTemplate: string; 76 | selectedKit: string; 77 | actionMaxSteps: number; 78 | contextPagination: number; 79 | searchWorker: boolean; 80 | newNotePlaceholder: string; 81 | cacheIndex: boolean; 82 | enhancedLogs: boolean; 83 | basics: boolean; 84 | basicsSettings: MakeBasicsSettings; 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/shared/types/PathState.ts: -------------------------------------------------------------------------------- 1 | import { PathLabel } from "./caches"; 2 | import { SpaceProperty, SpaceTable, SpaceTableSchema } from "./mdb"; 3 | import { MDBFrames } from "./mframe"; 4 | import { SpaceDefinition, SpaceType } from "./spaceDef"; 5 | import { SpaceInfo } from "./spaceInfo"; 6 | 7 | export type SuperstateEvent = { 8 | "pathCreated": { path: string; }; 9 | "pathChanged": { path: string; newPath: string; }; 10 | "pathDeleted": { path: string; }; 11 | "pathStateUpdated": { path: string; }; 12 | "spaceChanged": { path: string; newPath: string; }; 13 | "spaceDeleted": { path: string; }; 14 | "spaceStateUpdated": { path: string; }; 15 | "contextStateUpdated": { path: string; }; 16 | "frameStateUpdated": { path: string; schemaId?: string; }; 17 | "actionStateUpdated": { path: string; }; 18 | "settingsChanged": null; 19 | "warningsChanged": null; 20 | "focusesChanged": null; 21 | "superstateUpdated": null; 22 | "superstateReindex": null; 23 | }; 24 | export type WorkerJobType = { 25 | type: string; 26 | path: string; 27 | payload?: { [key: string]: any; }; 28 | }; 29 | export type SpaceState = { 30 | name: string; 31 | path: string; 32 | templates?: string[]; 33 | metadata?: SpaceDefinition; 34 | dependencies?: string[]; 35 | space?: SpaceInfo; 36 | contexts?: string[]; 37 | type: SpaceType; 38 | sortBy?: string; 39 | sortable?: boolean; 40 | 41 | propertyTypes?: SpaceProperty[]; 42 | properties?: Record; 43 | } & CacheState; 44 | 45 | 46 | 47 | export type ContextState = { 48 | path: string; 49 | schemas: SpaceTableSchema[]; 50 | contextTable: SpaceTable; 51 | //outlinks contained 52 | outlinks: string[]; 53 | //contexts to notify if values change 54 | contexts: string[]; 55 | paths: string[]; 56 | spaceMap: { [key: string]: { [key: string]: string[]; }; }; 57 | dbExists: boolean; 58 | 59 | }; 60 | 61 | export type FrameState = { 62 | path: string; 63 | schemas: SpaceTableSchema[]; 64 | frames: MDBFrames; 65 | }; 66 | 67 | export type TagsCache = { 68 | tag: string; 69 | files: string[]; 70 | }; 71 | 72 | export type CacheState = { 73 | rank?: number; 74 | }; 75 | //everything needed to construct the file 76 | export type PathState = { 77 | //File System Metadata 78 | path: string; 79 | 80 | name?: string; 81 | parent?: string; 82 | type?: string; 83 | subtype?: string; 84 | label: PathLabel; 85 | metadata?: Record; 86 | properties?: Record; 87 | hidden?: boolean; 88 | spaces?: string[]; 89 | linkedSpaces?: string[]; 90 | liveSpaces?: string[]; 91 | tags?: string[]; 92 | inlinks?: string[]; 93 | outlinks?: string[]; 94 | readOnly: boolean; 95 | spaceNames?: string[]; 96 | } & CacheState; 97 | -------------------------------------------------------------------------------- /src/css/Menus/MainMenu.css: -------------------------------------------------------------------------------- 1 | .mk-main-menu-container { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 12px 12px 0px 12px; 5 | gap: 4px; 6 | align-items: center; 7 | margin-right: 10px; 8 | } 9 | 10 | 11 | .mk-main-menu-inner { 12 | display: flex; 13 | width: 100%; 14 | align-items: center; 15 | gap: 8px; 16 | } 17 | 18 | .mk-main-menu { 19 | display: flex; 20 | transition: all 0.2s ease; 21 | align-items: center; 22 | gap: 8px; 23 | flex: 1; 24 | } 25 | 26 | .mk-main-menu.mk-hidden { 27 | transform: translateX(-100%); 28 | opacity: 0; 29 | pointer-events: none; 30 | } 31 | 32 | .mk-main-menu-search { 33 | transition: all 0.2s ease; 34 | align-items: center; 35 | gap: 8px; 36 | } 37 | 38 | .mk-main-menu-search.mk-hidden { 39 | 40 | opacity: 0; 41 | pointer-events: none; 42 | 43 | } 44 | .mk-main-menu-button { 45 | font-weight: var(--font-medium); 46 | font-size: 16px; 47 | padding: 8px 10px; 48 | text-align: left; 49 | border-radius: 4px; 50 | align-items: center; 51 | display: flex; 52 | gap: 4px; 53 | justify-content: center; 54 | } 55 | .mk-main-menu-search { 56 | position: absolute; 57 | display: flex; 58 | width: calc(100% - 60px) 59 | } 60 | .mk-main-menu-search input { 61 | width: 100%; 62 | } 63 | .mk-main-menu-icon { 64 | background: var(--nav-item-background-hover); 65 | padding: 2px; 66 | border-radius: 2px; 67 | text-transform: uppercase; 68 | width: 20px; 69 | height: 20px; 70 | font-size: 12px; 71 | justify-content: center; 72 | margin-right: 4px; 73 | } 74 | 75 | 76 | .is-mobile .mk-main-menu-button { 77 | font-size: 16px; 78 | font-weight: var(--font-medium); 79 | gap: 4px; 80 | padding: 8px 12px !important; 81 | } 82 | 83 | .mk-main-menu-button > div { 84 | display: flex; 85 | } 86 | 87 | .workspace-drawer.mod-left.is-pinned { 88 | min-width: 350px; 89 | } 90 | 91 | .mk-main-menu-button.mk-main-menu-button-primary { 92 | flex-grow: 1; 93 | line-height: 1; 94 | justify-content: flex-start; 95 | } 96 | 97 | .mk-main-menu .mk-main-menu-sticker { 98 | margin-right: 8px; 99 | } 100 | 101 | .mk-main-menu-button svg { 102 | height: 16px; 103 | width: 16px; 104 | } 105 | body:not(.is-mobile) .mk-main-menu-button:hover { 106 | background: var(--nav-item-background-hover); 107 | } 108 | 109 | .mk-menu-button { 110 | display: flex; 111 | padding-top: 0.5rem; 112 | padding-bottom: 0.5rem; 113 | padding-left: 0.5rem; 114 | padding-right: 0.5rem; 115 | font-size: 0.75rem; 116 | line-height: 1.25rem; 117 | align-items: center; 118 | width: 100%; 119 | border-radius: 0.375rem; 120 | } 121 | .mk-menu-button:hover { 122 | background: var(--nav-item-background-hover); 123 | } 124 | 125 | .mk-main-menu-container .mk-query { 126 | width: 100%; 127 | } 128 | -------------------------------------------------------------------------------- /src/css/Editor/Frames/Page.css: -------------------------------------------------------------------------------- 1 | .mk-frame-column { 2 | position: absolute !important; 3 | width: 100%; 4 | height: 100%; 5 | z-index: var(--layer-popover); 6 | } 7 | .mk-frame-column-resize-handle:hover { 8 | border-right: 1px solid var(--nav-indentation-guide-color) 9 | } 10 | .mk-frame-column-placeholder { 11 | position: absolute; 12 | right: -16px; 13 | width: 16px; 14 | bottom: 0; 15 | } 16 | 17 | .is-mobile .mk-editor-frame-hover-menu-container { 18 | right: -14px; 19 | } 20 | 21 | .mk-editor-frame-hover-menu-container { 22 | margin-left: -30px; 23 | position: absolute; 24 | padding: 4px; 25 | z-index: var(--layer-popover); 26 | pointer-events: auto; 27 | } 28 | .mk-editor-frame-hover-menu-container .mk-editor-frame-hover-button svg{ 29 | width: 20px; 30 | height: 20px; 31 | } 32 | 33 | .mk-editor-frame-hover-menu-container .mk-editor-frame-hover-button:hover { 34 | background: var(--nav-item-background-hover); 35 | border-radius: 4px; 36 | } 37 | 38 | .mk-editor-frame-hover-menu, .mk-editor-frame-hover-horizontal { 39 | transition: all 0.2s ease; 40 | border-radius: 4px; 41 | display: flex; 42 | font-size: 12px; 43 | background: var(--mk-ui-background); 44 | gap: 4px; 45 | flex-direction:column 46 | } 47 | 48 | .mk-frame-drop-zone-container { 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | z-index: var(--layer-popover); 53 | } 54 | .mk-frame-drop-zone { 55 | position: absolute; 56 | } 57 | .mk-indicator-bottom::before { 58 | position: absolute; 59 | content: " "; 60 | display: block; 61 | width: 100%; 62 | height: 2px; 63 | bottom: 0; 64 | border-radius: 1px; 65 | background: var(--mk-ui-active); 66 | 67 | } 68 | .mk-indicator-right::before { 69 | position: absolute; 70 | content: " "; 71 | display: block; 72 | width: 2px; 73 | height: 100%; 74 | left: 0; 75 | border-radius: 1px; 76 | background: var(--mk-ui-active); 77 | 78 | } 79 | .mk-indicator-insert::before { 80 | position: absolute; 81 | content: " "; 82 | display: block; 83 | width: 100%; 84 | height: 2px; 85 | bottom: 0; 86 | border-radius: 1px; 87 | background: yellow; 88 | } 89 | .mk-indicator-top::before { 90 | position: absolute; 91 | content: " "; 92 | display: block; 93 | width: 100%; 94 | height: 2px; 95 | top: 0; 96 | border-radius: 1px; 97 | background: var(--mk-ui-active); 98 | 99 | } 100 | .mk-indicator-left::before { 101 | position: absolute; 102 | content: " "; 103 | display: block; 104 | width: 2px; 105 | height: 100%; 106 | right: 0; 107 | border-radius: 1px; 108 | background: var(--mk-ui-active); 109 | 110 | } 111 | 112 | -------------------------------------------------------------------------------- /src/css/Editor/Properties/DatePicker.css: -------------------------------------------------------------------------------- 1 | .mk-date-picker-container { 2 | display: flex; 3 | gap: 8px; 4 | align-items: center; 5 | padding: 12px; 6 | width: 280px; 7 | flex-direction: column; 8 | } 9 | 10 | .mk-date-picker { 11 | display: flex; 12 | gap: 8px; 13 | align-items: center; 14 | width: 100%; 15 | } 16 | 17 | .mk-date-picker table { 18 | border-spacing: 0; 19 | table-layout: fixed; 20 | width: 100%; 21 | } 22 | 23 | .mk-date-picker-header { 24 | display: flex; 25 | width: 100%; 26 | justify-content: space-between; 27 | align-items: center; 28 | font-size: 14px; 29 | --icon-size: 16px; 30 | --icon-container-size: 24px; 31 | } 32 | 33 | .mk-date-picker-header .mk-date-picker-header-input { 34 | display: flex; 35 | gap: 4px; 36 | } 37 | .mk-date-picker-header-input input:first-child { 38 | width: 30px; 39 | } 40 | .mk-date-picker-header-input input { 41 | width: 60px; 42 | text-align: right; 43 | padding: 4px; 44 | border-radius: 4px; 45 | border: thin solid var(--mk-ui-border); 46 | } 47 | 48 | .mk-date-picker-time { 49 | display: flex; 50 | gap: 8px; 51 | align-items: center; 52 | --icon-size: 16px; 53 | --icon-container-size: 30px; 54 | } 55 | 56 | 57 | .mk-date-picker-time input { 58 | width: 40px; 59 | text-align: right; 60 | padding: 4px; 61 | border-radius: 4px; 62 | border: thin solid var(--mk-ui-border); 63 | } 64 | 65 | 66 | .mk-date-picker-time svg { 67 | width: var(--icon-size); 68 | height: var(--icon-size); 69 | } 70 | 71 | .mk-date-picker-header button, .mk-date-picker-time button { 72 | background: none; 73 | box-shadow: none; 74 | border: thin solid var(--mk-ui-border); 75 | width: var(--icon-container-size); 76 | height: var(--icon-container-size); 77 | padding: 0; 78 | } 79 | 80 | button.mk-date-picker-day.mk-date-picker-today { 81 | background: var(--mk-ui-background-active); 82 | } 83 | 84 | button.mk-date-picker-day.mk-date-picker-selected { 85 | background: var(--mk-ui-background-reverse); 86 | color: var(--mk-ui-text-reverse) 87 | } 88 | 89 | .mk-date-picker-header button svg { 90 | width: var(--icon-size); 91 | height: var(--icon-size); 92 | } 93 | 94 | button.mk-date-picker-day { 95 | width: 100%; 96 | background: none; 97 | box-shadow: none; 98 | padding: 0; 99 | display: flex; 100 | align-items: center; 101 | justify-content: center; 102 | font-size: 14px; 103 | border-radius: 4px; 104 | } 105 | 106 | button.mk-date-picker-day:not(.mk-date-picker-selected):hover { 107 | background: var(--mk-ui-background-hover); 108 | 109 | } 110 | 111 | .mk-date-picker-cell { 112 | width: 14%; 113 | } 114 | 115 | .mk-date-picker-day:hover { 116 | background: var(--mk-ui-background-hover); 117 | border-radius: 4px; 118 | } 119 | 120 | .mk-date-picker-months { 121 | width: 100%; 122 | } 123 | 124 | .mk-date-picker-month { 125 | width: 100%; 126 | display: flex; 127 | flex-direction: column; 128 | gap: 8px; 129 | 130 | } -------------------------------------------------------------------------------- /src/shared/types/frameExec.ts: -------------------------------------------------------------------------------- 1 | import { ScreenType } from "shared/types/ui"; 2 | import { IAPI } from "./api"; 3 | import { FrameNode, FrameTreeProp } from "./mframe"; 4 | 5 | 6 | export type FrameExecutable = FrameTreeNode & { 7 | execPropsOptions?: { 8 | props?: FrameExecPropCache[]; 9 | deps?: string[][]; 10 | children?: string[]; 11 | template?: FrameExecutable[]; 12 | }; 13 | execProps?: FrameExecProp; 14 | execStyles?: FrameExecProp; 15 | execActions?: FrameExecProp; 16 | children: FrameExecutable[]; 17 | }; 18 | 19 | export type FrameExecutableContext = { 20 | runID: string; 21 | root: FrameTreeNode; 22 | exec: FrameExecutable; 23 | saveState: (state: FrameState, instance: FrameRunInstance) => void; 24 | api: IAPI; 25 | contexts: FrameContexts; 26 | selectedSlide: string; 27 | };export type FrameRunInstance = { 28 | id: string; 29 | state: FrameState; 30 | prevState?: FrameState; 31 | slides: FrameState; 32 | root: FrameTreeNode; 33 | exec: FrameExecutable; 34 | newState?: FrameState; 35 | contexts: FrameContexts; 36 | }; 37 | export type FrameContexts = { [key: string]: FrameTreeProp; }; 38 | export type FrameStateKeys = 'props' | 'actions' | 'styles'; 39 | export type FrameNodeState = Partial<{ props: FrameTreeProp; actions: FrameTreeProp; styles: FrameTreeProp; }>; 40 | export type FrameState = { [key: string]: FrameNodeState; } & { $contexts?: FrameContexts; $api?: IAPI; }; 41 | export type FrameStateUpdate = { newState: FrameState; updatedState: FrameState; }; 42 | export enum FrameEditorResizeMode { 43 | ResizeNever, 44 | ResizeSelected, 45 | ResizeAlways 46 | } 47 | 48 | export enum FrameEditorMode { 49 | Read = 0, 50 | Page = 1, 51 | Frame = 2, 52 | Group = 3 53 | } 54 | 55 | export enum FrameResizeMode { 56 | ResizeNever, 57 | ResizeSelected, 58 | ResizeColumn, 59 | ResizeAlways 60 | } 61 | export enum FrameSelectMode { 62 | SelectNever, 63 | SelectManual, 64 | SelectDrag, 65 | SelectClick 66 | } 67 | 68 | export enum FrameDragMode { 69 | DragNever, 70 | DragHandle, 71 | DragSelected, 72 | DragAlways 73 | } 74 | export enum FrameDropMode { 75 | DropModeNone, 76 | DropModeRowColumn, 77 | DropModeRowOnly, 78 | DropModeColumnOnly 79 | } 80 | 81 | 82 | export type FrameEditorProps = { rootId?: string; editMode: FrameEditorMode; parentType?: string; parentLastChildID?: string; dragMode?: FrameDragMode; selectMode?: FrameSelectMode; resizeMode?: FrameResizeMode; dropMode?: FrameDropMode; linkedNode?: LinkedNode; screenType?: ScreenType; }; 83 | export const defaultFrameEditorProps: FrameEditorProps = { editMode: 0 }; 84 | export type FrameExecPropCache = { 85 | name: string; 86 | isConst: boolean; 87 | deps: string[][]; 88 | }; 89 | 90 | export type LinkedNode = { 91 | node: string; 92 | prop: string; 93 | }; 94 | 95 | export type LinkedContext = { 96 | context: string; 97 | prop: string; 98 | }; 99 | 100 | export type FrameExecProp = { 101 | [key: string]: any; 102 | }; 103 | export type FrameTreeNode = { 104 | id: string; 105 | node: FrameNode; 106 | isRef: boolean; 107 | children: FrameTreeNode[]; 108 | editorProps: FrameEditorProps; 109 | parent: FrameTreeNode | null; 110 | }; 111 | 112 | -------------------------------------------------------------------------------- /src/css/Panels/SpaceEditor.css: -------------------------------------------------------------------------------- 1 | .mk-space-editor-modal { 2 | height: var(--dialog-max-height) 3 | } 4 | 5 | .mk-space-editor-container { 6 | display: flex; 7 | flex-direction: column; 8 | overflow: hidden; 9 | flex: 1; 10 | } 11 | 12 | .mk-space-editor { 13 | display: flex; 14 | flex-direction: column; 15 | margin-bottom: 20px; 16 | gap: 12px; 17 | flex: 1; 18 | overflow: hidden; 19 | } 20 | .mk-space-editor-appearance .mk-path-icon, .mk-space-editor-appearance .mk-path-icon button, .mk-space-editor-appearance .mk-path-icon svg { 21 | width: 48px; 22 | height: 48px; 23 | font-size: 36px; 24 | } 25 | .mk-space-editor-appearance .mk-path-icon svg { 26 | padding: 6px; 27 | } 28 | .mk-space-query { 29 | display: flex; 30 | flex-direction: column; 31 | height: 100%; 32 | } 33 | .mk-space-editor-context-list { 34 | display: flex; 35 | margin-bottom: 8px; 36 | } 37 | .mk-space-editor-section { 38 | display: flex; 39 | flex-direction: column; 40 | gap: 8px; 41 | border-bottom: thin solid var(--mk-ui-divider); 42 | } 43 | 44 | .mk-space-editor-header .mk-button-new 45 | { 46 | --icon-size: 16px; 47 | padding: 8px 48 | 49 | } 50 | 51 | .mk-space-editor-header span{ 52 | flex:1 53 | } 54 | .mk-space-editor-contents { 55 | display: flex; 56 | flex-direction:column; 57 | gap: 4px; 58 | overflow: scroll; 59 | flex: 1; 60 | } 61 | .mk-space-editor-contents .mk-path { 62 | overflow: visible; 63 | } 64 | .mk-space-editor-controls { 65 | display: flex; 66 | justify-content: space-between 67 | } 68 | .mk-space-editor-link { 69 | display: flex; 70 | font-size: 13px; 71 | color: var(--mk-ui-text-secondary); 72 | gap: 6px; 73 | align-items: center; 74 | border-radius: 6px; 75 | } 76 | .mk-space-editor-link span { 77 | flex: 1; 78 | } 79 | .mk-space-editor-smart { 80 | padding: 8px; 81 | border-radius: 6px; 82 | display: flex; 83 | flex-direction: column; 84 | align-items: flex-start; 85 | gap: 8px; 86 | margin-bottom: 8px; 87 | } 88 | 89 | .mk-space-editor-smart > span { 90 | font-size: 14px; 91 | color: var(--mk-ui-text-tertiary); 92 | font-weight: var(--font-semibold); 93 | } 94 | 95 | .mk-space-editor-smart .mk-query { 96 | width: 100%;gap: 8px; 97 | } 98 | 99 | .mk-space-editor-smart .mk-query-filters { 100 | background: var(--mk-ui-background-contrast); 101 | padding: 4px 8px; 102 | } 103 | 104 | .mk-space-editor-smart .mk-query-group-type { 105 | width: 24px; 106 | padding: 4px; 107 | } 108 | 109 | .mk-space-editor-appearance { 110 | display: flex; 111 | width: 100%; 112 | gap: 16px; 113 | align-items: center; 114 | } 115 | 116 | .mk-space-editor-input input { 117 | font-weight: var(--inline-title-weight); 118 | font-size: var(--inline-title-size); 119 | line-height: var(--inline-title-line-height); 120 | font-style: var(--inline-title-style); 121 | font-variant: var(--inline-title-variant); 122 | font-family: var(--inline-title-font); 123 | color: var(--inline-title-color); 124 | background: none; 125 | outline: none; 126 | border: none; 127 | } -------------------------------------------------------------------------------- /src/css/SpaceViewer/Layout.css: -------------------------------------------------------------------------------- 1 | 2 | .mk-layout-row { 3 | display: flex; 4 | } 5 | 6 | .mk-padding-4 { 7 | padding: 4px; 8 | } 9 | 10 | .mk-border-radius-4 { 11 | border-radius: 4px; 12 | } 13 | 14 | .mk-hover:hover { 15 | background: var(--mk-ui-background-hover) 16 | } 17 | 18 | .mk-padding-12 { 19 | padding: 12px; 20 | } 21 | 22 | .mk-layout-scroll { 23 | overflow: scroll; 24 | } 25 | 26 | .mk-f-shadow { 27 | box-shadow: var(--shadow-x) var(--shadow-y) var(--shadow-blur) var(--shadow-spread) rgba(var(--shadow-color), calc(var(--shadow-alpha) / 100)) 28 | } 29 | .mk-layout-row > .mk-f { 30 | width: 0; 31 | } 32 | .mk-layout-column { 33 | display: flex; 34 | flex-direction: column; 35 | align-items: flex-start; 36 | } 37 | .mk-layout-grid { 38 | display: grid; 39 | grid-template-columns: repeat(var(--mk-grid-columns), minmax(var(--mk-grid-width), 1fr)); 40 | } 41 | .mk-layout-masonry { 42 | column-count: 3 43 | } 44 | .mk-layout-masonry > * { 45 | width: 100%; 46 | -webkit-column-break-inside: avoid; 47 | -o-column-break-inside: avoid; 48 | -moz-column-break-inside: avoid; 49 | break-inside: avoid; 50 | } 51 | 52 | .mk-layout-row.mk-layout-align-nw, 53 | .mk-layout-row.mk-layout-align-n, 54 | .mk-layout-row.mk-layout-align-ne, 55 | .mk-layout-column.mk-layout-align-nw, 56 | .mk-layout-column.mk-layout-align-w, 57 | .mk-layout-column.mk-layout-align-sw { 58 | align-items: flex-start; 59 | } 60 | 61 | .mk-layout-column.mk-layout-align-nw, 62 | .mk-layout-column.mk-layout-align-n, 63 | .mk-layout-column.mk-layout-align-ne, 64 | .mk-layout-row.mk-layout-align-nw, 65 | .mk-layout-row.mk-layout-align-w, 66 | .mk-layout-row.mk-layout-align-sw { 67 | justify-content: flex-start; 68 | } 69 | 70 | .mk-layout-row.mk-layout-align-w, 71 | .mk-layout-row.mk-layout-align-m, 72 | .mk-layout-row.mk-layout-align-e, 73 | .mk-layout-column.mk-layout-align-n, 74 | .mk-layout-column.mk-layout-align-m, 75 | .mk-layout-column.mk-layout-align-s { 76 | align-items: center; 77 | } 78 | 79 | 80 | .mk-layout-column.mk-layout-align-w, 81 | .mk-layout-column.mk-layout-align-m, 82 | .mk-layout-column.mk-layout-align-e, 83 | .mk-layout-row.mk-layout-align-n, 84 | .mk-layout-row.mk-layout-align-m, 85 | .mk-layout-row.mk-layout-align-s { 86 | justify-content: center; 87 | } 88 | 89 | 90 | .mk-layout-row.mk-layout-align-sw, 91 | .mk-layout-row.mk-layout-align-s, 92 | .mk-layout-row.mk-layout-align-se, 93 | .mk-layout-column.mk-layout-align-ne, 94 | .mk-layout-column.mk-layout-align-e, 95 | .mk-layout-column.mk-layout-align-se { 96 | align-items: flex-end; 97 | } 98 | 99 | .mk-layout-column.mk-layout-align-sw, 100 | .mk-layout-column.mk-layout-align-s, 101 | .mk-layout-column.mk-layout-align-se, 102 | .mk-layout-row.mk-layout-align-ne, 103 | .mk-layout-row.mk-layout-align-e, 104 | .mk-layout-row.mk-layout-align-se { 105 | justify-content: flex-end; 106 | } 107 | 108 | .mk-layout-wrap { 109 | flex-wrap: wrap; 110 | } 111 | 112 | .mk-layout-nowrap { 113 | flex-wrap: nowrap; 114 | } 115 | .mk-masonry { 116 | column-count: attr(columns) 117 | } 118 | 119 | .mk-gap-4 { 120 | gap: 4px; 121 | } 122 | .mk-gap-8 { 123 | gap: 8px; 124 | } 125 | .mk-gap-16 { 126 | gap: 16px; 127 | } -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import builtins from 'builtin-modules'; 2 | import * as dotenv from 'dotenv'; 3 | import esbuild from "esbuild"; 4 | import { copy } from 'esbuild-plugin-copy'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import process from "process"; 8 | 9 | dotenv.config() 10 | 11 | const banner = 12 | `/* 13 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 14 | if you want to view the source, please visit the github repository of this plugin 15 | */ 16 | `; 17 | 18 | const demo = (process.argv[2] === 'demo'); 19 | const prod = (process.argv[2] === 'production'); 20 | const prev = (process.argv[2] === 'preview'); 21 | const buildv = prod || prev 22 | 23 | 24 | import findCacheDir from 'find-cache-dir'; 25 | 26 | 27 | 28 | let cacheDir = findCacheDir({ 29 | name: 'esbuild-plugin-inline-worker', 30 | create: true, 31 | }); 32 | 33 | async function buildWorker(workerPath, extraConfig) { 34 | let scriptNameParts = path.basename(workerPath).split('.'); 35 | scriptNameParts.pop(); 36 | scriptNameParts.push('js'); 37 | let scriptName = scriptNameParts.join('.'); 38 | let bundlePath = path.resolve(cacheDir, scriptName); 39 | 40 | if (extraConfig) { 41 | delete extraConfig.entryPoints; 42 | delete extraConfig.outfile; 43 | delete extraConfig.outdir; 44 | delete extraConfig.workerName; 45 | } 46 | 47 | 48 | await esbuild.build({ 49 | entryPoints: [workerPath], 50 | bundle: true, 51 | minify: true, 52 | outfile: bundlePath, 53 | define: { 54 | process: 'process', 55 | }, 56 | target: 'es2020', 57 | format: 'cjs', 58 | ...extraConfig, 59 | }); 60 | 61 | return fs.promises.readFile(bundlePath, {encoding: 'utf-8'}); 62 | } 63 | 64 | 65 | 66 | let renamePlugin = { 67 | name: 'rename-styles', 68 | setup(build) { 69 | build.onEnd(() => { 70 | const { outfile } = build.initialOptions; 71 | const outcss = outfile.replace(/\.js$/, '.css'); 72 | const fixcss = outfile.replace(/main\.js$/, 'styles.css'); 73 | if (fs.existsSync(outcss)) { 74 | console.log('Renaming', outcss, 'to', fixcss); 75 | fs.renameSync(outcss, fixcss); 76 | } 77 | }); 78 | }, 79 | }; 80 | 81 | 82 | const outputDir = prev ? process.env.prevDir : prod ? process.env.buildDir : demo ? process.env.demoDir : process.env.devDir 83 | esbuild.build({ 84 | banner: { 85 | js: banner, 86 | }, 87 | entryPoints: ['main.ts'], 88 | bundle: true, 89 | external: [ 90 | 'obsidian', 91 | 'electron', 92 | // '@codemirror/autocomplete', 93 | '@codemirror/collab', 94 | '@codemirror/commands', 95 | // '@codemirror/language', 96 | '@codemirror/lint', 97 | '@codemirror/search', 98 | '@codemirror/state', 99 | '@codemirror/view', 100 | ...builtins], 101 | format: 'cjs', 102 | loader: { 103 | '.ttf': 'base64', 104 | }, 105 | watch: !buildv, 106 | target: 'es2020', 107 | logLevel: "info", 108 | sourcemap: buildv ? false : 'inline', 109 | treeShaking: true, 110 | minify: true, 111 | outfile: outputDir+'/main.js', 112 | define: { 'process.env.NODE_ENV': prod ? '"production"' : '"development"' }, 113 | plugins: [renamePlugin, 114 | // preactCompatPlugin, 115 | ...(buildv ? [copy({ 116 | resolveFrom: 'cwd', 117 | assets: { 118 | from: 'manifest.json', 119 | to: outputDir+'/manifest.json', 120 | }, 121 | })] : []), 122 | ], 123 | }).catch(() => process.exit(1)); 124 | 125 | -------------------------------------------------------------------------------- /src/basics/menus/StickerMenu.tsx: -------------------------------------------------------------------------------- 1 | import MakeBasicsPlugin from "main"; 2 | import { 3 | App, 4 | Editor, 5 | EditorPosition, 6 | EditorSuggest, 7 | EditorSuggestContext, 8 | EditorSuggestTriggerInfo, 9 | TFile, 10 | } from "obsidian"; 11 | import React from "react"; 12 | import { emojis } from "shared/assets/emoji"; 13 | import i18n from "shared/i18n"; 14 | import { Emoji, EmojiData } from "shared/types/emojis"; 15 | import { emojiFromString } from "shared/utils/stickers"; 16 | 17 | export default class StickerMenu extends EditorSuggest { 18 | inCmd = false; 19 | cmdStartCh = 0; 20 | plugin: MakeBasicsPlugin; 21 | 22 | constructor(app: App, plugin: MakeBasicsPlugin) { 23 | super(app); 24 | this.plugin = plugin; 25 | this.emojis = Object.keys(emojis as EmojiData).reduce( 26 | (p, c) => [ 27 | ...p, 28 | ...emojis[c].map((e) => ({ 29 | label: e.n[0], 30 | desc: e.n[1], 31 | variants: e.v, 32 | unicode: e.u, 33 | })), 34 | ], 35 | [] 36 | ); 37 | } 38 | resetInfos() { 39 | this.cmdStartCh = 0; 40 | this.inCmd = false; 41 | } 42 | 43 | onTrigger( 44 | cursor: EditorPosition, 45 | editor: Editor, 46 | _file: TFile 47 | ): EditorSuggestTriggerInfo { 48 | const currentLine = editor.getLine(cursor.line).slice(0, cursor.ch); 49 | 50 | if ( 51 | !this.inCmd && 52 | !( 53 | currentLine.slice(-2) == " " + this.plugin.settings.emojiTriggerChar || 54 | currentLine[0] == this.plugin.settings.emojiTriggerChar 55 | ) 56 | ) { 57 | this.resetInfos(); 58 | return null; 59 | } 60 | 61 | if (!this.inCmd) { 62 | this.cmdStartCh = currentLine.length - 1; 63 | this.inCmd = true; 64 | } 65 | 66 | const currentCmd = currentLine.slice(this.cmdStartCh, cursor.ch); 67 | 68 | if ( 69 | currentCmd.includes(" ") || 70 | !currentCmd.includes(this.plugin.settings.emojiTriggerChar) 71 | ) { 72 | this.resetInfos(); 73 | return null; 74 | } 75 | // @ts-ignore 76 | this.suggestEl.classList.toggle("mk-emoji-menu", true); 77 | return { start: cursor, end: cursor, query: currentCmd.slice(1) }; 78 | } 79 | 80 | emojis: Emoji[]; 81 | 82 | getSuggestions(context: EditorSuggestContext): Emoji[] | Promise { 83 | const suggestions = this.emojis.filter( 84 | ({ label, desc }) => 85 | label.includes(context.query) || desc?.includes(context.query) 86 | ); 87 | 88 | return suggestions.length > 0 89 | ? suggestions 90 | : [{ label: i18n.commandsSuggest.noResult, unicode: "", desc: "" }]; 91 | } 92 | 93 | renderSuggestion(value: Emoji, el: HTMLElement): void { 94 | const div = document.createElement("div"); 95 | div.setAttribute("aria-label", value.label); 96 | const reactElement = this.plugin.enactor.createRoot(div); 97 | reactElement.render( 98 | <> 99 | {value.unicode.length > 0 100 | ? emojiFromString(value.unicode) 101 | : i18n.commandsSuggest.noResult} 102 | 103 | ); 104 | el.appendChild(div); 105 | } 106 | 107 | selectSuggestion(cmd: Emoji, _evt: MouseEvent | KeyboardEvent): void { 108 | if (cmd.label === i18n.commandsSuggest.noResult) return; 109 | 110 | this.context.editor.replaceRange( 111 | emojiFromString(cmd.unicode), 112 | { ...this.context.start, ch: this.cmdStartCh }, 113 | this.context.end 114 | ); 115 | 116 | this.resetInfos(); 117 | 118 | this.close(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/css/SpaceViewer/Nodes.css: -------------------------------------------------------------------------------- 1 | .workspace-leaf-content img:not([width]).mk-node-image { 2 | max-width: unset; 3 | } 4 | .mk-node-new { 5 | display: flex; 6 | align-items: center; 7 | } 8 | .mk-frame-edit[data-path="main"] + .mk-node-new { 9 | margin-bottom: 100px; 10 | } 11 | .mk-node-type { 12 | display: flex; 13 | padding: 4px; 14 | border: thin solid var(--mk-ui-border); 15 | border-radius: 4px; 16 | font-size: 12px; 17 | } 18 | .mk-node-image { 19 | object-fit: cover; 20 | max-width: 100% !important; 21 | } 22 | .mk-node-link { 23 | max-width: 100%; 24 | display: flex; 25 | gap: 4px; 26 | align-items: center; 27 | margin-bottom: 4px; 28 | } 29 | .mk-node-link .mk-path { 30 | padding: 0; 31 | } 32 | .mk-node-link:hover .mk-collapse { 33 | opacity: 1; 34 | } 35 | .mk-node-link .mk-collapse { 36 | opacity: 0; 37 | width: 24px !important; 38 | height: 24px !important; 39 | } 40 | .mk-node-link .mk-collapse svg { 41 | transform: rotate(90deg) 42 | } 43 | .mk-node-link .mk-collapse.mk-collapsed svg { 44 | transform: rotate(-90deg) 45 | } 46 | .mk-node-link .mk-collapse:hover { 47 | background: var(--mk-ui-background-hover) !important; 48 | } 49 | .mk-frame-text { 50 | 51 | text-overflow: ellipsis; 52 | display: -webkit-box; 53 | overflow: hidden; 54 | -webkit-line-clamp: var(--line-count); 55 | -webkit-box-orient: vertical; 56 | color: inherit; 57 | font-size: var(--font-text-size); 58 | font-weight: var(--font-text-weight); 59 | font-style: var(--font-text-style); 60 | text-decoration: var(--font-text-decoration); 61 | color: var(--font-text-color); 62 | font-family: var(--font-text); 63 | white-space: pre-wrap; 64 | } 65 | .mk-frame-text p { 66 | display: inline; 67 | } 68 | .mk-frame-icon { 69 | display:flex; 70 | width: 100%; 71 | height: 100%; 72 | } 73 | 74 | .mk-frame-icon svg { 75 | width: var(--icon-size); 76 | height: var(--icon-size); 77 | } 78 | 79 | 80 | .mk-frame-text[data-placeholder]:empty::before { 81 | content: attr(data-placeholder); 82 | color: var(--mk-ui-text-tertiary); 83 | } 84 | 85 | .mk-node-input[type='text']:active, .mk-node-input[type='text']:focus { 86 | box-shadow: none; 87 | 88 | } 89 | 90 | .mk-button { 91 | -webkit-app-region: no-drag; 92 | display: inline-flex; 93 | align-items: center; 94 | justify-content: center; 95 | color: var(--mk-ui-text-primary); 96 | font-size: 13px; 97 | border-radius: var(--button-radius); 98 | border: 0; 99 | padding: 4px var(--size-4-3); 100 | height: var(--input-height); 101 | font-weight: var(--input-font-weight); 102 | cursor: var(--cursor); 103 | font-family: inherit; 104 | outline: none; 105 | user-select: none; 106 | white-space: nowrap; 107 | background-color: var(--mk-ui-active-normal); 108 | box-shadow: var(--input-shadow); 109 | } 110 | .mk-node-image-placeholder { 111 | width: 100%; 112 | height: 100%; 113 | background-color: var(--mk-ui-active-normal); 114 | align-items: center; 115 | display: flex; 116 | justify-content: center; 117 | } 118 | .mk-node-icon-placeholder { 119 | display: flex; 120 | color: var(--mk-ui-text-tertiary); 121 | } 122 | .mk-node-icon-placeholder svg { 123 | width: var(--icon-size); 124 | height: var(--icon-size); 125 | } 126 | .mk-frame-placeholder { 127 | display: flex; 128 | color: var(--mk-ui-text-tertiary); 129 | gap: 4px; 130 | padding: 0px; 131 | font-weight: 600; 132 | margin: 4px 0; 133 | } 134 | .mk-a { 135 | color: var(--link-color); 136 | outline: none; 137 | text-decoration-line: var(--link-decoration); 138 | text-decoration-thickness: var(--link-decoration-thickness); 139 | cursor: var(--cursor-link); 140 | } 141 | -------------------------------------------------------------------------------- /src/shared/types/metadata.ts: -------------------------------------------------------------------------------- 1 | import i18n from "shared/i18n" 2 | 3 | export type MetadataType = 'filemeta' | 'fileprop' | 'fm' 4 | export type Metadata = { 5 | id: string, 6 | field: string, 7 | vType: string, 8 | label: string, 9 | defaultFilter: string, 10 | type: string, 11 | description: string 12 | } 13 | 14 | 15 | 16 | export const labelProperties: Metadata[] = [ 17 | { 18 | id: 'label.sticker', 19 | label: i18n.metadataTypes.sticker, 20 | field: 'sticker', 21 | vType: 'text', 22 | defaultFilter: 'isEmpty', 23 | type: 'label', 24 | description: "Sticker label for the item" 25 | }, 26 | { 27 | id: 'label.color', 28 | label: i18n.metadataTypes.color, 29 | field: 'color', 30 | vType: 'color', 31 | defaultFilter: 'equals', 32 | type: 'label', 33 | description: "Color label for the item" 34 | }, 35 | ] 36 | 37 | export const fileProperties : Metadata[] = [ 38 | { 39 | id: 'file.name', 40 | label: i18n.metadataTypes.fileName, 41 | field: 'name', 42 | vType: 'text', 43 | defaultFilter: 'contains', 44 | type: 'file', 45 | description: "Name for a space or note" 46 | }, 47 | { 48 | id: 'file.path', 49 | label: i18n.metadataTypes.path, 50 | field: 'path', 51 | vType: 'link', 52 | defaultFilter: 'contains', 53 | type: 'file', 54 | description: "Path for a space or note" 55 | }, 56 | 57 | { 58 | id: 'file.parent', 59 | label: i18n.metadataTypes.folder, 60 | field: 'parent', 61 | vType: 'space', 62 | defaultFilter: 'contains', 63 | type: 'file', 64 | description: "Folder the note or space is in" 65 | }, 66 | 67 | { 68 | id: 'file.ctime', 69 | label: i18n.metadataTypes.created, 70 | field: 'ctime', 71 | vType: 'date', 72 | defaultFilter: 'isSameDate', 73 | type: 'file', 74 | description: "Creation date for the note or space" 75 | }, 76 | { 77 | id: 'file.mtime', 78 | label: i18n.metadataTypes.lastModified, 79 | field: 'mtime', 80 | vType: 'date', 81 | defaultFilter: 'isSameDate', 82 | type: 'file', 83 | description: "Last modified date for the note or space" 84 | }, 85 | { 86 | id: 'file.extension', 87 | label: i18n.metadataTypes.extension, 88 | field: 'extension', 89 | vType: 'text', 90 | defaultFilter: 'is', 91 | type: 'file', 92 | description: "File extension" 93 | }, 94 | { 95 | id: 'file.size', 96 | label: i18n.metadataTypes.size, 97 | field: 'size', 98 | vType: 'number', 99 | defaultFilter: 'lessThan', 100 | type: 'file', 101 | description: "File size" 102 | } 103 | ] 104 | export const pathCacheMetadata : Metadata[] = [ 105 | { 106 | id: 'path.tags', 107 | label: i18n.metadataTypes.tags, 108 | field: 'tags', 109 | vType: 'tags-multi', 110 | defaultFilter: 'contains', 111 | type: 'path', 112 | description: "Tags for the note or space" 113 | }, 114 | { 115 | id: 'path.inlinks', 116 | label: i18n.metadataTypes.inlinks, 117 | field: 'inlinks', 118 | vType: 'link-multi', 119 | defaultFilter: 'contains', 120 | type: 'path', 121 | description: "Links to the note or space" 122 | }, 123 | { 124 | id: 'path.outlinks', 125 | label: i18n.metadataTypes.outlinks, 126 | field: 'outlinks', 127 | vType: 'link-multi', 128 | defaultFilter: 'contains', 129 | type: 'path', 130 | description: "Links from the note or space" 131 | } 132 | ] 133 | 134 | -------------------------------------------------------------------------------- /src/shared/utils/makemd/inlineTable.ts: -------------------------------------------------------------------------------- 1 | import { defaultTableFields } from "shared/schemas/fields"; 2 | import { ISuperstate } from "shared/types/superstate"; 3 | import { uniqueNameFromString } from "shared/utils/array"; 4 | import { defaultPredicate } from "../../schemas/predicate"; 5 | 6 | 7 | export const predicateForViewType = (type: string) => { 8 | if (type == "table") { 9 | return ({ 10 | view: "table", 11 | listView: "", 12 | listGroup: "", 13 | listItem: "", 14 | }); 15 | } 16 | if (type == "flow") { 17 | return ({ 18 | view: "list", 19 | listView: "spaces://$kit/#*listView", 20 | listGroup: "spaces://$kit/#*listGroup", 21 | listItem: "spaces://$kit/#*flowListItem", 22 | }); 23 | } 24 | if (type == "list") { 25 | return ({ 26 | view: "list", 27 | listView: "spaces://$kit/#*listView", 28 | listGroup: "spaces://$kit/#*listGroup", 29 | listItem: "spaces://$kit/#*rowItem", 30 | }); 31 | } 32 | if (type == "details") { 33 | return ({ 34 | view: "list", 35 | listView: "spaces://$kit/#*listView", 36 | listGroup: "spaces://$kit/#*listGroup", 37 | listItem: "spaces://$kit/#*detailItem", 38 | }); 39 | } 40 | if (type == "board") { 41 | return ({ 42 | view: "list", 43 | listView: "spaces://$kit/#*columnView", 44 | listGroup: "spaces://$kit/#*columnGroup", 45 | listItem: "spaces://$kit/#*cardListItem", 46 | }); 47 | } 48 | if (type == "cards") { 49 | return ({ 50 | view: "list", 51 | listView: "spaces://$kit/#*listView", 52 | listGroup: "spaces://$kit/#*gridGroup", 53 | listItem: "spaces://$kit/#*cardsListItem", 54 | }); 55 | } 56 | if (type == "catalog") { 57 | return ({ 58 | view: "list", 59 | listView: "spaces://$kit/#*listView", 60 | listGroup: "spaces://$kit/#*rowGroup", 61 | listItem: "spaces://$kit/#*coverListItem", 62 | }); 63 | } 64 | if (type == "gallery") { 65 | return ({ 66 | view: "list", 67 | listView: "spaces://$kit/#*listView", 68 | listGroup: "spaces://$kit/#*masonryGroup", 69 | listItem: "spaces://$kit/#*imageListItem", 70 | }); 71 | } 72 | if (type == "calendar") { 73 | return ({ 74 | view: "list", 75 | listView: "spaces://$kit/#*calendarView", 76 | listGroup: "spaces://$kit/#*dateGroup", 77 | listItem: "spaces://$kit/#*eventItem", 78 | }); 79 | } 80 | } 81 | 82 | export const createInlineTable = async (superstate: ISuperstate, path: string, type?: string) => { 83 | 84 | 85 | let tableName = type == 'board' ? "Board" : "Table"; 86 | const schemas = await superstate.spaceManager.tablesForSpace(path) 87 | if (schemas) { 88 | tableName = uniqueNameFromString( 89 | tableName, 90 | schemas.map((f) => f.id) 91 | ) 92 | } 93 | const viewID = await superstate.spaceManager.createTable(path, { id: tableName, 94 | name: tableName, 95 | type: "db",}).then(f => 96 | superstate.spaceManager.addSpaceProperty(path, {...defaultTableFields[0], schemaId: tableName}) 97 | ).then(async (f) => 98 | { 99 | const schemaTable = await superstate.spaceManager.framesForSpace(path) 100 | const schema = { 101 | id: uniqueNameFromString( 102 | tableName, 103 | schemaTable?.map((f) => f.id) ?? [] 104 | ), 105 | name: tableName, 106 | type: "view", 107 | predicate: JSON.stringify({ 108 | ...defaultPredicate, 109 | ...(type ? predicateForViewType(type) : {view: "table"}), 110 | }), 111 | def: JSON.stringify({ 112 | db: tableName, 113 | icon: type == 'board' ? "ui//square-kanban" : "ui//table", 114 | }), 115 | } 116 | await superstate.spaceManager.createFrame(path, schema) 117 | return schema.id; 118 | } 119 | ) 120 | 121 | return viewID; 122 | }; -------------------------------------------------------------------------------- /src/basics/ui/UINote.tsx: -------------------------------------------------------------------------------- 1 | import MakeBasicsPlugin from "main"; 2 | import React, { forwardRef, useEffect, useRef, useState } from "react"; 3 | import i18n from "shared/i18n"; 4 | 5 | const removeLeadingSlash = (path: string) => 6 | path.charAt(0) == "/" ? path.substring(1) : path; 7 | 8 | const pathToString = (path: string) => { 9 | if (path.lastIndexOf("/") != -1) { 10 | if (path.lastIndexOf(".") != -1) 11 | return removeLeadingSlash( 12 | path.substring(path.lastIndexOf("/") + 1, path.lastIndexOf(".")) 13 | ); 14 | return path.substring(path.lastIndexOf("/") + 1); 15 | } 16 | if (path.lastIndexOf(".") != -1) { 17 | return path.substring(0, path.lastIndexOf(".")); 18 | } 19 | 20 | return path; 21 | }; 22 | export interface NoteViewProps { 23 | plugin: MakeBasicsPlugin; 24 | source?: string; 25 | path: string; 26 | load: boolean; 27 | properties?: Record; 28 | classname?: string; 29 | forceNote?: boolean; 30 | } 31 | 32 | export const UINote = forwardRef((props: NoteViewProps, ref) => { 33 | const flowRef = useRef(null); 34 | const [existsPas, setExistsPas] = useState(false); 35 | const [loaded, setLoaded] = useState(false); 36 | 37 | const loadPath = async (force?: boolean) => { 38 | const div = flowRef.current; 39 | 40 | const path = props.plugin.enactor.uriByString(props.path, props.source); 41 | 42 | const pathExists = await props.plugin.enactor.pathExists(path.basePath); 43 | const isFolder = props.plugin.enactor.isSpace(path.basePath); 44 | const filePath = 45 | isFolder && props.forceNote 46 | ? props.plugin.enactor.spaceNotePath(props.path) 47 | : pathExists 48 | ? path.fullPath 49 | : null; 50 | 51 | if (!filePath) { 52 | if (!force) { 53 | setExistsPas(true); 54 | setLoaded(false); 55 | return; 56 | } else { 57 | const parent = isFolder 58 | ? props.plugin.enactor.spaceFolderPath(props.path) 59 | : props.plugin.enactor.parentPath(path.basePath); 60 | if (!parent) return; 61 | const newPath = await props.plugin.enactor.createNote( 62 | parent, 63 | pathToString(props.path) 64 | ); 65 | setExistsPas(false); 66 | await props.plugin.enactor.openPath(newPath, div); 67 | } 68 | } else { 69 | setExistsPas(false); 70 | props.plugin.enactor.openPath(filePath, div); 71 | } 72 | 73 | setLoaded(true); 74 | }; 75 | 76 | const toggleFlow = () => { 77 | if (props.load) { 78 | loadPath(); 79 | } else { 80 | if (flowRef?.current) flowRef.current.innerHTML = ""; 81 | } 82 | }; 83 | 84 | useEffect(() => { 85 | toggleFlow(); 86 | }, [props.load, props.path]); 87 | 88 | useEffect(() => { 89 | const reloadFlow = () => { 90 | if ( 91 | flowRef.current && 92 | !flowRef.current.hasChildNodes() && 93 | props.load && 94 | !existsPas 95 | ) { 96 | loadPath(); 97 | } 98 | }; 99 | props.plugin.enactor.addActiveStateListener(reloadFlow); 100 | 101 | return () => { 102 | flowRef.current = null; 103 | props.plugin.enactor.removeActiveStateListener(reloadFlow); 104 | }; 105 | }, []); 106 | 107 | return ( 108 | <> 109 |
e.stopPropagation()} 113 | >
114 | 115 | {existsPas ? ( 116 |
loadPath(true)} 118 | className="mk-placeholder" 119 | style={{ color: "var(--mk-ui-text-tertiary)" }} 120 | > 121 | {i18n.labels.notePlaceholder.replace( 122 | "${1}", 123 | pathToString(props.path) 124 | )} 125 |
126 | ) : ( 127 | <> 128 | )} 129 | 130 | ); 131 | }); 132 | UINote.displayName = "UINote"; 133 | -------------------------------------------------------------------------------- /src/css/DefaultVibe.css: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | --mk-ui-divider: var(--divider-color); 4 | 5 | --mk-ui-border: var(--background-modifier-border); 6 | --mk-ui-border-accent: var(--divider-color-hover); 7 | 8 | --mk-ui-radius-small: var(--radius-s); 9 | --mk-ui-radius-medium: var(--radius-m); 10 | --mk-ui-radius-large: var(--radius-l); 11 | 12 | --mk-ui-background-overlay: rgba(0, 0, 0, 0.5); 13 | --mk-ui-border-overlay: rgba(15, 15, 15, 0.5); 14 | --mk-ui-background-blur: var(--mk-ui-background);/* rgba(var(--mono-rgb-0), 0.6);*/ 15 | --mk-ui-border-hover: var(--background-modifier-border-hover); 16 | 17 | --mk-ui-handle-color: rgba(70, 79, 200, 1); 18 | --mk-ui-handle-color-hover: #eb3b5a22; 19 | --mk-ui-handle-outline: rgba(255, 255, 255, 0.8); 20 | --mk-ui-handle-fill: #fff; 21 | --mk-ui-active: var(--interactive-accent); 22 | --mk-ui-active-hover: var(--interactive-hover); 23 | --mk-ui-active-normal: var(--interactive-normal); 24 | 25 | --mk-ui-background: var(--background-primary); 26 | --mk-ui-background-variant: var(--background-secondary); 27 | --mk-ui-background-contrast: var(--background-primary-alt); 28 | --mk-ui-background-active: var(--background-modifier-active); 29 | --mk-ui-background-selected: var(--background-modifier-active-hover); 30 | --mk-ui-background-reverse: var(--text-normal); 31 | --mk-ui-background-hover: var(--background-modifier-hover); 32 | --mk-ui-background-menu: var(--background-secondary); 33 | --mk-ui-background-menu-input: var(--background-modifier-form-field); 34 | --mk-ui-background-menu-hover: var(--background-modifier-hover); 35 | --mk-ui-background-input: var(--background-modifier-form-field); 36 | --mk-ui-text-primary: var(--text-normal); 37 | --mk-ui-text-secondary: var(--text-muted); 38 | --mk-ui-text-tertiary: var(--text-faint); 39 | --mk-ui-text-accent: var(--text-on-accent); 40 | --mk-ui-text-reverse: var(--background-primary); 41 | --mk-ui-text-error: var(--mk-color-red); 42 | 43 | --mk-shadow-card: 0px 1px 4px 0px rgba(0, 0, 0, 0.4); 44 | --mk-shadow-menu: 0px 0px 32px 0px rgba(0, 0, 0, 0.2); 45 | 46 | --mk-color-none: rgba(var(--mono-rgb-100), 0.025); 47 | --mk-color-blue: #3867d6; 48 | --mk-color-green: #20bf6b; 49 | --mk-color-orange: #fa8231; 50 | --mk-color-purple: #8854d0; 51 | --mk-color-red: #eb3b5a; 52 | --mk-color-turquoise: #0fb9b1; 53 | --mk-color-yellow: #E5A216; 54 | --mk-color-charcoal: #4b6584; 55 | --mk-color-teal: #2d98da; 56 | --mk-color-pink: #E83289; 57 | --mk-color-brown: #68381E; 58 | --mk-color-gray: #8392A4; 59 | --mk-color-black: #000000; 60 | --mk-color-white: #ffffff; 61 | 62 | --mk-color-base-0: var(--color-base-00); 63 | --mk-color-base-10: var(--color-base-10); 64 | --mk-color-base-20: var(--color-base-20); 65 | --mk-color-base-30: var(--color-base-30); 66 | --mk-color-base-40: var(--color-base-40); 67 | --mk-color-base-50: var(--color-base-50); 68 | --mk-color-base-60: var(--color-base-60); 69 | --mk-color-base-70: var(--color-base-70); 70 | --mk-color-base-100: var(--color-base-100); 71 | 72 | --mk-color-selection: var(--mk-ui-background-selected); 73 | --mk-color-ui-accent: var(--text-accent); 74 | 75 | 76 | --mk-layer-editor-popover: 100; 77 | --mk-layer-editor-overlay: 100; 78 | 79 | } 80 | 81 | .mk-smooth-border { 82 | --b: 5px; /* thickness of the border */ 83 | --c: red; /* color of the border */ 84 | --w: 20px; /* width of border */ 85 | 86 | 87 | border: var(--b) solid #0000; /* space for the border */ 88 | --_g: #0000 90deg,var(--c) 0; 89 | --_p: var(--w) var(--w) border-box no-repeat; 90 | background: 91 | conic-gradient(from 90deg at top var(--b) left var(--b),var(--_g)) 0 0 / var(--_p), 92 | conic-gradient(from 180deg at top var(--b) right var(--b),var(--_g)) 100% 0 / var(--_p), 93 | conic-gradient(from 0deg at bottom var(--b) left var(--b),var(--_g)) 0 100% / var(--_p), 94 | conic-gradient(from -90deg at bottom var(--b) right var(--b),var(--_g)) 100% 100% / var(--_p); 95 | } -------------------------------------------------------------------------------- /src/basics/flow/FlowEditorHover.tsx: -------------------------------------------------------------------------------- 1 | import { EditorView } from "@codemirror/view"; 2 | import MakeBasicsPlugin from "main"; 3 | import { App } from "obsidian"; 4 | import React, { useMemo } from "react"; 5 | import { uiIconSet } from "shared/assets/icons"; 6 | import i18n from "shared/i18n"; 7 | import { SelectOption } from "shared/types/menu"; 8 | import { SpaceFragmentSchema } from "shared/types/spaceFragment"; 9 | 10 | export const FlowEditorHover = (props: { 11 | path: string; 12 | pos: { from: number; to: number }; 13 | plugin: MakeBasicsPlugin; 14 | source?: string; 15 | app: App; 16 | view: EditorView; 17 | toggle: boolean; 18 | toggleState: boolean; 19 | dom?: HTMLElement; 20 | }) => { 21 | const path = props.plugin.enactor.resolvePath(props.path, props.source); 22 | const [spaceFragment, setSpaceFragment] = 23 | React.useState(); 24 | useMemo( 25 | () => 26 | props.plugin.enactor 27 | .spaceFragmentSchema(path) 28 | .then((f) => setSpaceFragment(f)), 29 | [path] 30 | ); 31 | 32 | const cutTable = () => { 33 | navigator.clipboard.writeText(`![![${props.path}]]`); 34 | props.view.dispatch({ 35 | changes: { from: props.pos.from - 4, to: props.pos.to + 2 }, 36 | }); 37 | }; 38 | const deleteTable = () => { 39 | props.view.dispatch({ 40 | changes: { from: props.pos.from - 4, to: props.pos.to + 2 }, 41 | }); 42 | props.plugin.enactor.notify(i18n.notice.tableDeleted); 43 | }; 44 | const toggleFlow = () => { 45 | const domPos = props.view.posAtDOM(props.dom); 46 | const line = props.view.state.doc.lineAt(domPos); 47 | const pos = line.from; 48 | if (props.toggleState) { 49 | props.view.dispatch({ 50 | changes: { from: pos, to: pos + 1 }, 51 | }); 52 | } else { 53 | props.view.dispatch({ 54 | changes: { 55 | from: pos, 56 | to: pos, 57 | insert: "!", 58 | }, 59 | }); 60 | } 61 | }; 62 | 63 | const showTableMenu = (e: React.MouseEvent) => { 64 | const menuOptions: SelectOption[] = []; 65 | menuOptions.push({ 66 | name: i18n.buttons.convertTable, 67 | icon: "ui//sync", 68 | onClick: (e) => { 69 | props.plugin.enactor.convertSpaceFragmentToMarkdown( 70 | spaceFragment, 71 | (markdown) => { 72 | props.view.dispatch({ 73 | changes: { 74 | from: props.pos.from - 4, 75 | to: props.pos.to + 2, 76 | insert: markdown, 77 | }, 78 | }); 79 | } 80 | ); 81 | }, 82 | }); 83 | menuOptions.push({ 84 | name: i18n.buttons.cutTable, 85 | icon: "ui//cut", 86 | onClick: (e) => { 87 | cutTable(); 88 | }, 89 | }); 90 | menuOptions.push({ 91 | name: i18n.buttons.deleteTable, 92 | icon: "ui//close", 93 | onClick: (e) => { 94 | deleteTable(); 95 | }, 96 | }); 97 | 98 | props.plugin.enactor.openMenu(e, menuOptions); 99 | }; 100 | 101 | return ( 102 |
103 | {!spaceFragment ? ( 104 | <> 105 | {props.toggle && ( 106 | 118 | )} 119 | 120 | ) : spaceFragment.type == "context" || 121 | spaceFragment.frameType == "view" ? ( 122 | 131 | ) : ( 132 | <> 133 | )} 134 |
135 | ); 136 | }; 137 | -------------------------------------------------------------------------------- /src/shared/utils/color.ts: -------------------------------------------------------------------------------- 1 | 2 | export function hexToRgb(hex: string) { 3 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 4 | return result ? { 5 | r: parseInt(result[1], 16), 6 | g: parseInt(result[2], 16), 7 | b: parseInt(result[3], 16) 8 | } : {r: 0, g: 0, b: 0} 9 | } 10 | 11 | export function hexToHsl(color: string) { 12 | const red = parseInt(color.slice(1, 3) ?? '0', 16) / 255; 13 | const green = parseInt(color.slice(3, 5) ?? '0', 16) / 255; 14 | const blue = parseInt(color.slice(5, 7) ?? '0', 16) / 255; 15 | 16 | const max = Math.max(red, green, blue); 17 | const min = Math.min(red, green, blue); 18 | 19 | const delta = max - min; 20 | 21 | let hue = 0; 22 | if (delta === 0) { 23 | hue = 0; 24 | } 25 | else if (max === red) { 26 | hue = 60 * (((green - blue) / delta) % 6); 27 | } 28 | else if (max === green) { 29 | hue = 60 * (((green - blue) / delta) + 2); 30 | } 31 | else if (max === blue) { 32 | hue = 60 * (((green - blue) / delta) + 4); 33 | } 34 | 35 | hue = Math.round(hue); 36 | if (hue < 0) { 37 | hue += 360; 38 | } 39 | const luminance = (max + min) / 2; 40 | const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * luminance - 1)); 41 | 42 | return {h: hue, s: saturation,l: luminance}; 43 | } 44 | 45 | 46 | function hslToHex(color: { h: number, s: number, l: number }) { 47 | const c = (1 - Math.abs(2 * color.l - 1)) * color.s; 48 | const x = c * (1 - Math.abs((color.h / 60) % 2 - 1)); 49 | const m = color.l - c / 2; 50 | 51 | let rgbValue: {r: number, g: number, b: number} = {r: 0, g: 0, b: 0}; 52 | if (color.h >= 0 && color.h < 60) { 53 | rgbValue = {r: c, g: x, b: 0}; 54 | } 55 | else if (color.h >= 60 && color.h < 120) { 56 | rgbValue = {r: x, g: c, b: 0}; 57 | } 58 | else if (color.h >= 120 && color.h < 180) { 59 | rgbValue = {r: 0, g: c, b: x}; 60 | } 61 | else if (color.h >= 180 && color.h < 240) { 62 | rgbValue = {r: 0, g: x, b: c}; 63 | } 64 | else if (color.h >= 240 && color.h < 300) { 65 | rgbValue = {r: x, g: 0, b: c}; 66 | } 67 | else if (color.h >= 300 && color.h < 360) { 68 | rgbValue = {r: c, g: 0, b: x}; 69 | } 70 | 71 | const red = Math.round((rgbValue.r + m) * 255); 72 | const green = Math.round((rgbValue.g + m) * 255); 73 | const blue = Math.round((rgbValue.b + m) * 255); 74 | 75 | return '#' + red.toString(16) + green.toString(16) + blue.toString(16); 76 | } 77 | 78 | export const colors = [ 79 | ["Red", "var(--mk-color-red)"], 80 | ["Pink", "var(--mk-color-pink)"], 81 | ["Orange", "var(--mk-color-orange)"], 82 | ["Yellow", "var(--mk-color-yellow)"], 83 | ["Green", "var(--mk-color-green)"], 84 | ["Turquoise", "var(--mk-color-turquoise)"], 85 | ["Teal", "var(--mk-color-teal)"], 86 | ["Blue", "var(--mk-color-blue)"], 87 | ["Purple", "var(--mk-color-purple)"], 88 | ["Brown", "var(--mk-color-brown)"], 89 | ["Charcoal", "var(--mk-color-charcoal)"], 90 | ["Gray", "var(--mk-color-gray)"], 91 | ]; 92 | 93 | export const colorsBase = [ 94 | ["Base 0", "var(--mk-color-base-0)"], 95 | ["Base 10", "var(--mk-color-base-10)"], 96 | ["Base 20", "var(--mk-color-base-20)"], 97 | ["Base 30", "var(--mk-color-base-30)"], 98 | ["Base 40", "var(--mk-color-base-40)"], 99 | ["Base 50", "var(--mk-color-base-50)"], 100 | ["Base 60", "var(--mk-color-base-60)"], 101 | ["Base 70", "var(--mk-color-base-70)"], 102 | ["Base 100", "var(--mk-color-base-100)"], 103 | ]; 104 | export const backgroundColors = [ 105 | ["Background", "var(--mk-ui-background)"], 106 | ["Background Variant", "var(--mk-ui-background-variant)"], 107 | ["Background Contrast", "var(--mk-ui-background-contrast)"], 108 | ["Background Active", "var(--mk-ui-background-active)"], 109 | ["Background Selected", "var(--mk-ui-background-selected)"], 110 | ] 111 | export const textColors = [ 112 | ["Text Primary", "var(--mk-ui-text-primary)"], 113 | ["Text Secondary", "var(--mk-ui-text-secondary)"], 114 | ["Text Tertiary", "var(--mk-ui-text-tertiary)"], 115 | ] 116 | 117 | 118 | export const shiftColor = (color: string, s: number, l: number) => { 119 | const hsl = hexToHsl(color); 120 | return hslToHex({ ...hsl, s: hsl.s +s, l: hsl.l +l}); 121 | } 122 | -------------------------------------------------------------------------------- /src/basics/flow/markdownPost.tsx: -------------------------------------------------------------------------------- 1 | import { EditorView } from "@codemirror/view"; 2 | import { FlowEditorHover } from "basics/flow/FlowEditorHover"; 3 | import { UINote } from "basics/ui/UINote"; 4 | import MakeBasicsPlugin from "main"; 5 | import { App, MarkdownPostProcessorContext } from "obsidian"; 6 | import React from "react"; 7 | 8 | const getCMFromElement = ( 9 | el: HTMLElement, 10 | app: App 11 | ): EditorView | undefined => { 12 | let dom: HTMLElement = el; 13 | while (!dom.hasClass("cm-editor") && dom.parentElement) { 14 | dom = dom.parentElement; 15 | } 16 | 17 | if (!dom.hasClass("cm-editor")) { 18 | return; 19 | } 20 | let rcm: EditorView; 21 | app.workspace.iterateLeaves((leaf) => { 22 | //@ts-ignore 23 | const cm = leaf.view.editor?.cm as EditorView; 24 | if (cm && dom == cm.dom) { 25 | rcm = cm; 26 | return true; 27 | } 28 | }, app.workspace["rootSplit"]!); 29 | return rcm; 30 | }; 31 | 32 | export const replaceAllTables = ( 33 | plugin: MakeBasicsPlugin, 34 | el: HTMLElement, 35 | ctx: MarkdownPostProcessorContext 36 | ) => { 37 | el.querySelectorAll("p").forEach((element) => { 38 | for (const match of element.textContent.matchAll( 39 | /(?:!\[!\[|!!\[\[)([^\]]+)\]\]/g 40 | )) { 41 | const link = match[1]; 42 | element.style.display = "none"; 43 | const reactEl = plugin.enactor.createRoot(element.parentElement); 44 | // const flowType = cm.state.field(flowTypeStateField, false); 45 | reactEl.render( 46 | 52 | ); 53 | } 54 | }); 55 | }; 56 | export const replaceMarkdownForEmbeds = ( 57 | el: HTMLElement, 58 | callback: (dom: HTMLElement) => void 59 | ) => { 60 | let dom: HTMLElement = el; 61 | setTimeout(async () => { 62 | //wait for el to be attached to the displayed document 63 | let counter = 0; 64 | while (!el.parentElement && counter++ <= 50) await sleep(50); 65 | if (!el.parentElement) return; 66 | while (!dom.hasClass("markdown-embed") && dom.parentElement) { 67 | dom = dom.parentElement; 68 | } 69 | if (dom) { 70 | callback(dom); 71 | } 72 | }); 73 | }; 74 | 75 | export const waitDOMInCM = ( 76 | el: HTMLElement, 77 | cm: EditorView, 78 | callback: (dom: HTMLElement) => void 79 | ) => { 80 | let dom: HTMLElement = el; 81 | setTimeout(async () => { 82 | //wait for el to be attached to the displayed document 83 | let counter = 0; 84 | while (!el.parentElement && counter++ <= 50) await sleep(50); 85 | if (!el.parentElement) return; 86 | 87 | while (!dom.hasClass("markdown-embed") && dom.parentElement) { 88 | dom = dom.parentElement; 89 | } 90 | if (dom) { 91 | callback(dom); 92 | } 93 | }); 94 | }; 95 | 96 | export const replaceAllEmbed = ( 97 | el: HTMLElement, 98 | ctx: MarkdownPostProcessorContext, 99 | plugin: MakeBasicsPlugin, 100 | app: App 101 | ) => { 102 | replaceMarkdownForEmbeds(el, async (dom) => { 103 | const nodes = dom.querySelectorAll(".markdown-embed-link"); 104 | 105 | for (let i = 0; i < nodes.length; i++) { 106 | if (nodes[i].parentNode === dom) { 107 | dom.removeChild(nodes[i]); 108 | const div = dom.createDiv("mk-floweditor-selector"); 109 | const reactEl = plugin.enactor.createRoot(div); 110 | const cm: EditorView = getCMFromElement(el, app); 111 | const pos = cm?.posAtDOM(dom); 112 | const index = [ 113 | ...Array.from(dom.parentElement?.childNodes ?? []), 114 | ].indexOf(dom); 115 | if (index == -1) return; 116 | const nextDom = dom.parentElement.childNodes[index]; 117 | const endPos = cm?.posAtDOM(nextDom); 118 | 119 | // const flowType = cm.state.field(flowTypeStateField, false); 120 | if (ctx.sourcePath) 121 | reactEl.render( 122 | 132 | ); 133 | } 134 | } 135 | }); 136 | }; 137 | -------------------------------------------------------------------------------- /src/shared/types/superstate.ts: -------------------------------------------------------------------------------- 1 | 2 | import { LocalCachePersister } from "shared/types/persister"; 3 | 4 | import { FrameExecutable } from "shared/types/frameExec"; 5 | import { Kit } from "shared/types/kits"; 6 | import { Metadata } from "shared/types/metadata"; 7 | import { MakeMDSettings } from "shared/types/settings"; 8 | import { EventDispatcher } from "shared/utils/dispatchers/dispatcher"; 9 | import { ICLIManager } from "./actions"; 10 | import { IAPI } from "./api"; 11 | import { Command } from "./commands"; 12 | import { Focus } from "./focus"; 13 | import { IndexMap } from "./indexMap"; 14 | import { FrameRoot, MDBFrames } from "./mframe"; 15 | import { ContextState, PathState, SpaceState, SuperstateEvent } from "./PathState"; 16 | import { SpaceDefGroup, SpaceDefinition } from "./spaceDef"; 17 | import { SpaceInfo } from "./spaceInfo"; 18 | import { SpaceManagerInterface } from "./spaceManager"; 19 | import { IUIManager } from "./uiManager"; 20 | 21 | 22 | 23 | export abstract class ISuperstate { 24 | formulaContext: any; 25 | initialized: boolean; 26 | eventsDispatcher: EventDispatcher; 27 | spaceManager: SpaceManagerInterface; 28 | settings: MakeMDSettings; 29 | onSpaceDefinitionChanged: (space: SpaceState, metadata?: SpaceDefinition) => Promise; 30 | saveSettings: () => void; 31 | api: IAPI; 32 | ui: IUIManager; 33 | cli: ICLIManager; 34 | pathsIndex: Map; 35 | spacesIndex: Map; 36 | contextsIndex: Map; 37 | actionsIndex: Map; 38 | kits: Map; 39 | actions: Map; 40 | selectedKit: Kit; 41 | kitFrames: Map; 42 | templateCache: Map; 43 | kit: FrameRoot[]; 44 | iconsCache: Map; 45 | imagesCache: Map; 46 | spacesDBLoaded: boolean; 47 | spacesMap: IndexMap; 48 | linksMap: IndexMap; 49 | tagsMap: IndexMap; 50 | liveSpaceLinkMap: IndexMap; 51 | allMetadata: Record; 52 | focuses: Focus[]; 53 | persister: LocalCachePersister; 54 | 55 | refreshMetadata: () => void; 56 | initializeIndex:() => Promise; 57 | addToContextStateQueue: (operation: () => Promise) => void; 58 | initialize:() => Promise; 59 | reloadSystemActions: () => Promise; 60 | initializePaths: () => Promise; 61 | initializeActions: () => Promise; 62 | initializeKits: () => Promise; 63 | initializeTemplates: () => Promise; 64 | initializeSpaces:() => Promise; 65 | getSpaceItems: (spacePath: string, filesOnly?: boolean)=> PathStateWithRank[]; 66 | loadFromCache: () => Promise; 67 | dispatchEvent:(event: keyof SuperstateEvent, payload: any)=> void; 68 | initializeBuiltins: () => Promise; 69 | initializeTags: () => Promise; 70 | onTagRenamed: (tag: string, newTag: string)=> Promise; 71 | onTagDeleted:(tag: string) => Promise; 72 | deleteTagInPath:(tag: string, path: string) => Promise; 73 | onMetadataChange:(path: string) => void; 74 | reloadSpaceByPath:(path: string, metadata?: SpaceDefinition) => Promise; 75 | onPathRename:(oldPath: string, newPath: string) => Promise; 76 | onPathCreated:(path: string) => Promise; 77 | onPathDeleted:(path: string) => void; 78 | onSpaceRenamed:(oldPath: string, newSpaceInfo: SpaceInfo) => Promise; 79 | onSpaceDeleted:(space: string) => void; 80 | reloadActions:(space: SpaceInfo) => Promise; 81 | reloadContextByPath:(path: string, force?: boolean) => Promise; 82 | reloadContext:(space: SpaceInfo, force?: boolean) => Promise; 83 | contextReloaded:(path: string, cache: ContextState, changed: boolean, force?: boolean) => Promise; 84 | allSpaces:(ordered?: boolean) => SpaceState[]; 85 | spaceOrder:() => string[]; 86 | updateSpaceMetadata:(spacePath: string, metadata: SpaceDefinition) => Promise; 87 | reloadSpace:(space: SpaceInfo, spaceMetadata?: SpaceDefinition, initialized?: boolean) => Promise; 88 | reloadPath:(path: string, force?: boolean) => Promise; 89 | onPathReloaded:(path: string) => Promise; 90 | search:(path: string, query?: string, queries?: SpaceDefGroup[]) => Promise; 91 | 92 | } 93 | 94 | export type PathStateWithRank = PathState & { rank?: number; }; 95 | 96 | -------------------------------------------------------------------------------- /src/basics/menus/MakeMenu/MakeMenu.tsx: -------------------------------------------------------------------------------- 1 | import MakeBasicsPlugin from "main"; 2 | import { 3 | App, 4 | Editor, 5 | EditorPosition, 6 | EditorSuggest, 7 | EditorSuggestContext, 8 | EditorSuggestTriggerInfo, 9 | TFile, 10 | } from "obsidian"; 11 | import { uiIconSet } from "shared/assets/icons"; 12 | import i18n from "shared/i18n"; 13 | import { Command, CommandType } from "../../types/command"; 14 | 15 | export default class MakeMenu extends EditorSuggest { 16 | inCmd = false; 17 | cmdStartCh = 0; 18 | plugin: MakeBasicsPlugin; 19 | file: TFile; 20 | 21 | constructor(app: App, plugin: MakeBasicsPlugin) { 22 | super(app); 23 | this.plugin = plugin; 24 | } 25 | resetInfos() { 26 | this.cmdStartCh = 0; 27 | this.inCmd = false; 28 | } 29 | 30 | onTrigger( 31 | cursor: EditorPosition, 32 | editor: Editor, 33 | _file: TFile 34 | ): EditorSuggestTriggerInfo { 35 | const currentLine = editor.getLine(cursor.line).slice(0, cursor.ch); 36 | const triggerCharLength = this.plugin.settings.menuTriggerChar.length; 37 | this.file = _file; 38 | if ( 39 | !this.inCmd && 40 | currentLine.slice(0, triggerCharLength) !== 41 | this.plugin.settings.menuTriggerChar && 42 | currentLine.slice(-2 - triggerCharLength) !== 43 | "- " + this.plugin.settings.menuTriggerChar 44 | ) { 45 | this.resetInfos(); 46 | return null; 47 | } 48 | 49 | if (!this.inCmd) { 50 | this.cmdStartCh = currentLine.length - triggerCharLength; 51 | this.inCmd = true; 52 | } 53 | 54 | const currentCmd = currentLine.slice(this.cmdStartCh, cursor.ch); 55 | 56 | if ( 57 | (currentCmd.length > 1 && currentCmd.includes(" ")) || 58 | !currentCmd.includes(this.plugin.settings.menuTriggerChar) 59 | ) { 60 | this.resetInfos(); 61 | return null; 62 | } 63 | return { 64 | start: cursor, 65 | end: cursor, 66 | query: currentCmd.slice(triggerCharLength), 67 | }; 68 | } 69 | 70 | getSuggestions( 71 | context: EditorSuggestContext 72 | ): Command[] | Promise { 73 | const suggestions = this.plugin.commands.filter( 74 | ({ label }) => 75 | label.toLowerCase().includes(context.query.toLowerCase()) || 76 | //@ts-ignore 77 | (i18n.commands[label] && 78 | //@ts-ignore 79 | i18n.commands[label] 80 | .toLowerCase() 81 | .includes(context.query.toLowerCase())) 82 | ); 83 | 84 | return suggestions.length > 0 85 | ? suggestions 86 | : [ 87 | { 88 | label: i18n.commandsSuggest.noResult, 89 | value: "", 90 | icon: "", 91 | type: CommandType.None, 92 | }, 93 | ]; 94 | } 95 | 96 | renderSuggestion(value: Command, el: HTMLElement): void { 97 | if (value.value == "") { 98 | el.setText(i18n.commandsSuggest.noResult); 99 | return; 100 | } 101 | const div = el.createDiv("mk-slash-item"); 102 | const icon = div.createDiv("mk-slash-icon"); 103 | icon.innerHTML = uiIconSet[value.icon]; 104 | const title = div.createDiv(); 105 | //@ts-ignore 106 | title.setText(i18n.commands[value.label] ?? value.label); 107 | } 108 | 109 | selectSuggestion(cmd: Command, _evt: MouseEvent | KeyboardEvent): void { 110 | const start = this.context.start; 111 | const end = this.context.end; 112 | const startCh = this.cmdStartCh; 113 | const editor = this.context.editor; 114 | if (cmd.label === i18n.commandsSuggest.noResult) return; 115 | if (cmd.onSelect) { 116 | cmd.onSelect( 117 | _evt, 118 | this.plugin, 119 | this.file, 120 | editor, 121 | start, 122 | startCh, 123 | end, 124 | () => { 125 | this.resetInfos(); 126 | this.close(); 127 | } 128 | ); 129 | } else { 130 | this.context.editor.replaceRange( 131 | cmd.value, 132 | { ...this.context.start, ch: this.cmdStartCh }, 133 | this.context.end 134 | ); 135 | if (cmd.offset) { 136 | this.context.editor.setSelection( 137 | { ...this.context.start, ch: this.cmdStartCh + cmd.offset[1] }, 138 | { 139 | ...this.context.end, 140 | ch: this.cmdStartCh + cmd.value.length + cmd.offset[0], 141 | } 142 | ); 143 | } 144 | this.resetInfos(); 145 | 146 | this.close(); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/shared/types/indexMap.ts: -------------------------------------------------------------------------------- 1 | //https://github.com/blacksmithgu/obsidian-dataview 2 | 3 | export class IndexMap { 4 | /** Maps key -> values for that key. */ 5 | map: Map>; 6 | /** Cached inverse map; maps value -> keys that reference that value. */ 7 | invMap: Map>; 8 | 9 | /** Create a new, empty index map. */ 10 | public constructor() { 11 | this.map = new Map(); 12 | this.invMap = new Map(); 13 | } 14 | 15 | /** Returns all values for the given key. */ 16 | public get(key: string): Set { 17 | const result = this.map.get(key); 18 | if (result) { 19 | return new Set(result); 20 | } else { 21 | return new Set(); 22 | } 23 | } 24 | 25 | /** Returns all keys that reference the given key. Mutating the returned set is not allowed. */ 26 | public getInverse(value: string): Readonly> { 27 | return this.invMap.get(value) || IndexMap.EMPTY_SET; 28 | } 29 | 30 | /** Sets the key to the given values; this will delete the old mapping for the key if one was present. */ 31 | public set(key: string, values: Set): this { 32 | if (!values.size) { 33 | // no need to store if no values 34 | this.delete(key); 35 | return this; 36 | } 37 | const oldValues = this.map.get(key); 38 | if (oldValues) { 39 | for (const value of oldValues) { 40 | // Only delete the ones we're not adding back 41 | if (!values.has(key)) this.invMap.get(value)?.delete(key); 42 | } 43 | } 44 | this.map.set(key, values); 45 | for (const value of values) { 46 | if (!this.invMap.has(value)) this.invMap.set(value, new Set([key])); 47 | else this.invMap.get(value)?.add(key); 48 | } 49 | return this; 50 | } 51 | 52 | /** Sets the key to the given values; this will delete the old mapping for the key if one was present. */ 53 | public setInverse(key: string, values: Set): this { 54 | if (!values.size) { 55 | // no need to store if no values 56 | this.deleteInverse(key); 57 | return this; 58 | } 59 | const oldValues = this.invMap.get(key); 60 | if (oldValues) { 61 | for (const value of oldValues) { 62 | // Only delete the ones we're not adding back 63 | if (!values.has(key)) this.map.get(value)?.delete(key); 64 | } 65 | } 66 | this.invMap.set(key, values); 67 | for (const value of values) { 68 | if (!this.map.has(value)) this.map.set(value, new Set([key])); 69 | else this.map.get(value)?.add(key); 70 | } 71 | return this; 72 | } 73 | 74 | /** Clears all values for the given key so they can be re-added. */ 75 | public delete(key: string): boolean { 76 | const oldValues = this.map.get(key); 77 | if (!oldValues) return false; 78 | 79 | this.map.delete(key); 80 | for (const value of oldValues) { 81 | this.invMap.get(value)?.delete(key); 82 | } 83 | 84 | return true; 85 | } 86 | 87 | public deleteInverse(key: string): boolean { 88 | const oldValues = this.invMap.get(key); 89 | if (!oldValues) return false; 90 | 91 | this.invMap.delete(key); 92 | for (const value of oldValues) { 93 | this.map.get(value)?.delete(key); 94 | } 95 | 96 | return true; 97 | } 98 | 99 | /** Rename all references to the given key to a new value. */ 100 | public rename(oldKey: string, newKey: string): boolean { 101 | const oldValues = this.map.get(oldKey); 102 | if (!oldValues) return false; 103 | 104 | this.delete(oldKey); 105 | this.set(newKey, oldValues); 106 | return true; 107 | } 108 | 109 | /** Rename all references to the given key to a new value. */ 110 | public renameInverse(oldKey: string, newKey: string): boolean { 111 | const oldValues = this.invMap.get(oldKey); 112 | if (!oldValues) return false; 113 | 114 | this.deleteInverse(oldKey); 115 | this.setInverse(newKey, oldValues); 116 | return true; 117 | } 118 | 119 | /** Clear the entire index. */ 120 | public clear() { 121 | this.map.clear(); 122 | this.invMap.clear(); 123 | } 124 | 125 | static EMPTY_SET: Readonly> = Object.freeze(new Set()); 126 | } 127 | 128 | -------------------------------------------------------------------------------- /src/basics/flow/patchWorkspaceForFlow.ts: -------------------------------------------------------------------------------- 1 | import { around } from "monkey-around"; 2 | import { EphemeralState, PaneType, ViewState, Workspace, WorkspaceContainer, WorkspaceItem, WorkspaceLeaf } from "obsidian"; 3 | 4 | import MakeBasicsPlugin from "main"; 5 | import { FlowEditor } from "shared/FlowEditor"; 6 | 7 | 8 | export const patchWorkspaceForFlow = (plugin: MakeBasicsPlugin) => { 9 | let layoutChanging = false; 10 | const uninstaller = around(Workspace.prototype, { 11 | 12 | 13 | changeLayout(old) { 14 | return async function (workspace: unknown) { 15 | layoutChanging = true; 16 | try { 17 | // Don't consider hover popovers part of the workspace while it's changing 18 | await old.call(this, workspace); 19 | } finally { 20 | layoutChanging = false; 21 | } 22 | }; 23 | }, 24 | 25 | getLeaf(old) { 26 | //Patch get leaf to always return root leaf if leaf is a flow block 27 | return function (newLeaf?: PaneType | boolean) { 28 | 29 | let leaf: WorkspaceLeaf = old.call(this, newLeaf); 30 | 31 | 32 | if (leaf.isFlowBlock) { 33 | const currentLeafId = leaf.id; 34 | let foundLeaf = false; 35 | plugin.app.workspace.iterateLeaves((_leaf) => { 36 | if (_leaf.flowEditors && !foundLeaf) { 37 | _leaf.flowEditors.forEach((f) => { 38 | f.leaves().forEach((l: any) => { 39 | if (l.id == currentLeafId) { 40 | foundLeaf = true; 41 | leaf = _leaf; 42 | return; 43 | } 44 | }); 45 | 46 | }); 47 | } 48 | return; 49 | }, plugin.app.workspace["rootSplit"]!); 50 | } 51 | 52 | return leaf; 53 | }; 54 | }, 55 | 56 | 57 | setActiveLeaf(old) { 58 | return function setActiveLeaf(leaf, params) { 59 | if (leaf.view.getViewType() == 'markdown') { 60 | this.activeEditor = leaf.view; 61 | if (leaf.view.file) { 62 | 63 | } 64 | } 65 | return old.call(this, leaf, params); 66 | }; 67 | }, 68 | 69 | getDropLocation(old) { 70 | return function getDropLocation(event: MouseEvent) { 71 | for (const popover of FlowEditor.activePopovers(plugin.app)) { 72 | const dropLoc = this.recursiveGetTarget(event, popover.rootSplit); 73 | if (dropLoc) { 74 | return dropLoc; 75 | } 76 | } 77 | return old.call(this, event); 78 | }; 79 | }, 80 | onDragLeaf(old) { 81 | return function (event: MouseEvent, leaf: WorkspaceLeaf) { 82 | const hoverPopover = FlowEditor.forLeaf(leaf); 83 | return old.call(this, event, leaf); 84 | }; 85 | }, 86 | }); 87 | plugin.register(uninstaller); 88 | }; 89 | export const patchWorkspaceLeafForFlow = (plugin: MakeBasicsPlugin) => { 90 | plugin.register( 91 | around(WorkspaceLeaf.prototype, { 92 | getRoot(old) { 93 | return function () { 94 | const top = old.call(this); 95 | return top.getRoot === this.getRoot ? top : top.getRoot(); 96 | }; 97 | }, 98 | setViewState(old) { 99 | return async function (viewState: ViewState, eState?: unknown) { 100 | const result = await old.call(this, viewState, eState); 101 | try { 102 | if (this.flowEditors) { 103 | for (const he of this.flowEditors) { 104 | he.hide(); 105 | } 106 | } 107 | this.flowEditors = []; 108 | } catch { } 109 | return result; 110 | }; 111 | }, 112 | setEphemeralState(old) { 113 | return function (state: EphemeralState) { 114 | old.call(this, state); 115 | if (state.focus && this.view?.getViewType() === "empty") { 116 | // Force empty (no-file) view to have focus so dialogs don't reset active pane 117 | this.view.contentEl.tabIndex = -1; 118 | this.view.contentEl.focus(); 119 | } 120 | }; 121 | }, 122 | }) 123 | ); 124 | plugin.register( 125 | around(WorkspaceItem.prototype, { 126 | getContainer(old) { 127 | return function () { 128 | if (!old) return; // 0.14.x doesn't have this 129 | if (!this.parentSplit || this instanceof WorkspaceContainer) 130 | return old.call(this); 131 | return this.parentSplit.getContainer(); 132 | }; 133 | }, 134 | }) 135 | ); 136 | }; 137 | -------------------------------------------------------------------------------- /src/basics/flow/flowEditor.ts: -------------------------------------------------------------------------------- 1 | import { EditorView } from "@codemirror/view"; 2 | 3 | 4 | import MakeBasicsPlugin from "main"; 5 | import { MarkdownView, WorkspaceLeaf } from "obsidian"; 6 | import i18n from "shared/i18n"; 7 | import { FlowEditorInfo, flowEditorInfo } from "../codemirror/flowEditor"; 8 | 9 | export const parseOutReferences = ( 10 | ostr: string 11 | ): [string, string | undefined] => { 12 | const str = ostr.split("|")[0]; 13 | const refIndex = str.lastIndexOf("#"); 14 | return refIndex > 0 15 | ? [str.substring(0, refIndex), str.substring(refIndex + 1)] 16 | : [str, undefined]; 17 | }; 18 | 19 | 20 | 21 | export const loadFlowEditorByDOM = ( 22 | plugin: MakeBasicsPlugin, 23 | el: HTMLElement, 24 | view: EditorView, 25 | id: string 26 | ) => { 27 | setTimeout(async () => { 28 | //wait for el to be attached to the displayed document 29 | let counter = 0; 30 | while (!el.parentElement && counter++ <= 50) await sleep(50); 31 | if (!el.parentElement) return; 32 | let dom: HTMLElement = el; 33 | while ( 34 | !dom.hasClass("mk-floweditor") && 35 | !dom.hasClass("workspace") && 36 | dom.parentElement 37 | ) { 38 | dom = dom.parentElement; 39 | } 40 | 41 | if ( 42 | !dom.hasClass("mk-floweditor") && 43 | !dom.hasClass("workspace") && 44 | !(dom.nodeName == "HTML") 45 | ) { 46 | return; 47 | } 48 | setTimeout(async () => { 49 | //wait for el to be attached to the displayed document 50 | let leafFound = false; 51 | if (plugin.app.workspace.activeEditor) { 52 | if (plugin.app.workspace.activeEditor?.editor?.cm.dom == view.dom) { 53 | leafFound = true; 54 | plugin.app.workspace.iterateLeaves((leaf) => { 55 | if (leaf.view.editor?.cm.dom == view.dom) 56 | { 57 | loadFlowEditorsForLeafForID(leaf, 58 | plugin.app.workspace.activeEditor.editor.cm, 59 | plugin.app.workspace.activeEditor.file?.path, 60 | plugin, 61 | id 62 | ); 63 | } 64 | }, plugin.app.workspace["rootSplit"]!); 65 | 66 | } 67 | } 68 | if (!leafFound) { 69 | plugin.app.workspace.iterateLeaves((leaf) => { 70 | const cm = leaf.view.editor?.cm as EditorView; 71 | if (cm && view.dom == cm.dom) { 72 | leafFound = true; 73 | loadFlowEditorsForLeafForID( 74 | leaf, 75 | cm, 76 | (leaf.view as MarkdownView).file?.path, 77 | plugin, 78 | id 79 | ); 80 | } 81 | }, plugin.app.workspace["rootSplit"]!); 82 | } 83 | }); 84 | }); 85 | }; 86 | export const loadFlowEditorsForLeafForID = ( 87 | leaf: WorkspaceLeaf, 88 | cm: EditorView, 89 | source: string, 90 | plugin: MakeBasicsPlugin, 91 | id: string 92 | ) => { 93 | 94 | const stateField = cm.state.field(flowEditorInfo, false); 95 | if (!stateField) return; 96 | const flowInfo = stateField.find((f) => f.id == id); 97 | if (flowInfo && flowInfo.expandedState == 2) { 98 | loadFlowEditor(leaf, cm, flowInfo, source, plugin); 99 | } 100 | }; 101 | 102 | const loadFlowEditor = async ( 103 | leaf: WorkspaceLeaf, 104 | cm: EditorView, 105 | flowEditorInfo: FlowEditorInfo, 106 | source: string, 107 | plugin: MakeBasicsPlugin 108 | ) => { 109 | const dom = cm.dom.querySelector( 110 | "#mk-flow-" + flowEditorInfo.id 111 | ) as HTMLElement; 112 | 113 | const path = plugin.enactor.uriByString(flowEditorInfo.link, source); 114 | const basePath = plugin.enactor.resolvePath(path.basePath, source); 115 | 116 | if (dom) { 117 | 118 | const spaceCache = plugin.enactor.isSpace(path.basePath); 119 | if (spaceCache) { 120 | 121 | if (!dom.hasAttribute("ready")) { 122 | // dom.empty(); 123 | dom.setAttribute("ready", ""); 124 | plugin.enactor.openPath(path.fullPath, dom); 125 | 126 | return; 127 | } 128 | } else { 129 | 130 | 131 | const file = plugin.app.vault.getAbstractFileByPath(path.basePath) ?? plugin.app.vault.getAbstractFileByPath(basePath); 132 | if (file) { 133 | if (!dom.hasAttribute("ready")) { 134 | // dom.empty(); 135 | dom.setAttribute("ready", ""); 136 | plugin.enactor.openPath(basePath, dom); 137 | 138 | } 139 | } else { 140 | 141 | dom.innerHTML = ""; 142 | const createDiv = dom.createDiv("file-embed"); 143 | createDiv.classList.add("mod-empty"); 144 | const createFile = async (e: MouseEvent) => { 145 | e.stopPropagation(); 146 | e.stopImmediatePropagation(); 147 | await plugin.enactor.createNote('/', basePath); 148 | loadFlowEditor(leaf, cm, flowEditorInfo, source, plugin); 149 | }; 150 | createDiv.setText(`"${basePath}" ` + i18n.labels.noFile); 151 | createDiv.addEventListener("click", createFile); 152 | } 153 | } 154 | } 155 | }; 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /src/shared/components/PathSticker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import StickerModal from "shared/components/StickerModal"; 3 | import { SelectOption } from "shared/types/menu"; 4 | import { ISuperstate as Superstate } from "shared/types/superstate"; 5 | import { removeIconsForPaths, savePathSticker } from "shared/utils/sticker"; 6 | import t from "../i18n"; 7 | import { PathState } from "../types/PathState"; 8 | import { windowFromDocument } from "../utils/dom"; 9 | import { parseStickerString } from "../utils/stickers"; 10 | export const PathStickerView = (props: { 11 | superstate: Superstate; 12 | pathState: PathState; 13 | editable?: boolean; 14 | }) => { 15 | const { pathState } = props; 16 | const sticker = pathState?.label?.sticker; 17 | const color = pathState?.label?.color; 18 | const triggerStickerContextMenu = (e: React.MouseEvent) => { 19 | if (!pathState) return; 20 | 21 | e.preventDefault(); 22 | e.stopPropagation(); 23 | const menuOptions: SelectOption[] = []; 24 | menuOptions.push({ 25 | name: t.buttons.changeIcon, 26 | icon: "ui//sticker", 27 | onClick: (e) => { 28 | props.superstate.ui.openPalette( 29 | 32 | savePathSticker(props.superstate, pathState?.path, emoji) 33 | } 34 | />, 35 | windowFromDocument(e.view.document) 36 | ); 37 | }, 38 | }); 39 | 40 | menuOptions.push({ 41 | name: t.buttons.removeIcon, 42 | icon: "ui//file-minus", 43 | onClick: () => { 44 | removeIconsForPaths(props.superstate, [pathState.path]); 45 | }, 46 | }); 47 | 48 | props.superstate.ui.openMenu( 49 | (e.target as HTMLElement).getBoundingClientRect(), 50 | { 51 | ui: props.superstate.ui, 52 | multi: false, 53 | value: [], 54 | editable: false, 55 | options: menuOptions, 56 | searchable: false, 57 | showAll: true, 58 | }, 59 | windowFromDocument(e.view.document) 60 | ); 61 | 62 | return false; 63 | }; 64 | const triggerStickerMenu = (e: React.MouseEvent) => { 65 | e.stopPropagation(); 66 | if (pathState?.type == "space") { 67 | props.superstate.ui.openPalette( 68 | 71 | savePathSticker(props.superstate, pathState.path, emoji) 72 | } 73 | />, 74 | windowFromDocument(e.view.document) 75 | ); 76 | 77 | return; 78 | } 79 | props.superstate.ui.openPalette( 80 | 83 | savePathSticker(props.superstate, pathState.path, emoji) 84 | } 85 | />, 86 | windowFromDocument(e.view.document) 87 | ); 88 | }; 89 | const [stickerType, stickerPath] = parseStickerString(sticker); 90 | return ( 91 |
94 | {stickerType == "image" ? ( 95 | 100 | ) : ( 101 | 119 | )} 120 |
121 | ); 122 | }; 123 | 124 | export const PathStickerContainer = (props: { 125 | superstate: Superstate; 126 | path: string; 127 | }) => { 128 | const [cache, setCache] = useState(null); 129 | const reloadCache = () => { 130 | setCache(props.superstate.pathsIndex.get(props.path)); 131 | }; 132 | const reloadIcon = (payload: { path: string }) => { 133 | if (payload.path == props.path) { 134 | reloadCache(); 135 | } 136 | }; 137 | 138 | useEffect(() => { 139 | reloadCache(); 140 | props.superstate.eventsDispatcher.addListener( 141 | "pathStateUpdated", 142 | reloadIcon 143 | ); 144 | 145 | return () => { 146 | props.superstate.eventsDispatcher.removeListener( 147 | "pathStateUpdated", 148 | reloadIcon 149 | ); 150 | }; 151 | }, [props.path]); 152 | 153 | return cache ? ( 154 | 159 | ) : ( 160 | <> 161 | ); 162 | }; 163 | -------------------------------------------------------------------------------- /src/types/obsidian.d.ts: -------------------------------------------------------------------------------- 1 | import { EditorView } from "@codemirror/view"; 2 | import { FlowEditor, FlowEditorParent } from "shared/FlowEditor"; 3 | 4 | declare module "obsidian" { 5 | export enum PopoverState { 6 | Shown, 7 | Hiding, 8 | Hidden, 9 | Showing 10 | } 11 | interface WorkspaceMobileDrawer { 12 | togglePinned(): void; 13 | } 14 | interface WorkspaceSidedock { 15 | togglePinned(): void; 16 | } 17 | interface Vault { 18 | getConfig(config: string): any 19 | on(name: 'raw', callback: (path: string) => any, ctx?: any): EventRef; 20 | config: { 21 | cssTheme: string; 22 | } 23 | } 24 | interface App { 25 | appId: string; 26 | plugins: any; 27 | dragManager: any; 28 | commands: { 29 | listCommands(): Command[]; 30 | findCommand(id: string): Command; 31 | removeCommand(id: string): void; 32 | executeCommandById(id: string): void; 33 | commands: Record; 34 | }; 35 | embedRegistry: { 36 | embedByExtension: Record; 37 | }; 38 | mobileToolbar: { 39 | containerEl: HTMLElement; 40 | }; 41 | hotkeyManager: { 42 | getHotkeys(id: string): Hotkey[]; 43 | getDefaultHotkeys(id: string): Hotkey[]; 44 | }; 45 | internalPlugins: { 46 | getPluginById(id: string): { instance: { options: { pinned: [] } } }; 47 | config: Record; 48 | plugins: any 49 | }; 50 | } 51 | 52 | interface FileManager { 53 | processFrontMatter: ( 54 | file: TFile, 55 | callback: (FrontMatterCache) => void 56 | ) => void; 57 | createNewMarkdownFile: (folder: TFolder, name: string) => Promise; 58 | } 59 | 60 | interface MetadataCache { 61 | getCachedFiles(): string[]; 62 | getTags(): Record; 63 | } 64 | 65 | class FileExplorerPlugin extends Plugin_2 { 66 | revealInFolder(this: any, ...args: any[]): any; 67 | } 68 | 69 | interface WorkspaceParent { 70 | insertChild(index: number, child: WorkspaceItem, resize?: boolean): void; 71 | replaceChild(index: number, child: WorkspaceItem, resize?: boolean): void; 72 | removeChild(leaf: WorkspaceLeaf, resize?: boolean): void; 73 | containerEl: HTMLElement; 74 | } 75 | 76 | interface EmptyView extends View { 77 | actionListEl: HTMLElement; 78 | emptyTitleEl: HTMLElement; 79 | } 80 | 81 | interface MousePos { 82 | x: number; 83 | y: number; 84 | } 85 | 86 | interface EphemeralState { 87 | focus?: boolean; 88 | subpath?: string; 89 | line?: number; 90 | startLoc?: Loc; 91 | endLoc?: Loc; 92 | scroll?: number; 93 | } 94 | interface WorkspaceMobileDrawer { 95 | currentTab: number; 96 | children: WorkspaceLeaf[]; 97 | } 98 | 99 | interface HoverPopover { 100 | parent: FlowEditorParent | null; 101 | targetEl: HTMLElement; 102 | hoverEl: HTMLElement; 103 | hide(): void; 104 | show(): void; 105 | shouldShowSelf(): boolean; 106 | timer: number; 107 | waitTime: number; 108 | shouldShow(): boolean; 109 | transition(): void; 110 | } 111 | interface MarkdownFileInfo { 112 | contentEl: HTMLElement; 113 | } 114 | interface Workspace { 115 | activeEditor: MarkdownFileInfo; 116 | recordHistory(leaf: WorkspaceLeaf, pushHistory: boolean): void; 117 | iterateLeaves( 118 | callback: (item: WorkspaceLeaf) => boolean | void, 119 | item: WorkspaceItem | WorkspaceItem[] 120 | ): boolean; 121 | iterateLeaves( 122 | item: WorkspaceItem | WorkspaceItem[], 123 | callback: (item: WorkspaceLeaf) => boolean | void 124 | ): boolean; 125 | getDropLocation(event: MouseEvent): { 126 | target: WorkspaceItem; 127 | sidedock: boolean; 128 | }; 129 | recursiveGetTarget( 130 | event: MouseEvent, 131 | parent: WorkspaceParent 132 | ): WorkspaceItem; 133 | recordMostRecentOpenedFile(file: TFile): void; 134 | onDragLeaf(event: MouseEvent, leaf: WorkspaceLeaf): void; 135 | onLayoutChange(): void; // tell Obsidian leaves have been added/removed/etc. 136 | floatingSplit: WorkspaceSplit; 137 | } 138 | interface WorkspaceSplit { 139 | children: any[]; 140 | } 141 | interface WorkspaceLeaf { 142 | id: string; 143 | containerEl: HTMLElement; 144 | tabHeaderInnerTitleEl: HTMLElement; 145 | tabHeaderInnerIconEl: HTMLElement; 146 | history: { 147 | backHistory: any[]; 148 | }; 149 | isFlowBlock?: boolean; 150 | flowEditors?: FlowEditor[]; 151 | } 152 | interface Editor { 153 | cm: EditorView; 154 | } 155 | interface WorkspaceTabs { 156 | children: WorkspaceLeaf[]; 157 | } 158 | interface View { 159 | headerEl: HTMLDivElement; 160 | editor?: Editor, 161 | setMode?: (unknown) => unknown, 162 | editMode?: unknown, 163 | file?: TAbstractFile, 164 | getMode?: () => unknown, 165 | } 166 | interface MenuItem { 167 | dom: HTMLElement; 168 | iconEl: HTMLElement 169 | } 170 | interface Menu { 171 | dom: HTMLElement; 172 | scope: Scope; 173 | } 174 | interface Scope { 175 | keys: KeymapEventHandler[]; 176 | } 177 | interface EditorSuggest { 178 | suggestEl: HTMLElement; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/shared/types/uiManager.ts: -------------------------------------------------------------------------------- 1 | import { Root, RootOptions } from "react-dom/client"; 2 | import { SelectMenuProps } from "shared/types/menu"; 3 | import { EventDispatcher } from "shared/utils/dispatchers/dispatcher"; 4 | import { InputManager } from "shared/utils/inputManager"; 5 | import { MenuObject } from "./menu"; 6 | import { TargetLocation } from "./path"; 7 | import { SpaceState } from "./PathState"; 8 | import { Anchors, Pos, Rect } from "./Pos"; 9 | import { ISuperstate } from "./superstate"; 10 | import { InteractionType, ScreenType, Sticker } from "./ui"; 11 | import { Warning } from "./Warning"; 12 | 13 | export interface IUIManager { 14 | inputManager: InputManager; 15 | superstate: ISuperstate; 16 | mainFrame: UIAdapter; 17 | resetFunctions: ((id: string) => void)[]; 18 | addResetFunction: (reset: (id: string) => void) => void; 19 | removeResetFunction: (reset: (id: string) => void) => void; 20 | resetSelection: (id: string) => void; 21 | eventsDispatch: EventDispatcher; 22 | defaultAdd: (space: SpaceState, win: Window, location?: TargetLocation) => void; 23 | quickOpen: ( 24 | mode?: number, 25 | offset?: Rect, 26 | win?: Window, 27 | onSelect?: (link: string) => void, source?: string) => void; 28 | availableViews: () => string[]; 29 | activeState: Record; 30 | setActiveState: (state: Record) => void; 31 | activePath: string; 32 | setActivePath: (path: string) => void; 33 | setActiveSelection: (path: string, content: any) => void; 34 | mainMenu: (el: HTMLElement, superstate: ISuperstate) => void; 35 | navigationHistory: () => string[]; 36 | allViews: () => ViewAdapter[]; 37 | viewsByPath: (path: string) => ViewAdapter[]; 38 | isEverViewOpen: () => boolean; 39 | adapters: UIAdapter[]; 40 | getWarnings: () => Warning[]; 41 | createRoot: (container: Element | DocumentFragment, options?: RootOptions) => Root; 42 | openMenu: (rect: Rect, menuProps: SelectMenuProps, win: Window, defaultAnchor?: Anchors, onHide?: () => void, force?: boolean) => MenuObject; 43 | openCustomMenu: (rect: Rect, fc: JSX.Element, props: any, win: Window, defaultAnchor?: Anchors, onHide?: () => void, className?: string, onSubmenu?: (openSubmenu: (offset: Rect, onHide: () => void) => MenuObject) => MenuObject) => MenuObject; 44 | notify: (content: string, destination?: string) => void; 45 | error: (error: any) => void; 46 | openPalette: (modal: JSX.Element, win: Window, className?: string) => MenuObject; 47 | openModal: (title: string, modal: JSX.Element, win: Window, className?: string, props?: any) => MenuObject; 48 | openPopover: (position: Pos, popover: JSX.Element) => void; 49 | openPath: (path: string, newLeaf?: TargetLocation, source?: any, props?: Record) => void; 50 | primaryInteractionType: () => InteractionType; 51 | getScreenType: () => ScreenType; 52 | getOS: () => string; 53 | getSticker: (icon: string) => string; 54 | getPlaceholderImage: (icon: string) => string; 55 | allStickers: () => Sticker[]; 56 | getUIPath: (path: string, thumbnail?: boolean) => string; 57 | dragStarted: (e: React.DragEvent, paths: string[]) => void; 58 | dragEnded: (e: React.DragEvent) => void; 59 | setDragLabel: (label: string) => void; 60 | } 61 | 62 | export interface UIAdapter { 63 | manager: IUIManager; 64 | availableViews: () => string[]; 65 | viewsByPath: (path: string) => ViewAdapter[]; 66 | createRoot: (container: Element | DocumentFragment, options?: RootOptions) => Root; 67 | openToast: (content: string) => void; 68 | openModal: (title: string, modal: JSX.Element, win: Window, className?: string, props?: any) => MenuObject; 69 | openPalette: (modal: JSX.Element, win: Window, className?: string) => MenuObject; 70 | openPath: (path: string, newLeaf: TargetLocation, source?: any, props?: Record) => void; 71 | openPopover: (position: Pos, popover: JSX.Element) => void; 72 | getScreenType: () => ScreenType; 73 | getOS: () => string; 74 | getWarnings: () => Warning[]; 75 | primaryInteractionType: () => InteractionType; 76 | getSticker: (icon: string) => string; 77 | allStickers: () => Sticker[]; 78 | getUIPath: (path: string, thumbnail?: boolean) => string; 79 | dragStarted: (e: React.DragEvent, paths: string[]) => void; 80 | dragEnded: (e: React.DragEvent) => void; 81 | setDragLabel: (label: string) => void; 82 | navigationHistory: () => string[]; 83 | mainMenu: (el: HTMLElement, superstate: ISuperstate) => void; 84 | quickOpen: ( 85 | mode?: number, 86 | offset?: Rect, 87 | win?: Window, 88 | onSelect?: (link: string) => void, source?: string) => void; 89 | isEverViewOpen: () => boolean; 90 | }export type UIManagerEventTypes = { 91 | 'activePathChanged': string; 92 | 'activeStateChanged': null; 93 | 'activeSelectionChanged': { path: string; content: string; }; 94 | 'windowReady': null; 95 | }; 96 | export abstract class ViewAdapter { 97 | path: string; 98 | openPath: (path: string) => void; 99 | parent: ViewAdapter; 100 | children: ViewAdapter[]; 101 | } 102 | 103 | -------------------------------------------------------------------------------- /src/shared/utils/uri.ts: -------------------------------------------------------------------------------- 1 | import { PathRefTypes, URI } from "shared/types/path"; 2 | import { removeTrailingSlashFromFolder } from "shared/utils/paths"; 3 | 4 | 5 | export const parseURI = (uri: string): URI => { 6 | const fullPath= uri; 7 | // const nt = uriByStr(uri, source); 8 | // return nt; 9 | // } 10 | // export const uriByStr = (uri: string, source?: string) => { 11 | 12 | let refTypeChar = ''; 13 | const parseQuery = (queryString: string) => { 14 | const query: { [key: string]: string } = {}; 15 | queryString.split('&').forEach(param => { 16 | const [key, value] = param.split('='); 17 | query[decodeURIComponent(key)] = decodeURIComponent(value); 18 | }); 19 | return query; 20 | }; 21 | 22 | const mapRefType = (refTypeChar: string, isSpace: boolean) => { 23 | if (isSpace) { 24 | if (refTypeChar === '^') return 'context'; 25 | if (refTypeChar === '*') return 'frame'; 26 | if (refTypeChar === ';') return 'action'; 27 | return null 28 | } 29 | if (refTypeChar === '^') return 'block'; 30 | return 'heading'; 31 | }; 32 | 33 | let space: string | null = null; 34 | let path: string | null = null; 35 | let alias: string | null = null; 36 | let reference: string | null = null; 37 | let refType: PathRefTypes= null; 38 | let query: { [key: string]: string } | null = null; 39 | let scheme: string | null = 'vault'; 40 | 41 | if (fullPath.indexOf('://') != -1) { 42 | scheme = uri.slice(0, uri.indexOf('://')) 43 | const spaceStr = uri.slice(uri.indexOf('://')+3); 44 | 45 | if (spaceStr.charAt(0) == "#" || spaceStr.charAt(0) == "$") { 46 | const endIndex = spaceStr.split('/')[0].lastIndexOf('#'); 47 | if (endIndex > 0) { 48 | space = removeTrailingSlashFromFolder(spaceStr.slice(0, endIndex)) 49 | uri = spaceStr.slice(endIndex) 50 | } else { 51 | space = spaceStr.split('/')[0]; 52 | uri = spaceStr.replace(space, '') 53 | if (uri.length > 0) { 54 | uri = uri.slice(1) 55 | } 56 | if (uri == '') { 57 | uri = '/' 58 | } 59 | } 60 | } else { 61 | const spaceParts = spaceStr.split('/'); 62 | space = spaceParts[0]; 63 | uri = (spaceParts.slice(1).join('/') || ''); // Convert the rest back to a relative URI 64 | } 65 | 66 | } 67 | 68 | 69 | const lastSlashIndex = uri.lastIndexOf('/'); 70 | const lastHashIndex = uri.lastIndexOf('#'); 71 | const lastPipeIndex = uri.lastIndexOf('|'); 72 | const queryIndex = uri.lastIndexOf('?'); 73 | let trailSlash = false; 74 | if (queryIndex !== -1) { 75 | query = parseQuery(uri.slice(queryIndex + 1)); 76 | uri = uri.slice(0, queryIndex); 77 | } 78 | 79 | if (lastHashIndex !== -1 && lastHashIndex > lastSlashIndex) { 80 | if (lastHashIndex == lastSlashIndex+1) { 81 | trailSlash = true 82 | } 83 | const refPart = uri.slice(lastHashIndex + 1); 84 | refType = mapRefType(refPart[0], trailSlash); 85 | if (refType || lastHashIndex != lastSlashIndex+1) { 86 | refTypeChar = refPart[0]; 87 | reference = refType ? refPart.slice(1) : refPart; 88 | uri = uri.slice(0, lastHashIndex); 89 | } 90 | } 91 | 92 | if (lastPipeIndex !== -1 && lastPipeIndex > lastSlashIndex) { 93 | alias = uri.slice(lastPipeIndex + 1); 94 | uri = uri.slice(0, lastPipeIndex); 95 | } 96 | if (uri.charAt(uri.length - 1) == '/') { 97 | trailSlash = true 98 | } 99 | path = uri 100 | return { 101 | basePath: removeTrailingSlashFromFolder(`${space ? `${scheme}://${space}/${path != '/' ? path : ''}` : path}`), 102 | 103 | authority: space, 104 | fullPath, 105 | scheme, 106 | path: removeTrailingSlashFromFolder(uri), 107 | alias: alias, 108 | ref: reference, 109 | refType: refType, 110 | refStr: refType ? refTypeChar+reference : reference, 111 | query: query, 112 | trailSlash 113 | }; 114 | } 115 | 116 | export const movePath = (path: string, newParent: string) : string => { 117 | const parts = path.split("/") 118 | const newPath = newParent + "/" + parts[parts.length - 1] 119 | 120 | return newPath 121 | } 122 | export const renamePathWithoutExtension = (path: string, newName: string): string => { 123 | const dir = path.substring(0, path.lastIndexOf("/")); 124 | return dir.length > 0 ? `${dir}/${newName}` : `${newName}`; 125 | } 126 | 127 | export const renamePathWithExtension = (path: string, newName: string): string => { 128 | const dir = path.substring(0, path.lastIndexOf("/")); 129 | const ext = path.lastIndexOf(".") != -1 ? path.substring(path.lastIndexOf(".")) : ""; 130 | return dir.length > 0 ? `${dir}/${newName}${ext}` : `${newName}${ext}`; 131 | } 132 | 133 | 134 | export const uriForFolder = (path: string) : URI => { 135 | return { 136 | basePath: path, 137 | fullPath: path, 138 | authority: null, 139 | path, 140 | scheme: 'vault', 141 | alias: null, 142 | ref: null, 143 | refStr: null, 144 | refType: null, 145 | query: null, 146 | trailSlash: true 147 | }; 148 | } 149 | 150 | 151 | -------------------------------------------------------------------------------- /src/css/Panels/Navigator/FileTree.css: -------------------------------------------------------------------------------- 1 | .mk-hide-tabs 2 | .mod-left-split 3 | .mod-top-left-space 4 | .workspace-tab-header-container-inner { 5 | visibility: hidden; 6 | } 7 | 8 | .is-phone .mod-root .workspace-tabs:not(.mod-visible) { 9 | display: flex !important; 10 | } 11 | 12 | .mk-mobile-sidepanel.is-mobile.mk-hide-ribbon:not(.mk-spaces-right) 13 | .workspace-drawer.mod-left 14 | .workspace-drawer-inner, .mk-mobile-sidepanel.is-mobile.mk-hide-ribbon.mk-spaces-right 15 | .workspace-drawer.mod-right 16 | .workspace-drawer-inner { 17 | padding-left: 0 !important; 18 | } 19 | 20 | .mk-hide-ribbon .workspace-ribbon { 21 | display: none; 22 | } 23 | 24 | .mk-hide-ribbon.is-hidden-frameless:not(.is-fullscreen) 25 | .workspace-tabs.mod-top-left-space 26 | .workspace-tab-header-container:before { 27 | width: calc(var(--frame-left-space) + var(--ribbon-width)); 28 | } 29 | 30 | .mk-hide-ribbon.is-hidden-frameless:not(.is-fullscreen) 31 | .workspace-tabs.mod-top-left-space 32 | .workspace-tab-header-container { 33 | padding-left: calc(var(--frame-left-space) + var(--ribbon-width)); 34 | } 35 | 36 | .mk-mobile-sidepanel.is-mobile.mk-hide-ribbon .workspace-drawer-ribbon { 37 | display: none; 38 | } 39 | 40 | .mk-mobile-sidepanel.is-mobile:not(.mk-spaces-right) 41 | .workspace-drawer.mod-left 42 | .workspace-drawer-inner 43 | .workspace-drawer-header, .mk-mobile-sidepanel.is-mobile.mk-spaces-right 44 | .workspace-drawer.mod-right 45 | .workspace-drawer-inner 46 | .workspace-drawer-header { 47 | padding-left: 0 !important; 48 | } 49 | .mk-mobile-sidepanel.is-tablet:not(.mk-spaces-right) 50 | .workspace-drawer.mod-left 51 | .workspace-drawer-inner 52 | .workspace-drawer-header, .mk-mobile-sidepanel.is-tablet.mk-spaces-right 53 | .workspace-drawer.mod-right 54 | .workspace-drawer-inner 55 | .workspace-drawer-header { 56 | padding-right: 0 !important; 57 | } 58 | 59 | .mk-mobile-sidepanel.is-phone:not(.mk-spaces-right) 60 | .workspace-drawer.mod-left 61 | .workspace-drawer-inner 62 | .workspace-drawer-header, .mk-mobile-sidepanel.is-phone.mk-spaces-right 63 | .workspace-drawer.mod-right 64 | .workspace-drawer-inner 65 | .workspace-drawer-header { 66 | padding-right: 0 !important; 67 | padding-top: 0; 68 | } 69 | 70 | .mk-mobile-sidepanel.is-mobile:not(.mk-spaces-right) 71 | .workspace-drawer.mod-left 72 | .workspace-drawer-active-tab-header, 73 | .mk-mobile-sidepanel.is-mobile.mk-spaces-right 74 | .workspace-drawer.mod-right 75 | .workspace-drawer-active-tab-header { 76 | display: none; 77 | } 78 | 79 | .mk-mobile-sidepanel.is-mobile 80 | .workspace-drawer.mod-left 81 | .workspace-drawer-inner 82 | .mod-settings, .mk-mobile-sidepanel.is-mobile.mk-spaces-right 83 | .workspace-drawer.mod-right 84 | .workspace-drawer-inner 85 | .mod-pin, .mk-mobile-sidepanel.is-mobile:not(.mk-spaces-right) 86 | .workspace-drawer.mod-left 87 | .workspace-drawer-inner 88 | .mod-pin { 89 | display: none; 90 | } 91 | 92 | .is-mobile .mk-sidebar button:not(.clickable-icon) { 93 | 94 | } 95 | 96 | .is-mobile .mk-sidebar .mk-path-icon button { 97 | font-size: 16px; 98 | margin: 0; 99 | height: 24px; 100 | width: 24px; 101 | } 102 | 103 | body.mk-mobile-sidepanel.is-mobile .sidebar-toggle-button { 104 | display: flex !important; 105 | } 106 | 107 | .mk-mobile-sidepanel.is-mobile:not(.mk-spaces-right) .workspace-drawer.mod-left .workspace-drawer-header-icon, .mk-mobile-sidepanel.is-mobile.mk-spaces-right .workspace-drawer.mod-right .workspace-drawer-header-icon { 108 | position: absolute; 109 | right: 20px; 110 | top: 12px; 111 | z-index: 100; 112 | } 113 | 114 | .mk-mobile-sidepanel.is-phone .workspace-drawer.mod-left .workspace-drawer-header-icon { 115 | top: 20px; 116 | } 117 | 118 | .mk-mobile-sidepanel.is-mobile .workspace-drawer.mod-left { 119 | border-top-right-radius: 0; 120 | border-bottom-right-radius: 0; 121 | } 122 | .mk-sidebar { 123 | display: flex; 124 | flex-direction: column; 125 | height: 100%; 126 | } 127 | 128 | .is-mobile .mk-sidebar { 129 | /* flex-direction: column-reverse; */ 130 | } 131 | 132 | .mk-path-tree-focus { 133 | display: flex; 134 | flex-direction: column; 135 | align-items: center; 136 | gap: 8px; 137 | justify-content: center; 138 | height: 100%; 139 | } 140 | .mk-path-tree-focus .mk-focuses-item { 141 | background: var(--mk-ui-background) 142 | 143 | } 144 | 145 | .mk-path-tree-focus input { 146 | width: 60%; 147 | padding: 8px; 148 | border: none; 149 | border-radius: 4px; 150 | text-align: center; 151 | } 152 | .mk-button-group { 153 | display: flex; 154 | gap: 8px; 155 | } 156 | 157 | .mk-path-tree-empty { 158 | display: flex; 159 | flex-direction: column; 160 | align-items: center; 161 | justify-content: center; 162 | height: 100%; 163 | padding: 12px; 164 | gap: 8px; 165 | } 166 | 167 | .mk-empty-state-description { 168 | font-size: 14px; 169 | color: var(--mk-ui-text-secondary); 170 | text-align: center; 171 | } 172 | 173 | .mk-empty-state-title { 174 | font-size: 16px; 175 | color: var(--mk-ui-text-primary); 176 | } 177 | 178 | .mk-path-tree { 179 | flex: 1; 180 | overflow: hidden; 181 | /* -webkit-overflow-scrolling: touch; */ 182 | } 183 | .mk-context-tree .tree-item { 184 | padding: 4px; 185 | } 186 | 187 | .mk-context-tree .tree-item-self { 188 | margin-left: var(--spacing); 189 | align-items: center; 190 | padding: 2px 2px; 191 | } 192 | 193 | .mk-context-tree .mk-tree-wrapper { 194 | padding: 0px !important; 195 | } 196 | 197 | 198 | -------------------------------------------------------------------------------- /src/shared/components/StickerModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useEffect, 4 | useMemo, 5 | useRef, 6 | useState, 7 | } from "react"; 8 | import { Sticker } from "shared/types/ui"; 9 | import { emojiFromString } from "shared/utils/stickers"; 10 | import i18n from "../i18n"; 11 | import { IUIManager as UIManager } from "../types/uiManager"; 12 | 13 | interface StickerModalProps { 14 | ui: UIManager; 15 | selectedSticker: (path: string) => void; 16 | hide?: () => void; 17 | } 18 | 19 | const StickerModal: React.FC = (props) => { 20 | const [query, setQuery] = useState(""); 21 | const [allStickers, setAllStickers] = useState([]); 22 | const [stickers, setStickers] = useState([]); 23 | const [selectedSticker, setSelectedSticker] = useState(null); 24 | 25 | const htmlFromSticker = (sticker: Sticker) => { 26 | if (sticker.type == "emoji") { 27 | return emojiFromString(sticker.html); 28 | } 29 | return sticker.html; 30 | }; 31 | 32 | useEffect(() => { 33 | const _allImages: Sticker[] = []; 34 | _allImages.push(...props.ui.allStickers()); 35 | setAllStickers(_allImages); 36 | }, []); 37 | 38 | const categories = useMemo( 39 | () => new Set(allStickers.map((f) => f.type)), 40 | [allStickers] 41 | ); 42 | 43 | const [page, setPage] = useState(1); 44 | 45 | const loadNextPage = useCallback(() => { 46 | setPage((p) => p + 1); 47 | }, [page]); 48 | const loaderRef = useRef(null); 49 | useEffect(() => { 50 | const observer = new IntersectionObserver((entries) => { 51 | const target = entries[0]; 52 | if (target.isIntersecting) { 53 | loadNextPage(); 54 | } 55 | }); 56 | 57 | if (loaderRef.current) { 58 | observer.observe(loaderRef.current); 59 | } 60 | 61 | return () => { 62 | if (loaderRef.current) { 63 | observer.unobserve(loaderRef.current); 64 | } 65 | }; 66 | }, [loadNextPage]); 67 | const [selectedCategory, setSelectedCategory] = useState(null); 68 | useEffect(() => { 69 | setStickers( 70 | allStickers 71 | .filter( 72 | (f) => 73 | f.name.includes(query) && 74 | (selectedCategory == null || f.type == selectedCategory) 75 | ) 76 | .slice(0, page * 250) 77 | ); 78 | }, [query, allStickers, page, selectedCategory]); 79 | 80 | const handleInputChange = (event: React.ChangeEvent) => { 81 | setQuery(event.target.value); 82 | }; 83 | 84 | const handleKeyDown = (event: React.KeyboardEvent) => { 85 | const currentIndex = selectedSticker; 86 | 87 | if (event.key === "ArrowUp" && currentIndex > 0) { 88 | setSelectedSticker(currentIndex - 1); 89 | } else if ( 90 | event.key === "ArrowDown" && 91 | currentIndex < stickers.length - 1 92 | ) { 93 | setSelectedSticker(currentIndex + 1); 94 | } else if (event.key === "Enter") { 95 | props.selectedSticker( 96 | stickers[selectedSticker].type + "//" + stickers[selectedSticker].value 97 | ); 98 | props.hide(); 99 | } 100 | }; 101 | 102 | const ref = useRef(null); 103 | useEffect(() => { 104 | if (ref.current) { 105 | ref.current.focus(); 106 | } 107 | }, [ref.current]); 108 | return ( 109 | <> 110 |
111 |
117 | 124 | 125 |
126 |
127 |
setSelectedCategory(null)} 129 | className={`${ 130 | selectedCategory == null ? "is-active" : "" 131 | } mk-menu-section`} 132 | > 133 | All 134 |
135 | 136 | {[...categories].map((f) => ( 137 |
setSelectedCategory(f)} 140 | className={`${ 141 | selectedCategory == f ? "is-active" : "" 142 | } mk-menu-section`} 143 | > 144 | {f} 145 |
146 | ))} 147 |
148 |
149 | {stickers.map((icon, i) => ( 150 |
{ 153 | props.selectedSticker( 154 | stickers[i].type + "//" + stickers[i].value 155 | ); 156 | props.hide(); 157 | }} 158 | className={ 159 | selectedSticker === i 160 | ? "selected mk-padding-4 mk-border-radius-4 mk-hover" 161 | : "mk-padding-4 mk-border-radius-4 mk-hover" 162 | } 163 | > 164 |
168 |
169 | ))} 170 |
171 |
172 | 173 | ); 174 | }; 175 | 176 | export default StickerModal; 177 | -------------------------------------------------------------------------------- /src/css/Modal/Modal.css: -------------------------------------------------------------------------------- 1 | @keyframes slideInFromBottom { 2 | 3 | 0% { 4 | transform-origin: top 50%; 5 | transform: translateY(50px); 6 | opacity: 0.5 7 | } 8 | 100% { 9 | transform: translateY(0); 10 | opacity: 1 11 | } 12 | } 13 | 14 | @keyframes slideInFromTop { 15 | 16 | 0% { 17 | transform-origin: top 50%; 18 | transform: translateY(-100%); 19 | opacity: 0.5 20 | } 21 | 100% { 22 | transform: translateY(0); 23 | opacity: 1 24 | } 25 | } 26 | 27 | .mk-modal-actions { 28 | display: flex; 29 | justify-content: flex-end; 30 | gap: 8px; 31 | margin-top: 16px; 32 | } 33 | 34 | .mk-input { 35 | background-color: var(--mk-ui-background); 36 | border: none; 37 | border-radius: 0; 38 | } 39 | 40 | .mk-border-bottom { 41 | border-bottom: 1px solid var(--mk-ui-divider); 42 | } 43 | 44 | .mk-input-large[data-placeholder]:empty::before { 45 | content: attr(data-placeholder); 46 | color: var(--mk-ui-text-tertiary); 47 | } 48 | .mk-input-large { 49 | padding: 16px 20px; 50 | font-size: 14px; 51 | } 52 | .mk-modal-header { 53 | display: flex; 54 | justify-content: space-between; 55 | } 56 | .mk-modal-title { 57 | font-size: var(--font-ui-large); 58 | margin-bottom: 0.75em; 59 | font-weight: var(--font-semibold); 60 | text-align: left; 61 | line-height: 1.3; 62 | } 63 | .mk-modal:before, .mk-palette:before { 64 | content: ''; 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | right: 0; 69 | bottom: 0; 70 | background: inherit; 71 | backdrop-filter: blur(10px) saturate(2); 72 | z-index: -1; 73 | } 74 | .mk-palette.mk-ready, .mk-modal.mk-ready { 75 | 76 | 77 | } 78 | .mk-palette, .mk-modal { 79 | display: flex; 80 | flex-direction: column; 81 | border-radius: 12px; 82 | background-color: var(--mk-ui-background-blur); 83 | box-shadow: var(--shadow-l); 84 | border: thin solid var(--mk-ui-divider); 85 | z-index: 1; 86 | 87 | } 88 | 89 | .mk-palette { 90 | top: 80px; 91 | width: 700px; 92 | max-width: 80vw; 93 | height: 70vh; 94 | overflow: hidden; 95 | position: absolute; 96 | } 97 | .mk-modal { 98 | padding: var(--size-4-4); 99 | position: relative; 100 | min-height: 100px; 101 | width: var(--dialog-width); 102 | max-width: var(--dialog-max-width); 103 | max-height: var(--dialog-max-height); 104 | overflow: auto; 105 | 106 | } 107 | .is-phone .mk-palette, .is-phone .mk-modal { 108 | bottom: 0; 109 | max-width: 100%; 110 | width: 100%; 111 | max-height: 100%; 112 | left: 0; 113 | --mobile-height: 100vh; 114 | --prompt-bottom: 0px; 115 | --prompt-top: calc(var(--safe-area-inset-top) + var(--header-height) + var(--size-4-2)); 116 | border-radius: var(--radius-l) var(--radius-l) 0 0; 117 | min-width: unset; 118 | margin-bottom: var(--prompt-bottom); 119 | margin-top: var(--prompt-top); 120 | box-shadow: none; 121 | top: 0; 122 | height: calc(var(--mobile-height) - var(--prompt-top) - var(--prompt-bottom)); 123 | } 124 | .is-phone .mk-palette-search { 125 | background: var(--mk-ui-background-input); 126 | border-radius: 8px; 127 | border-bottom: none; 128 | padding: 8px; 129 | margin: 8px; 130 | display: flex; 131 | } 132 | .mk-palette-search { 133 | display:flex; 134 | gap: 8px; 135 | padding: 12px; 136 | } 137 | .mk-palette-search input { 138 | border:none; 139 | background: none; 140 | } 141 | 142 | .mk-modal-wrapper { 143 | padding: 0; 144 | -webkit-app-region: no-drag; 145 | background-color: var(--mk-ui-background-overlay); 146 | position: fixed; 147 | z-index: var(--layer-menu); 148 | user-select: none; 149 | overflow: hidden; 150 | width: 100%; 151 | height: 100%; 152 | top: 0; 153 | left: 0; 154 | 155 | } 156 | .mk-modal-container { 157 | width: 100%; 158 | height: 100%; 159 | display: flex; 160 | align-items: center; 161 | justify-content: center; 162 | } 163 | 164 | .mk-modal-message { 165 | text-overflow: ellipsis; 166 | width: 100%; 167 | overflow: hidden; 168 | white-space: pre-wrap; 169 | } 170 | 171 | .mk-modal-contents { 172 | display: flex; 173 | flex-direction: column; 174 | gap: 16px; 175 | width: 100%; 176 | } 177 | 178 | .mk-modal-card { 179 | display: flex; 180 | flex-direction: column; 181 | gap: 8px; 182 | padding: 12px; 183 | border-radius: 8px; 184 | background: var(--mk-ui-background); 185 | box-shadow: var(--mk-shadow-card); 186 | } 187 | 188 | 189 | .mk-modal-description { 190 | font-size: var(--font-ui-small); 191 | color: var(--mk-ui-text-secondary); 192 | } 193 | 194 | .mk-modal-items { 195 | display: flex; 196 | flex-direction: column; 197 | gap: 8px; 198 | } 199 | 200 | .mk-modal-item { 201 | display: flex; 202 | gap: 8px; 203 | } 204 | 205 | .mk-tab-group { 206 | display: flex; 207 | gap: 4px; 208 | margin-bottom: 8px; 209 | } 210 | 211 | .mk-tab { 212 | padding: 4px 12px; 213 | border-radius: 8px; 214 | display: flex; 215 | --icon-size: 20px; 216 | border: thin solid transparent; 217 | } 218 | 219 | .mk-tab svg { 220 | width: var(--icon-size); 221 | height: var(--icon-size); 222 | } 223 | .mk-tab:hover { 224 | background: var(--mk-ui-background-hover) 225 | } 226 | 227 | .mk-tab.mk-active { 228 | border: thin solid var(--mk-ui-border); 229 | background: var(--mk-ui-background-selected); 230 | color: var(--mk-color-ui-accent); 231 | 232 | } -------------------------------------------------------------------------------- /src/basics/makemods/replaceMobileMainMenu.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | 3 | import { Platform } from "obsidian"; 4 | import React, { useEffect, useRef } from "react"; 5 | import { uiIconSet } from "shared/assets/icons"; 6 | import i18n from "shared/i18n"; 7 | 8 | import MakeBasicsPlugin from "main"; 9 | import { ISuperstate } from "shared/types/superstate"; 10 | import { Warning } from "shared/types/Warning"; 11 | import { windowFromDocument } from "shared/utils/dom"; 12 | 13 | export const replaceMobileMainMenu = ( 14 | plugin: MakeBasicsPlugin, 15 | superstate: ISuperstate 16 | ) => { 17 | if (plugin.isTouchScreen()) { 18 | const header = plugin.app.workspace.containerEl.querySelector( 19 | superstate.settings.spacesRightSplit 20 | ? ".workspace-drawer.mod-right .workspace-drawer-header-left" 21 | : ".workspace-drawer.mod-left .workspace-drawer-header-left" 22 | ); 23 | header.innerHTML = ""; 24 | const reactEl = plugin.enactor.createRoot(header); 25 | reactEl.render( 26 | 30 | ); 31 | } 32 | }; 33 | 34 | export const ObsidianMobileMainMenu = (props: { 35 | superstate: ISuperstate; 36 | plugin: MakeBasicsPlugin; 37 | }) => { 38 | const ref = useRef(); 39 | const [warnings, setWarnings] = React.useState([]); 40 | useEffect(() => { 41 | setTimeout(() => { 42 | props.superstate.ui 43 | .getWarnings() 44 | .filter( 45 | (f) => 46 | !props.superstate.settings.suppressedWarnings.some((g) => f.id == g) 47 | ); 48 | }, 1000); 49 | }, []); 50 | 51 | const settingsChanged = () => { 52 | setWarnings( 53 | props.superstate.ui 54 | .getWarnings() 55 | .filter( 56 | (f) => 57 | !props.superstate.settings.suppressedWarnings.some((g) => f.id == g) 58 | ) 59 | ); 60 | }; 61 | useEffect(() => { 62 | props.superstate.eventsDispatcher.addListener( 63 | "superstateUpdated", 64 | settingsChanged 65 | ); 66 | props.superstate.eventsDispatcher.addListener( 67 | "settingsChanged", 68 | settingsChanged 69 | ); 70 | props.superstate.eventsDispatcher.addListener( 71 | "warningsChanged", 72 | settingsChanged 73 | ); 74 | return () => { 75 | props.superstate.eventsDispatcher.removeListener( 76 | "superstateUpdated", 77 | settingsChanged 78 | ); 79 | props.superstate.eventsDispatcher.removeListener( 80 | "settingsChanged", 81 | settingsChanged 82 | ); 83 | props.superstate.eventsDispatcher.removeListener( 84 | "warningsChanged", 85 | settingsChanged 86 | ); 87 | }; 88 | }, []); 89 | return ( 90 |
91 |
92 |
93 |
{ 97 | props.superstate.ui.mainMenu(ref.current, props.superstate); 98 | }} 99 | > 100 | {props.superstate.settings.systemName} 101 | {warnings.length > 0 && ( 102 |
108 | )} 109 |
115 |
116 | 117 | {props.superstate.settings.blinkEnabled && ( 118 |
props.superstate.ui.quickOpen()} 121 | > 122 |
128 |
129 | )} 130 |
131 | 132 | 150 | {Platform.isTablet && ( 151 |
155 | props.superstate.settings.spacesRightSplit 156 | ? props.plugin.app.workspace.rightSplit.togglePinned() 157 | : props.plugin.app.workspace.leftSplit.togglePinned() 158 | } 159 | > 160 |
166 |
167 | )} 168 |
169 |
170 | ); 171 | }; 172 | --------------------------------------------------------------------------------