feat: 编辑模式也能预览sheet, 文件格式变为 .sheet.md

This commit is contained in:
leijun 2023-09-06 14:45:48 +08:00
parent b5d9f413fd
commit f166222309
14 changed files with 1047 additions and 1660 deletions

View file

@ -16,7 +16,7 @@ const context = await esbuild.context({
banner: { banner: {
js: banner, js: banner,
}, },
entryPoints: ["main.ts"], entryPoints: ["src/main.ts"],
bundle: true, bundle: true,
external: [ external: [
"obsidian", "obsidian",

130
main.ts
View file

@ -1,130 +0,0 @@
import {
TFile,
Plugin,
WorkspaceLeaf,
normalizePath,
} from "obsidian";
import { ExcelSettings, DEFAULT_SETTINGS } from "./src/utils/Settings";
import {
PaneTarget,
} from "./src/utils/ModifierkeyHelper";
import { ExcelView, VIEW_TYPE_EXCEL } from "./src/ExcelView";
import {
getExcelFilename,
} from "./src/utils/FileUtils";
import {
initializeMarkdownPostProcessor,
markdownPostProcessor,
} from "./src/MarkdownPostProcessor";
export default class ExcelPlugin extends Plugin {
public settings: ExcelSettings;
async onload() {
this.registerView(
VIEW_TYPE_EXCEL,
(leaf: WorkspaceLeaf) => new ExcelView(leaf, this)
);
this.registerExtensions(["sheet"], VIEW_TYPE_EXCEL);
// This creates an icon in the left ribbon.
this.addRibbonIcon(
"table",
"Excel",
(e: MouseEvent) => {
// Called when the user clicks the icon.
this.createAndOpenExcel(getExcelFilename(this.settings));
}
);
// markdwon后处理
this.addMarkdownPostProcessor();
}
private addMarkdownPostProcessor() {
initializeMarkdownPostProcessor(this);
this.registerMarkdownPostProcessor(markdownPostProcessor);
}
onunload() {}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
await this.saveData(this.settings);
}
public async createExcel(
filename: string,
foldername?: string,
initData?: string
): Promise<TFile> {
// const folderpath = normalizePath(
// foldername ? foldername : "Excel"
// );
// await checkAndCreateFolder(folderpath); //create folder if it does not exist
const fname = normalizePath(filename);
const file = await this.app.vault.create(fname, initData ?? "{}");
return file;
}
public async createAndOpenExcel(
filename: string,
foldername?: string,
initData?: string
): Promise<string> {
const file = await this.createExcel(filename, foldername, initData);
this.openExcel(file, "new-pane", true, undefined);
return file.path;
}
public openExcel(
excelFile: TFile,
location: PaneTarget,
active = false,
subpath?: string
) {
if (location === "md-properties") {
location = "new-tab";
}
var leaf: WorkspaceLeaf | null = null;
if (location === "popout-window") {
leaf = this.app.workspace.openPopoutLeaf();
}
if (location === "new-tab") {
leaf = this.app.workspace.getLeaf("tab");
}
if (!leaf) {
leaf = this.app.workspace.getLeaf(false);
if (
leaf.view.getViewType() !== "empty" &&
location === "new-pane"
) {
leaf = this.app.workspace.getMostRecentLeaf();
}
}
leaf?.openFile(
excelFile,
!subpath || subpath === ""
? { active }
: { active, eState: { subpath } }
).then(() => {});
}
public isExcelFile(f: TFile) {
if (!f) return false;
if (f.extension === "sheet") {
return true;
}
return false;
}
}

View file

@ -1,7 +1,7 @@
{ {
"id": "excel", "id": "excel",
"name": "Excel", "name": "Excel",
"version": "1.2.3", "version": "1.3.0",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Create spreadsheets and easily embed them in Markdown", "description": "Create spreadsheets and easily embed them in Markdown",
"author": "ljcoder", "author": "ljcoder",

1820
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -29,6 +29,7 @@
"typescript": "4.7.4" "typescript": "4.7.4"
}, },
"dependencies": { "dependencies": {
"monkey-around": "^2.3.0",
"x-data-spreadsheet": "^1.1.9", "x-data-spreadsheet": "^1.1.9",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz" "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz"
} }

View file

@ -1,38 +0,0 @@
import { MarkdownRenderChild } from "obsidian";
import Spreadsheet from "x-data-spreadsheet";
export class Excel extends MarkdownRenderChild {
data: string;
index: number;
constructor(containerEl: HTMLElement, data: string, index: number) {
super(containerEl);
this.data = data;
this.index = index;
}
onload(): void {
console.log
const sheetEle = this.containerEl.createDiv({
cls: 'sheet-iframe',
attr: {
id: `x-spreadsheet-${this.index}`,
},
});
const jsonData = JSON.parse(this.data || "{}") || {};
//@ts-ignore
const sheet = new Spreadsheet(sheetEle, {
mode: "read",
showToolbar: false,
showBottomBar: true,
view: {
height: () => 300,
width: () => this.containerEl.clientWidth,
},
}).loadData(jsonData); // load data
// @ts-ignore
sheet.validate();
}
}

View file

@ -1,45 +0,0 @@
import { syntaxTree } from "@codemirror/language";
import {
Extension,
RangeSetBuilder,
StateField,
Transaction,
} from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
} from "@codemirror/view";
import { ExcelWidget } from "./ExcelWidget";
export const ExcelField = StateField.define<DecorationSet>({
create(state): DecorationSet {
return Decoration.none;
},
update(oldState: DecorationSet, transaction: Transaction): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
syntaxTree(transaction.state).iterate({
enter(node) {
console.log(node)
// if (node.type.name.startsWith("list")) {
// // Position of the '-' or the '*'.
// const listCharFrom = node.from - 2;
// builder.add(
// listCharFrom,
// listCharFrom + 1,
// Decoration.replace({
// widget: new EmojiWidget(),
// })
// );
// }
},
});
return builder.finish();
},
provide(field: StateField<DecorationSet>): Extension {
return EditorView.decorations.from(field);
},
});

View file

@ -1,10 +1,9 @@
import ExcelPlugin from "main"; import ExcelPlugin from "src/main";
import { TextFileView, WorkspaceLeaf, Platform, Notice } from "obsidian"; import { TextFileView, WorkspaceLeaf, Platform, Notice } from "obsidian";
import Spreadsheet from "x-data-spreadsheet"; import Spreadsheet from "x-data-spreadsheet";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import { stox, xtos } from "./utils/xlsxspread"; import { stox, xtos } from "./utils/xlsxspread";
import { VIEW_TYPE_EXCEL, FRONTMATTER } from "./constants";
export const VIEW_TYPE_EXCEL = "excel-view";
export class ExcelView extends TextFileView { export class ExcelView extends TextFileView {
public plugin: ExcelPlugin; public plugin: ExcelPlugin;
@ -16,18 +15,18 @@ export class ExcelView extends TextFileView {
public copyHTMLEle: HTMLElement; public copyHTMLEle: HTMLElement;
public sheetEle: HTMLElement; public sheetEle: HTMLElement;
public cellsSelected: { public cellsSelected: {
sheet: Record<string, any> | null, sheet: Record<string, any> | null;
sri: number | null, // 选中开始行 index sri: number | null; // 选中开始行 index
sci: number | null, // 选中开始列 index sci: number | null; // 选中开始列 index
eri: number | null, // 选中结束行 index eri: number | null; // 选中结束行 index
eci: number | null, // 选中结束列 index eci: number | null; // 选中结束列 index
} = { } = {
sheet: null, sheet: null,
sri: null, sri: null,
sci: null, sci: null,
eri: null, eri: null,
eci: null, eci: null,
} };
constructor(leaf: WorkspaceLeaf, plugin: ExcelPlugin) { constructor(leaf: WorkspaceLeaf, plugin: ExcelPlugin) {
super(leaf); super(leaf);
@ -38,6 +37,31 @@ export class ExcelView extends TextFileView {
return this.data; return this.data;
} }
getExcelData(): string {
const tagText = "# Excel\n";
const trimLocation = this.data.search(tagText);
if (trimLocation == -1) return this.data;
const excelData = this.data.substring(
trimLocation + tagText.length,
this.data.length
);
// console.log("trimLocation", trimLocation, excelData, this.data);
return excelData;
}
headerData() {
return FRONTMATTER + "\n# Excel\n";
}
saveData(data: string) {
this.data = this.headerData() + data;
// console.log("saveData", this.data)
}
clear(): void {
this.data = this.headerData();
}
setViewData(data: string, clear: boolean): void { setViewData(data: string, clear: boolean): void {
this.data = data; this.data = data;
@ -57,8 +81,8 @@ export class ExcelView extends TextFileView {
//@ts-ignore //@ts-ignore
const files = e.target?.files; const files = e.target?.files;
if (!files) { if (!files) {
new Notice('Failed to get file') new Notice("Failed to get file");
return return;
} }
const f = files[0]; const f = files[0];
const reader = new FileReader(); const reader = new FileReader();
@ -68,7 +92,7 @@ export class ExcelView extends TextFileView {
if (data) { if (data) {
this.process_wb(XLSX.read(data)); this.process_wb(XLSX.read(data));
} else { } else {
new Notice('Read file error') new Notice("Read file error");
} }
}; };
reader.readAsArrayBuffer(f); reader.readAsArrayBuffer(f);
@ -80,7 +104,7 @@ export class ExcelView extends TextFileView {
this.sheet.loadData(sheetData); this.sheet.loadData(sheetData);
this.data = JSON.stringify(sheetData); this.data = JSON.stringify(sheetData);
} else { } else {
new Notice('Data parsing error') new Notice("Data parsing error");
} }
} }
@ -92,14 +116,12 @@ export class ExcelView extends TextFileView {
XLSX.writeFile(new_wb, title + ".xlsx", {}); XLSX.writeFile(new_wb, title + ".xlsx", {});
} }
handleEmbedLink(e:Event) { handleEmbedLink(e: Event) {
if (this.file) { if (this.file) {
navigator.clipboard.writeText( navigator.clipboard.writeText(`![[${this.file.path}]]`);
`![[${this.file.path}]]`, new Notice("Copy embed link to clipboard");
);
new Notice('Copy embed link to clipboard')
} else { } else {
new Notice('Copy embed link failed') new Notice("Copy embed link failed");
} }
} }
@ -126,9 +148,7 @@ export class ExcelView extends TextFileView {
super.onload(); super.onload();
} }
clear(): void {
this.data = "";
}
getViewType(): string { getViewType(): string {
return VIEW_TYPE_EXCEL; return VIEW_TYPE_EXCEL;
@ -159,96 +179,96 @@ export class ExcelView extends TextFileView {
); );
// 初始化 sheet // 初始化 sheet
const jsonData = JSON.parse(this.data || "{}") || {}; const jsonData = JSON.parse(this.getExcelData() || "{}") || {};
//@ts-ignore //@ts-ignore
this.sheet = new Spreadsheet(this.sheetEle, { this.sheet = new Spreadsheet(this.sheetEle, {
showBottomBar: true, showBottomBar: true,
view: { view: {
height: () => this.contentEl.clientHeight, height: () => this.contentEl.clientHeight,
width: () => this.contentEl.clientWidth, width: () => this.contentEl.clientWidth,
}, },
}) })
.loadData(jsonData) // load data .loadData(jsonData) // load data
.change(() => { .change(() => {
// save data to db // save data to db
const data = this.sheet.getData() const data = this.sheet.getData();
console.log('save data to db', data) // console.log("save data to db", data);
this.data = JSON.stringify(data); this.saveData(JSON.stringify(data))
}) })
.onAddSheet(()=> { .onAddSheet(() => {
const data = this.sheet.getData() const data = this.sheet.getData();
// console.log('onAddSheet', data) // console.log('onAddSheet', data)
this.data = JSON.stringify(data); this.saveData(JSON.stringify(data))
}) })
.onRenameSheet(()=> { .onRenameSheet(() => {
const data = this.sheet.getData() const data = this.sheet.getData();
// console.log('onRenameSheet', data) // console.log('onRenameSheet', data)
this.data = JSON.stringify(data); this.saveData(JSON.stringify(data))
}) });
this.sheet.on('cells-selected', (sheetData, {sri, sci, eri, eci}) => { this.sheet.on("cells-selected", (sheetData, { sri, sci, eri, eci }) => {
// console.log('cells-selected',sheetData, sri, sci, eri, eci) // console.log('cells-selected',sheetData, sri, sci, eri, eci)
this.cellsSelected.sheet = sheetData this.cellsSelected.sheet = sheetData;
this.cellsSelected.sri = sri this.cellsSelected.sri = sri;
this.cellsSelected.sci = sci this.cellsSelected.sci = sci;
this.cellsSelected.eri = eri this.cellsSelected.eri = eri;
this.cellsSelected.eci = eci this.cellsSelected.eci = eci;
});
}) this.sheet.on("cell-selected", (sheetData, ri, ci) => {
this.sheet.on('cell-selected', (sheetData, ri, ci) => {
// console.log('cell-selected',sheetData, ri, ci) // console.log('cell-selected',sheetData, ri, ci)
this.cellsSelected.sheet = sheetData this.cellsSelected.sheet = sheetData;
this.cellsSelected.sri = ri this.cellsSelected.sri = ri;
this.cellsSelected.sci = ci this.cellsSelected.sci = ci;
this.cellsSelected.eri = ri this.cellsSelected.eri = ri;
this.cellsSelected.eci = ci this.cellsSelected.eci = ci;
}) });
// @ts-ignore // @ts-ignore
this.sheet.validate(); this.sheet.validate();
} }
copyToHTML() { copyToHTML() {
const data = this.cellsSelected.sheet const data = this.cellsSelected.sheet;
const sri = this.cellsSelected.sri const sri = this.cellsSelected.sri;
const sci = this.cellsSelected.sci const sci = this.cellsSelected.sci;
const eri = this.cellsSelected.eri const eri = this.cellsSelected.eri;
const eci = this.cellsSelected.eci const eci = this.cellsSelected.eci;
// console.log('data', data, sri, sci, eri, eci) // console.log('data', data, sri, sci, eri, eci)
var html = "<table>" var html = "<table>";
if (data && sri && sci && eri && eci) { if (data && sri && sci && eri && eci) {
for (var row = sri; row <= eri; row ++) { for (var row = sri; row <= eri; row++) {
html += "<tr>" html += "<tr>";
const cells = data.rows._[`${row}`] const cells = data.rows._[`${row}`];
// console.log('cells', row, cells.cells) // console.log('cells', row, cells.cells)
if (cells) { if (cells) {
for (var col = sci; col <= eci; col ++ ) { for (var col = sci; col <= eci; col++) {
const cell = cells.cells[`${col}`] const cell = cells.cells[`${col}`];
// console.log('cell', row, col, cell) // console.log('cell', row, col, cell)
if (cell) { if (cell) {
if (cell.merge) { if (cell.merge) {
html += `<td rowspan="${cell.merge[0]}" colspan="${cell.merge[1]}">${cell.text}</td>` html += `<td rowspan="${cell.merge[0]}" colspan="${cell.merge[1]}">${cell.text}</td>`;
} else { } else {
html += `<td>${cell.text}</td>` html += `<td>${cell.text}</td>`;
} }
} }
} }
} }
html += "</tr>" html += "</tr>";
} }
} else { } else {
new Notice("Please first select the data to copy") new Notice("Please first select the data to copy");
} }
html +="</table>" html += "</table>";
navigator.clipboard.writeText(html); navigator.clipboard.writeText(html);
new Notice("copied") new Notice("copied");
} }
onResize() { onResize() {
@ -256,6 +276,6 @@ export class ExcelView extends TextFileView {
this.refresh(); this.refresh();
} }
// console.log('resize') // console.log('resize')
super.onResize() super.onResize();
} }
} }

