mirror of
https://github.com/ljcoder2015/obsidian-excel.git
synced 2026-07-22 04:48:22 +00:00
feat: 编辑模式也能预览sheet, 文件格式变为 .sheet.md
This commit is contained in:
parent
b5d9f413fd
commit
f166222309
14 changed files with 1047 additions and 1660 deletions
|
|
@ -16,7 +16,7 @@ const context = await esbuild.context({
|
|||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
|
|
|
|||
130
main.ts
130
main.ts
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "excel",
|
||||
"name": "Excel",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Create spreadsheets and easily embed them in Markdown",
|
||||
"author": "ljcoder",
|
||||
|
|
|
|||
1820
package-lock.json
generated
1820
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -29,6 +29,7 @@
|
|||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"monkey-around": "^2.3.0",
|
||||
"x-data-spreadsheet": "^1.1.9",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz"
|
||||
}
|
||||
|
|
|
|||
38
src/Excel.ts
38
src/Excel.ts
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
170
src/ExcelView.ts
170
src/ExcelView.ts
|
|
@ -1,10 +1,9 @@
|
|||
import ExcelPlugin from "main";
|
||||
import ExcelPlugin from "src/main";
|
||||
import { TextFileView, WorkspaceLeaf, Platform, Notice } from "obsidian";
|
||||
import Spreadsheet from "x-data-spreadsheet";
|
||||
import * as XLSX from "xlsx";
|
||||
import { stox, xtos } from "./utils/xlsxspread";
|
||||
|
||||
export const VIEW_TYPE_EXCEL = "excel-view";
|
||||
import { VIEW_TYPE_EXCEL, FRONTMATTER } from "./constants";
|
||||
|
||||
export class ExcelView extends TextFileView {
|
||||
public plugin: ExcelPlugin;
|
||||
|
|
@ -16,18 +15,18 @@ export class ExcelView extends TextFileView {
|
|||
public copyHTMLEle: HTMLElement;
|
||||
public sheetEle: HTMLElement;
|
||||
public cellsSelected: {
|
||||
sheet: Record<string, any> | null,
|
||||
sri: number | null, // 选中开始行 index
|
||||
sci: number | null, // 选中开始列 index
|
||||
eri: number | null, // 选中结束行 index
|
||||
eci: number | null, // 选中结束列 index
|
||||
sheet: Record<string, any> | null;
|
||||
sri: number | null; // 选中开始行 index
|
||||
sci: number | null; // 选中开始列 index
|
||||
eri: number | null; // 选中结束行 index
|
||||
eci: number | null; // 选中结束列 index
|
||||
} = {
|
||||
sheet: null,
|
||||
sri: null,
|
||||
sci: null,
|
||||
eri: null,
|
||||
eci: null,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ExcelPlugin) {
|
||||
super(leaf);
|
||||
|
|
@ -38,6 +37,31 @@ export class ExcelView extends TextFileView {
|
|||
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 {
|
||||
this.data = data;
|
||||
|
||||
|
|
@ -57,18 +81,18 @@ export class ExcelView extends TextFileView {
|
|||
//@ts-ignore
|
||||
const files = e.target?.files;
|
||||
if (!files) {
|
||||
new Notice('Failed to get file')
|
||||
return
|
||||
new Notice("Failed to get file");
|
||||
return;
|
||||
}
|
||||
const f = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const data = e.target?.result;
|
||||
|
||||
|
||||
if (data) {
|
||||
this.process_wb(XLSX.read(data));
|
||||
} else {
|
||||
new Notice('Read file error')
|
||||
new Notice("Read file error");
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(f);
|
||||
|
|
@ -80,7 +104,7 @@ export class ExcelView extends TextFileView {
|
|||
this.sheet.loadData(sheetData);
|
||||
this.data = JSON.stringify(sheetData);
|
||||
} 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", {});
|
||||
}
|
||||
|
||||
handleEmbedLink(e:Event) {
|
||||
handleEmbedLink(e: Event) {
|
||||
if (this.file) {
|
||||
navigator.clipboard.writeText(
|
||||
`![[${this.file.path}]]`,
|
||||
);
|
||||
new Notice('Copy embed link to clipboard')
|
||||
navigator.clipboard.writeText(`![[${this.file.path}]]`);
|
||||
new Notice("Copy embed link to clipboard");
|
||||
} else {
|
||||
new Notice('Copy embed link failed')
|
||||
new Notice("Copy embed link failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,9 +148,7 @@ export class ExcelView extends TextFileView {
|
|||
super.onload();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.data = "";
|
||||
}
|
||||
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_EXCEL;
|
||||
|
|
@ -159,96 +179,96 @@ export class ExcelView extends TextFileView {
|
|||
);
|
||||
|
||||
// 初始化 sheet
|
||||
const jsonData = JSON.parse(this.data || "{}") || {};
|
||||
const jsonData = JSON.parse(this.getExcelData() || "{}") || {};
|
||||
|
||||
//@ts-ignore
|
||||
this.sheet = new Spreadsheet(this.sheetEle, {
|
||||
showBottomBar: true,
|
||||
view: {
|
||||
height: () => this.contentEl.clientHeight,
|
||||
width: () => this.contentEl.clientWidth,
|
||||
},
|
||||
})
|
||||
view: {
|
||||
height: () => this.contentEl.clientHeight,
|
||||
width: () => this.contentEl.clientWidth,
|
||||
},
|
||||
})
|
||||
.loadData(jsonData) // load data
|
||||
.change(() => {
|
||||
// save data to db
|
||||
const data = this.sheet.getData()
|
||||
console.log('save data to db', data)
|
||||
this.data = JSON.stringify(data);
|
||||
const data = this.sheet.getData();
|
||||
// console.log("save data to db", data);
|
||||
this.saveData(JSON.stringify(data))
|
||||
})
|
||||
.onAddSheet(()=> {
|
||||
const data = this.sheet.getData()
|
||||
.onAddSheet(() => {
|
||||
const data = this.sheet.getData();
|
||||
// console.log('onAddSheet', data)
|
||||
this.data = JSON.stringify(data);
|
||||
this.saveData(JSON.stringify(data))
|
||||
})
|
||||
.onRenameSheet(()=> {
|
||||
const data = this.sheet.getData()
|
||||
.onRenameSheet(() => {
|
||||
const data = this.sheet.getData();
|
||||
// console.log('onRenameSheet', data)
|
||||
this.data = JSON.stringify(data);
|
||||
})
|
||||
|
||||
this.sheet.on('cells-selected', (sheetData, {sri, sci, eri, eci}) => {
|
||||
this.saveData(JSON.stringify(data))
|
||||
});
|
||||
|
||||
this.sheet.on("cells-selected", (sheetData, { sri, sci, eri, eci }) => {
|
||||
// console.log('cells-selected',sheetData, sri, sci, eri, eci)
|
||||
this.cellsSelected.sheet = sheetData
|
||||
this.cellsSelected.sri = sri
|
||||
this.cellsSelected.sci = sci
|
||||
this.cellsSelected.eri = eri
|
||||
this.cellsSelected.eci = eci
|
||||
|
||||
})
|
||||
|
||||
this.sheet.on('cell-selected', (sheetData, ri, ci) => {
|
||||
this.cellsSelected.sheet = sheetData;
|
||||
this.cellsSelected.sri = sri;
|
||||
this.cellsSelected.sci = sci;
|
||||
this.cellsSelected.eri = eri;
|
||||
this.cellsSelected.eci = eci;
|
||||
});
|
||||
|
||||
this.sheet.on("cell-selected", (sheetData, ri, ci) => {
|
||||
// console.log('cell-selected',sheetData, ri, ci)
|
||||
this.cellsSelected.sheet = sheetData
|
||||
this.cellsSelected.sri = ri
|
||||
this.cellsSelected.sci = ci
|
||||
this.cellsSelected.eri = ri
|
||||
this.cellsSelected.eci = ci
|
||||
})
|
||||
this.cellsSelected.sheet = sheetData;
|
||||
this.cellsSelected.sri = ri;
|
||||
this.cellsSelected.sci = ci;
|
||||
this.cellsSelected.eri = ri;
|
||||
this.cellsSelected.eci = ci;
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
this.sheet.validate();
|
||||
}
|
||||
|
||||
copyToHTML() {
|
||||
const data = this.cellsSelected.sheet
|
||||
const sri = this.cellsSelected.sri
|
||||
const sci = this.cellsSelected.sci
|
||||
const eri = this.cellsSelected.eri
|
||||
const eci = this.cellsSelected.eci
|
||||
const data = this.cellsSelected.sheet;
|
||||
const sri = this.cellsSelected.sri;
|
||||
const sci = this.cellsSelected.sci;
|
||||
const eri = this.cellsSelected.eri;
|
||||
const eci = this.cellsSelected.eci;
|
||||
|
||||
// console.log('data', data, sri, sci, eri, eci)
|
||||
|
||||
var html = "<table>"
|
||||
var html = "<table>";
|
||||
|
||||
if (data && sri && sci && eri && eci) {
|
||||
for (var row = sri; row <= eri; row ++) {
|
||||
html += "<tr>"
|
||||
const cells = data.rows._[`${row}`]
|
||||
for (var row = sri; row <= eri; row++) {
|
||||
html += "<tr>";
|
||||
const cells = data.rows._[`${row}`];
|
||||
// console.log('cells', row, cells.cells)
|
||||
if (cells) {
|
||||
for (var col = sci; col <= eci; col ++ ) {
|
||||
const cell = cells.cells[`${col}`]
|
||||
for (var col = sci; col <= eci; col++) {
|
||||
const cell = cells.cells[`${col}`];
|
||||
// console.log('cell', row, col, cell)
|
||||
if (cell) {
|
||||
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 {
|
||||
html += `<td>${cell.text}</td>`
|
||||
html += `<td>${cell.text}</td>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html += "</tr>"
|
||||
|
||||
html += "</tr>";
|
||||
}
|
||||
} 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);
|
||||
new Notice("copied")
|
||||
new Notice("copied");
|
||||
}
|
||||
|
||||
onResize() {
|
||||
|
|
@ -256,6 +276,6 @@ export class ExcelView extends TextFileView {
|
|||
this.refresh();
|
||||
}
|
||||
// console.log('resize')
|
||||
super.onResize()
|
||||
super.onResize();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ import {
|
|||
TFile,
|
||||
Vault,
|
||||
} from "obsidian";
|
||||
import ExcelPlugin from "../main";
|
||||
import { Excel } from "./Excel";
|
||||
import ExcelPlugin from "./main";
|
||||
import Spreadsheet from "x-data-spreadsheet";
|
||||
|
||||
let plugin: ExcelPlugin;
|
||||
let vault: Vault;
|
||||
|
|
@ -23,7 +23,7 @@ const tmpObsidianWYSIWYG = async (
|
|||
ctx: MarkdownPostProcessorContext
|
||||
) => {
|
||||
const file = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath);
|
||||
console.log('tmpObsidianWYSIWYG',file, el, ctx.sourcePath)
|
||||
console.log("tmpObsidianWYSIWYG");
|
||||
if (!(file instanceof TFile)) return;
|
||||
if (!plugin.isExcelFile(file)) return;
|
||||
|
||||
|
|
@ -72,19 +72,81 @@ const tmpObsidianWYSIWYG = async (
|
|||
//the excalidraw file in markdown preview mode
|
||||
const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter"));
|
||||
el.empty();
|
||||
|
||||
if (!isFrontmatterDiv) {
|
||||
if (el.parentElement === containerEl) containerEl.removeChild(el);
|
||||
return;
|
||||
}
|
||||
internalEmbedDiv.empty();
|
||||
const data = await vault.read(file);
|
||||
const excel = new Excel(el, data, new Date().getTime());
|
||||
ctx.addChild(excel);
|
||||
const sheetDiv = createSheetEl(
|
||||
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();
|
||||
|
||||
if (internalEmbedDiv.hasAttribute("ready")) {
|
||||
return;
|
||||
}
|
||||
internalEmbedDiv.setAttribute("ready", "");
|
||||
|
||||
internalEmbedDiv.empty();
|
||||
const data = await vault.read(file);
|
||||
const excel = new Excel(el, data, 0);
|
||||
ctx.addChild(excel);
|
||||
const sheetDiv = createSheetEl(
|
||||
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
|
||||
//if yes, then there should be no .internal-embed containers
|
||||
const embeddedItems = el.querySelectorAll(".internal-embed");
|
||||
console.log("markdownPostProcessor", embeddedItems.length);
|
||||
if (embeddedItems.length === 0) {
|
||||
tmpObsidianWYSIWYG(el, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +174,7 @@ const processReadingMode = async (
|
|||
embeddedItems: NodeListOf<Element> | [HTMLElement],
|
||||
ctx: MarkdownPostProcessorContext
|
||||
) => {
|
||||
console.log("processReadingMode");
|
||||
//We are processing a non-excalidraw file in reading mode
|
||||
//Embedded files will be displayed in an .internal-embed container
|
||||
|
||||
|
|
@ -123,21 +188,31 @@ const processReadingMode = async (
|
|||
if (!fname) return true;
|
||||
|
||||
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
|
||||
//then lets replace the .internal-embed with the generated PNG or SVG image
|
||||
if (file && file instanceof TFile && plugin.isExcelFile(file)) {
|
||||
const data = await vault.read(file);
|
||||
const parent = maybeDrawing.parentElement;
|
||||
if (data && parent) {
|
||||
const excel = new Excel(
|
||||
maybeDrawing.parentElement,
|
||||
data,
|
||||
index
|
||||
);
|
||||
ctx.addChild(excel);
|
||||
}
|
||||
|
||||
maybeDrawing.parentElement?.replaceChild(
|
||||
await processInternalEmbed(maybeDrawing,file),
|
||||
maybeDrawing
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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
5
src/constants.ts
Normal 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
336
src/main.ts
Normal 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -79,8 +79,8 @@ export function getNewUniqueFilepath(
|
|||
let fname = normalizePath(`${folderpath}/${filename}`);
|
||||
let file: TAbstractFile | null = vault.getAbstractFileByPath(fname);
|
||||
let i = 0;
|
||||
const extension = filename.endsWith(".xlsx.md")
|
||||
? ".xlsx.md"
|
||||
const extension = filename.endsWith(".sheet.md")
|
||||
? ".sheet.md"
|
||||
: filename.slice(filename.lastIndexOf("."));
|
||||
while (file) {
|
||||
fname = normalizePath(
|
||||
|
|
@ -99,7 +99,7 @@ export function getExcelFilename(settings: ExcelSettings): string {
|
|||
return (
|
||||
"Excel " +
|
||||
window.moment().format('YYYY-MM-DD HH.mm.ss') +
|
||||
".sheet"
|
||||
".sheet.md"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import ExcelPlugin from "main";
|
||||
import ExcelPlugin from "src/main";
|
||||
import { WorkspaceLeaf } from "obsidian";
|
||||
|
||||
const getLeafLoc = (
|
||||
|
|
|
|||
Loading…
Reference in a new issue