View file

@ -1,37 +0,0 @@
import { EditorView, WidgetType } from "@codemirror/view"
import Spreadsheet from "x-data-spreadsheet";
export class ExcelWidget extends WidgetType {
data: string
constructor(data: string) {
super()
this.data = data
}
toDOM(view: EditorView): HTMLElement {
const time = new Date().getTime()
const sheetEle = document.createDiv({
cls: 'sheet-iframe',
attr: {
id: `x-spreadsheet-${time}`,
},
});
const jsonData = JSON.parse(this.data || "{}") || {};
//@ts-ignore
const sheet = new Spreadsheet(sheetEle, {
mode: "read",
showToolbar: false,
showBottomBar: true,
view: {
height: () => 300,
width: () => document.body.clientWidth,
},
}).loadData(jsonData); // load data
// @ts-ignore
sheet.validate();
return sheetEle
}
}

View file

@ -4,8 +4,8 @@ import {
TFile, TFile,
Vault, Vault,
} from "obsidian"; } from "obsidian";
import ExcelPlugin from "../main"; import ExcelPlugin from "./main";
import { Excel } from "./Excel"; import Spreadsheet from "x-data-spreadsheet";
let plugin: ExcelPlugin; let plugin: ExcelPlugin;
let vault: Vault; let vault: Vault;
@ -23,7 +23,7 @@ const tmpObsidianWYSIWYG = async (
ctx: MarkdownPostProcessorContext ctx: MarkdownPostProcessorContext
) => { ) => {
const file = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); const file = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath);
console.log('tmpObsidianWYSIWYG',file, el, ctx.sourcePath) console.log("tmpObsidianWYSIWYG");
if (!(file instanceof TFile)) return; if (!(file instanceof TFile)) return;
if (!plugin.isExcelFile(file)) return; if (!plugin.isExcelFile(file)) return;
@ -72,19 +72,81 @@ const tmpObsidianWYSIWYG = async (
//the excalidraw file in markdown preview mode //the excalidraw file in markdown preview mode
const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter")); const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter"));
el.empty(); el.empty();
if (!isFrontmatterDiv) {
if (el.parentElement === containerEl) containerEl.removeChild(el);
return;
}
internalEmbedDiv.empty(); internalEmbedDiv.empty();
const data = await vault.read(file); const data = await vault.read(file);
const excel = new Excel(el, data, new Date().getTime()); const sheetDiv = createSheetEl(
ctx.addChild(excel); getExcelData(data),
internalEmbedDiv.clientWidth
);
if (markdownEmbed) {
//display image on canvas without markdown frame
internalEmbedDiv.removeClass("markdown-embed");
internalEmbedDiv.removeClass("inline-embed");
}
internalEmbedDiv.appendChild(sheetDiv);
// console.log('internalEmbedDiv', internalEmbedDiv, markdownEmbed)
} }
el.empty(); el.empty();
if (internalEmbedDiv.hasAttribute("ready")) {
return;
}
internalEmbedDiv.setAttribute("ready", "");
internalEmbedDiv.empty(); internalEmbedDiv.empty();
const data = await vault.read(file); const data = await vault.read(file);
const excel = new Excel(el, data, 0); const sheetDiv = createSheetEl(
ctx.addChild(excel); getExcelData(data),
internalEmbedDiv.clientWidth
);
if (markdownEmbed) {
//display image on canvas without markdown frame
internalEmbedDiv.removeClass("markdown-embed");
internalEmbedDiv.removeClass("inline-embed");
}
internalEmbedDiv.appendChild(sheetDiv);
};
const createSheetEl = (data: string, width: number): HTMLDivElement => {
const sheetEle = createDiv({
cls: "sheet-iframe",
attr: {
id: `x-spreadsheet-${new Date().getTime()}`,
},
});
const jsonData = JSON.parse(data || "{}") || {};
//@ts-ignore
const sheet = new Spreadsheet(sheetEle, {
mode: "read",
showToolbar: false,
showBottomBar: true,
view: {
height: () => 300,
width: () => width,
},
}).loadData(jsonData); // load data
// @ts-ignore
sheet.validate();
return sheetEle;
};
const getExcelData = (data: string): string => {
const tagText = "# Excel\n";
const trimLocation = data.search(tagText);
if (trimLocation == -1) return data;
const excelData = data.substring(
trimLocation + tagText.length,
data.length
);
// console.log("trimLocation", trimLocation, excelData, this.data);
return excelData;
}; };
/** /**
@ -99,7 +161,9 @@ export const markdownPostProcessor = async (
//check to see if we are rendering in editing mode or live preview //check to see if we are rendering in editing mode or live preview
//if yes, then there should be no .internal-embed containers //if yes, then there should be no .internal-embed containers
const embeddedItems = el.querySelectorAll(".internal-embed"); const embeddedItems = el.querySelectorAll(".internal-embed");
console.log("markdownPostProcessor", embeddedItems.length);
if (embeddedItems.length === 0) { if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
return; return;
} }
@ -110,6 +174,7 @@ const processReadingMode = async (
embeddedItems: NodeListOf<Element> | [HTMLElement], embeddedItems: NodeListOf<Element> | [HTMLElement],
ctx: MarkdownPostProcessorContext ctx: MarkdownPostProcessorContext
) => { ) => {
console.log("processReadingMode");
//We are processing a non-excalidraw file in reading mode //We are processing a non-excalidraw file in reading mode
//Embedded files will be displayed in an .internal-embed container //Embedded files will be displayed in an .internal-embed container
@ -123,21 +188,31 @@ const processReadingMode = async (
if (!fname) return true; if (!fname) return true;
const file = metadataCache.getFirstLinkpathDest(fname, ctx.sourcePath); const file = metadataCache.getFirstLinkpathDest(fname, ctx.sourcePath);
console.log("forEach", file, ctx.sourcePath); // console.log("forEach", file, ctx.sourcePath);
//if the embeddedFile exits and it is an Excalidraw file //if the embeddedFile exits and it is an Excalidraw file
//then lets replace the .internal-embed with the generated PNG or SVG image //then lets replace the .internal-embed with the generated PNG or SVG image
if (file && file instanceof TFile && plugin.isExcelFile(file)) { if (file && file instanceof TFile && plugin.isExcelFile(file)) {
const data = await vault.read(file);
const parent = maybeDrawing.parentElement; maybeDrawing.parentElement?.replaceChild(
if (data && parent) { await processInternalEmbed(maybeDrawing,file),
const excel = new Excel( maybeDrawing
maybeDrawing.parentElement, );
data,
index
);
ctx.addChild(excel);
}
} }
}); });
}; };
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
const src = internalEmbedEl.getAttribute("src");
//@ts-ignore
if (!src) return;
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1059
internalEmbedEl.removeClass("markdown-embed");
internalEmbedEl.removeClass("inline-embed");
const data = await vault.read(file);
return await createSheetEl(getExcelData(data), internalEmbedEl.clientWidth);
}

5
src/constants.ts Normal file
View file

@ -0,0 +1,5 @@
export const VIEW_TYPE_EXCEL = "excel-view";
export const FRONTMATTER_KEY = "excel-plugin";
export const RERENDER_EVENT = "excel-embed-rerender";
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: parsed`,"","---", "", ""].join("\n");

336
src/main.ts Normal file
View file

@ -0,0 +1,336 @@
import {
TFile,
Plugin,
WorkspaceLeaf,
normalizePath,
ViewState,
MarkdownView,
Workspace,
} from "obsidian";
import { ExcelSettings, DEFAULT_SETTINGS } from "./utils/Settings";
import { PaneTarget } from "./utils/ModifierkeyHelper";
import { ExcelView } from "./ExcelView";
import { getExcelFilename } from "./utils/FileUtils";
import { around, dedupe } from "monkey-around";
import {
initializeMarkdownPostProcessor,
markdownPostProcessor,
} from "./MarkdownPostProcessor";
import { FRONTMATTER_KEY, FRONTMATTER, VIEW_TYPE_EXCEL } from "src/constants";
export default class ExcelPlugin extends Plugin {
public settings: ExcelSettings;
public excelFileModes: { [file: string]: string } = {};
private _loaded: boolean = false;
async onload() {
this.registerView(
VIEW_TYPE_EXCEL,
(leaf: WorkspaceLeaf) => new ExcelView(leaf, this)
);
this.registerExtensions(["sheet"], VIEW_TYPE_EXCEL);
// This creates an icon in the left ribbon.
this.addRibbonIcon("table", "Excel", (e: MouseEvent) => {
// Called when the user clicks the icon.
this.createAndOpenExcel(getExcelFilename(this.settings), undefined, this.getBlackData());
});
// markdwon后处理
this.addMarkdownPostProcessor();
//inspiration taken from kanban: https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
this.switchToExcelAfterLoad();
this.registerEventListeners();
}
onunload() {}
private getBlackData() {
return FRONTMATTER + "\n# Excel\n";
}
private addMarkdownPostProcessor() {
initializeMarkdownPostProcessor(this);
this.registerMarkdownPostProcessor(markdownPostProcessor);
}
private registerEventListeners() {
// const self = this;
// //save Excalidraw leaf and update embeds when switching to another leaf
// const activeLeafChangeEventHandler = async (leaf: WorkspaceLeaf) => {
// console.log('activeLeafChangeEventHandler', leaf)
// // this.switchToExcelAfterLoad()
// };
// self.registerEvent(
// this.app.workspace.on(
// "active-leaf-change",
// activeLeafChangeEventHandler
// )
// );
}
private switchToExcelAfterLoad() {
const self = this;
this.app.workspace.onLayoutReady(() => {
let leaf: WorkspaceLeaf;
let markdownLeaf = this.app.workspace.getLeavesOfType("markdown")
console.log("switchToExcelAfterLoad", markdownLeaf);
for (leaf of markdownLeaf) {
if (
leaf.view instanceof MarkdownView &&
leaf.view.file &&
self.isExcelFile(leaf.view.file)
) {
self.excelFileModes[
(leaf as any).id || leaf.view.file?.path
] = VIEW_TYPE_EXCEL;
self.setExcelView(leaf);
}
}
});
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
await this.saveData(this.settings);
}
private registerMonkeyPatches() {
const key =
"https://github.com/zsviczian/obsidian-excalidraw-plugin/issues";
this.register(
around(Workspace.prototype, {
getActiveViewOfType(old) {
console.log("Workspace.prototype", old);
return dedupe(key, old, function (...args) {
const result = old && old.apply(this, args);
const maybeSheetView =
this.app?.workspace?.activeLeaf?.view;
if (
!maybeSheetView ||
!(maybeSheetView instanceof ExcelView)
)
return result;
});
},
})
);
//@ts-ignore
if (!this.app.plugins?.plugins?.["obsidian-hover-editor"]) {
this.register(
//stolen from hover editor
around(WorkspaceLeaf.prototype, {
getRoot(old) {
console.log("stolen from hover editor");
return function () {
const top = old.call(this);
return top.getRoot === this.getRoot
? top
: top.getRoot();
};
},
})
);
}
this.registerEvent(
this.app.workspace.on("editor-menu", (menu, editor, view) => {
if (!view || !(view instanceof MarkdownView)) return;
const file = view.file;
const leaf = view.leaf;
if (!file) return;
const cache = this.app.metadataCache.getFileCache(file);
if (!cache?.frontmatter || !cache.frontmatter[FRONTMATTER_KEY])
return;
menu.addItem((item) =>
item
.setTitle("OPEN_AS_EXCEL")
.setIcon("grid")
.setSection("excel")
.onClick(() => {
//@ts-ignore
this.excelFileModes[leaf.id || file.path] =
VIEW_TYPE_EXCEL;
this.setExcelView(leaf);
})
);
})
);
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file, source, leaf) => {
if (!leaf || !(leaf.view instanceof MarkdownView)) return;
if (!(file instanceof TFile)) return;
const cache = this.app.metadataCache.getFileCache(file);
if (!cache?.frontmatter || !cache.frontmatter[FRONTMATTER_KEY])
return;
menu.addItem((item) => {
item.setTitle("OPEN_AS_EXCEL")
.setIcon("grid")
.setSection("pane")
.onClick(() => {
//@ts-ignore
this.excelFileModes[leaf.id || file.path] =
VIEW_TYPE_EXCEL;
this.setExcelView(leaf);
});
});
//@ts-ignore
menu.items.unshift(menu.items.pop());
})
);
const self = this;
// Monkey patch WorkspaceLeaf to open Excalidraw drawings with ExcalidrawView by default
this.register(
around(WorkspaceLeaf.prototype, {
// Drawings can be viewed as markdown or Excalidraw, and we keep track of the mode
// while the file is open. When the file closes, we no longer need to keep track of it.
detach(next) {
return function () {
const state = this.view?.getState();
console.log('state--', state.file)
if (
state?.file &&
self.excelFileModes[this.id || state.file]
) {
delete self.excelFileModes[this.id || state.file];
}
return next.apply(this);
};
},
setViewState(next) {
return function (state: ViewState, ...rest: any[]) {
if (
// Don't force excalidraw mode during shutdown
self._loaded &&
// If we have a markdown file
state.type === "markdown" &&
state.state?.file &&
// And the current mode of the file is not set to markdown
self.excelFileModes[this.id || state.state.file] !==
"markdown"
) {
// Then check for the excalidraw frontMatterKey
const cache = app.metadataCache.getCache(
state.state.file
);
if (
cache?.frontmatter &&
cache.frontmatter[FRONTMATTER_KEY]
) {
// If we have it, force the view type to excalidraw
const newState = {
...state,
type: VIEW_TYPE_EXCEL,
};
self.excelFileModes[state.state.file] =
VIEW_TYPE_EXCEL;
return next.apply(this, [newState, ...rest]);
}
}
return next.apply(this, [state, ...rest]);
};
},
})
);
}
public async setExcelView(leaf: WorkspaceLeaf) {
await leaf.setViewState({
type: VIEW_TYPE_EXCEL,
state: leaf.view.getState(),
popstate: true,
} as ViewState);
}
public async createExcel(
filename: string,
foldername?: string,
initData?: string
): Promise<TFile> {
const fname = normalizePath(filename);
const file = await this.app.vault.create(fname, initData ?? "{}");
return file;
}
public async createAndOpenExcel(
filename: string,
foldername?: string,
initData?: string
): Promise<string> {
const file = await this.createExcel(filename, foldername, initData);
this.openExcel(file, "new-pane", true, undefined);
return file.path;
}
public openExcel(
excelFile: TFile,
location: PaneTarget,
active = false,
subpath?: string
) {
if (location === "md-properties") {
location = "new-tab";
}
var leaf: WorkspaceLeaf | null = null;
if (location === "popout-window") {
leaf = this.app.workspace.openPopoutLeaf();
}
if (location === "new-tab") {
leaf = this.app.workspace.getLeaf("tab");
}
if (!leaf) {
leaf = this.app.workspace.getLeaf(false);
if (
leaf.view.getViewType() !== "empty" &&
location === "new-pane"
) {
leaf = this.app.workspace.getMostRecentLeaf();
}
}
leaf?.openFile(
excelFile,
!subpath || subpath === ""
? { active, state: { type: VIEW_TYPE_EXCEL } }
: { active, eState: { subpath }, state: { type: VIEW_TYPE_EXCEL } }
).then(() => {
if (leaf) {
this.setExcelView(leaf)
}
});
}
public isExcelFile(f: TFile) {
if (!f) return false;
if (f.extension === "sheet") {
return true;
}
const fileCache = f ? this.app.metadataCache.getFileCache(f) : null;
return (
!!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEY]
);
}
}

View file

@ -79,8 +79,8 @@ export function getNewUniqueFilepath(
let fname = normalizePath(`${folderpath}/${filename}`); let fname = normalizePath(`${folderpath}/${filename}`);
let file: TAbstractFile | null = vault.getAbstractFileByPath(fname); let file: TAbstractFile | null = vault.getAbstractFileByPath(fname);
let i = 0; let i = 0;
const extension = filename.endsWith(".xlsx.md") const extension = filename.endsWith(".sheet.md")
? ".xlsx.md" ? ".sheet.md"
: filename.slice(filename.lastIndexOf(".")); : filename.slice(filename.lastIndexOf("."));
while (file) { while (file) {
fname = normalizePath( fname = normalizePath(
@ -99,7 +99,7 @@ export function getExcelFilename(settings: ExcelSettings): string {
return ( return (
"Excel " + "Excel " +
window.moment().format('YYYY-MM-DD HH.mm.ss') + window.moment().format('YYYY-MM-DD HH.mm.ss') +
".sheet" ".sheet.md"
); );
} }

View file

@ -1,4 +1,4 @@
import ExcelPlugin from "main"; import ExcelPlugin from "src/main";
import { WorkspaceLeaf } from "obsidian"; import { WorkspaceLeaf } from "obsidian";
const getLeafLoc = ( const getLeafLoc = (