xcloud-ai_table-layout-helper/main.js

747 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* XU Table Layout Helper - main.js
* Pure JavaScript implementation, no compilation needed
*
* Features:
* 1. Custom table layout (fixed / auto)
* 2. Table width control (100% / custom / auto)
* 3. Column width: first column fixed, others auto
* 4. Text wrapping: nowrap / break-word / break-all
* 5. Vertical alignment: top / middle / bottom
* 6. Custom line height
* 7. Overflow control: visible / hidden / scroll / auto
* 8. Max height with scroll
* 9. Sticky header
* 10. Zebra stripes
* 11. Link no-wrap
* 12. Border control
*
* Implementation: Dynamic <style> injection into document.head
* No !important used — overrides via selector specificity
* Bilingual UI (Chinese / English) with language switcher
*/
const { Plugin, Notice, PluginSettingTab, Setting } = require("obsidian");
const PLUGIN_ID = "table-layout-helper";
const STYLE_ID = "table-layout-helper-style";
// ================================================================
// i18n (Bilingual support)
// ================================================================
const I18N = {
zh: {
// Commands
cmd_toggle: "切换表格布局助手开关",
cmd_reload: "重新加载表格样式",
// Notices
notice_enabled: "已开启",
notice_disabled: "已关闭",
notice_reloaded: "样式已重新加载",
notice_reset: "设置已恢复为默认值",
// Settings - language
setting_language: "界面语言",
setting_language_desc: "选择设置面板的显示语言",
lang_zh: "中文",
lang_en: "English",
// Settings - basic
setting_enable: "启用插件",
setting_enable_desc: "关闭后将恢复 Obsidian 默认表格样式",
// Settings - sections
sec_table_layout: "表格布局",
sec_column_width: "列宽",
sec_alignment: "对齐与行高",
sec_overflow: "溢出控制",
sec_styling: "样式",
sec_border: "边框",
// Settings - table layout
setting_table_layout: "表格布局模式",
setting_table_layout_desc: "fixed = 固定列宽推荐auto = 自动列宽",
setting_table_width: "表格宽度",
setting_table_width_desc: "支持 100% / auto / 像素值(如 800px",
// Settings - column width
setting_first_col_width: "首列宽度",
setting_first_col_width_desc: "像素值(如 200留空为自动",
setting_first_col_nowrap: "首列不换行",
setting_first_col_nowrap_desc: "防止首列文字换行(适合标签列)",
setting_content_wrap: "内容列换行模式",
setting_content_wrap_desc: "break-word推荐/ break-all / normal",
// Settings - alignment
setting_vertical_align: "垂直对齐",
setting_vertical_align_desc: "单元格内容的垂直对齐方式",
setting_line_height: "行高",
setting_line_height_desc: "单元格文字行高(如 1.6",
// Settings - overflow
setting_overflow_mode: "溢出模式",
setting_overflow_mode_desc: "visible / hidden / scroll / auto",
setting_max_height: "最大高度",
setting_max_height_desc: "像素值(如 400px留空为无限制。与 overflow=scroll 配合使用效果最佳",
setting_sticky_header: "粘性表头",
setting_sticky_header_desc: "滚动时表头固定在顶部(需配合最大高度 + overflow=scroll",
// Settings - styling
setting_zebra: "斑马纹",
setting_zebra_desc: "交替行背景色",
setting_zebra_color: "斑马纹颜色",
setting_zebra_color_desc: "偶数行背景色",
setting_link_nowrap: "链接不换行",
setting_link_nowrap_desc: "防止表格中的链接换行",
// Settings - border
setting_border_width: "边框宽度",
setting_border_width_desc: "像素值0 = 无边框",
setting_border_color: "边框颜色",
setting_border_color_desc: "边框颜色",
setting_border_collapse: "边框合并",
setting_border_collapse_desc: "collapse / separate",
// Settings - reset
setting_reset: "恢复默认设置",
setting_reset_desc: "将所有设置恢复为默认值",
btn_reset: "重置",
// Tip
tip_title: "使用提示",
tip_1: "1. 本插件自动应用于所有 Markdown 表格",
tip_2: "2. 修改设置后立即生效,无需重启",
tip_3: "3. 使用 Ctrl+P → \"切换表格布局助手开关\" 快速开关",
tip_4: "4. 推荐配置:长表格使用 overflow=scroll + max-height=400px + sticky-header=on",
tip_css_title: "替换 CSS 代码片段",
tip_css_desc: "本插件替换 table-fixed.css 代码片段。启用本插件后请禁用原代码片段。",
},
en: {
// Commands
cmd_toggle: "Toggle table layout control",
cmd_reload: "Reload table style",
// Notices
notice_enabled: "Enabled",
notice_disabled: "Disabled",
notice_reloaded: "style reloaded",
notice_reset: "Settings reset to defaults",
// Settings - language
setting_language: "UI Language",
setting_language_desc: "Select the display language for settings panel",
lang_zh: "中文",
lang_en: "English",
// Settings - basic
setting_enable: "Enable plugin",
setting_enable_desc: "Turn off to restore default Obsidian table styles",
// Settings - sections
sec_table_layout: "Table Layout",
sec_column_width: "Column Width",
sec_alignment: "Alignment & Line Height",
sec_overflow: "Overflow Control",
sec_styling: "Styling",
sec_border: "Border",
// Settings - table layout
setting_table_layout: "Table layout mode",
setting_table_layout_desc: "fixed = fixed column width (recommended), auto = auto column width",
setting_table_width: "Table width",
setting_table_width_desc: "Supports 100% / auto / pixel value (e.g. 800px)",
// Settings - column width
setting_first_col_width: "First column width",
setting_first_col_width_desc: "Pixel value (e.g. 200), leave empty for auto",
setting_first_col_nowrap: "First column no-wrap",
setting_first_col_nowrap_desc: "Prevent first column text from wrapping (good for label columns)",
setting_content_wrap: "Content column wrap mode",
setting_content_wrap_desc: "break-word (recommended) / break-all / normal",
// Settings - alignment
setting_vertical_align: "Vertical alignment",
setting_vertical_align_desc: "Vertical alignment of cell content",
setting_line_height: "Line height",
setting_line_height_desc: "Cell text line height (e.g. 1.6)",
// Settings - overflow
setting_overflow_mode: "Overflow mode",
setting_overflow_mode_desc: "visible / hidden / scroll / auto",
setting_max_height: "Max height",
setting_max_height_desc: "Pixel value (e.g. 400px), leave empty for no limit. Works best with overflow=scroll",
setting_sticky_header: "Sticky header",
setting_sticky_header_desc: "Header stays on top when scrolling (requires max height + overflow=scroll)",
// Settings - styling
setting_zebra: "Zebra stripes",
setting_zebra_desc: "Alternating row background colors",
setting_zebra_color: "Zebra color",
setting_zebra_color_desc: "Even row background color",
setting_link_nowrap: "Link no-wrap",
setting_link_nowrap_desc: "Prevent links in tables from wrapping",
// Settings - border
setting_border_width: "Border width",
setting_border_width_desc: "Pixel value, 0 = no border",
setting_border_color: "Border color",
setting_border_color_desc: "Border color",
setting_border_collapse: "Border collapse",
setting_border_collapse_desc: "collapse / separate",
// Settings - reset
setting_reset: "Reset to defaults",
setting_reset_desc: "Restore all settings to default values",
btn_reset: "Reset",
// Tip
tip_title: "Usage Tips",
tip_1: "1. This plugin applies to all Markdown tables automatically",
tip_2: "2. Changes apply instantly — no restart needed",
tip_3: "3. Use Ctrl+P → \"Toggle table layout control\" for quick on/off",
tip_4: "4. Recommended: overflow=scroll + max-height=400px + sticky-header=on for long tables",
tip_css_title: "Replaces CSS snippet",
tip_css_desc: "This plugin replaces the table-fixed.css snippet. Disable the original snippet after enabling this plugin.",
},
};
const DEFAULT_SETTINGS = {
enabled: true,
language: "zh", // "zh" or "en"
// Table layout
tableLayout: "fixed",
tableWidth: "100%",
// First column
firstColumnWidth: "200",
firstColumnWrap: false,
// Content columns
contentColumnWrap: "break-word",
verticalAlign: "top",
lineHeight: "1.6",
// Overflow
overflowMode: "visible",
maxHeight: "",
// Sticky header
stickyHeader: false,
// Zebra stripes
zebraStripes: false,
zebraColor: "#f8f9fa",
// Link no-wrap
linkNoWrap: true,
// Border
borderWidth: "1",
borderColor: "#e8e8e8",
borderCollapse: "collapse",
};
// ================================================================
// CSS Generator
// ================================================================
function generateCSS(settings) {
if (!settings.enabled) {
return "/* XU Table Layout Helper: disabled */";
}
const css = [];
// Selectors with high specificity to avoid !important
const tableSel = ".markdown-preview-view table, .markdown-source-view .cm-table-widget table";
const thSel = ".markdown-preview-view table th, .markdown-source-view .cm-table-widget table th";
const tdSel = ".markdown-preview-view table td, .markdown-source-view .cm-table-widget table td";
const firstColSel = ".markdown-preview-view table th:first-child, .markdown-preview-view table td:first-child, .markdown-source-view .cm-table-widget table th:first-child, .markdown-source-view .cm-table-widget table td:first-child";
const lastColSel = ".markdown-preview-view table th:last-child, .markdown-preview-view table td:last-child, .markdown-source-view .cm-table-widget table th:last-child, .markdown-source-view .cm-table-widget table td:last-child";
// 1. Table layout
const tableWidth = settings.tableWidth || "100%";
css.push(`${tableSel} {
table-layout: ${settings.tableLayout};
width: ${tableWidth};
border-collapse: ${settings.borderCollapse};
${settings.borderWidth > 0 ? `border: ${settings.borderWidth}px solid ${settings.borderColor};` : ""}
}`);
// 2. Cell styles
const cellStyles = [
`vertical-align: ${settings.verticalAlign};`,
`line-height: ${settings.lineHeight};`,
];
if (settings.borderWidth > 0) {
cellStyles.push(`border: ${settings.borderWidth}px solid ${settings.borderColor};`);
}
css.push(`${thSel}, ${tdSel} {
${cellStyles.join("\n ")}
}`);
// 3. First column
const firstColStyles = [];
if (settings.firstColumnWidth) {
firstColStyles.push(`width: ${settings.firstColumnWidth}px;`);
}
firstColStyles.push(`white-space: ${settings.firstColumnWrap ? "normal" : "nowrap"};`);
css.push(`${firstColSel} {
${firstColStyles.join("\n ")}
}`);
// 4. Content columns (last column)
const contentColStyles = [`width: auto;`];
if (settings.contentColumnWrap === "break-word") {
contentColStyles.push("word-break: break-word;");
} else if (settings.contentColumnWrap === "break-all") {
contentColStyles.push("word-break: break-all;");
} else {
contentColStyles.push("word-break: normal;");
}
css.push(`${lastColSel} {
${contentColStyles.join("\n ")}
}`);
// 5. Overflow
if (settings.overflowMode !== "visible" || settings.maxHeight) {
const wrapperSel = ".markdown-preview-view .tlh-table-wrapper, .markdown-source-view .cm-table-widget .tlh-table-wrapper";
const wrapperStyles = [];
if (settings.overflowMode !== "visible") {
wrapperStyles.push(`overflow: ${settings.overflowMode};`);
}
if (settings.maxHeight) {
wrapperStyles.push(`max-height: ${settings.maxHeight};`);
}
css.push(`${wrapperSel} {
${wrapperStyles.join("\n ")}
}`);
}
// 6. Sticky header
if (settings.stickyHeader) {
css.push(`${thSel} {
position: sticky;
top: 0;
z-index: 10;
background: var(--background-secondary, #f8f9fa);
}`);
}
// 7. Zebra stripes
if (settings.zebraStripes) {
css.push(`.markdown-preview-view table tr:nth-child(even), .markdown-source-view .cm-table-widget table tr:nth-child(even) {
background: ${settings.zebraColor};
}`);
}
// 8. Link no-wrap
if (settings.linkNoWrap) {
css.push(`.markdown-preview-view table td:last-child a, .markdown-source-view .cm-table-widget table td:last-child a {
white-space: nowrap;
}`);
}
return css.join("\n\n");
}
// ================================================================
// Main Plugin Class
// ================================================================
class TableLayoutHelperPlugin extends Plugin {
settings;
// i18n helper
t(key) {
const lang = this.settings ? this.settings.language : "zh";
const dict = I18N[lang] || I18N.zh;
return dict[key] || key;
}
async onload() {
await this.loadSettings();
this.injectStyle();
// Commands (names use i18n, re-registered on language change)
this.registerCommands();
// Settings tab
this.addSettingTab(new TableLayoutHelperSettingTab(this.app, this));
}
registerCommands() {
// Commands (names use i18n, re-adding with same id is idempotent for our use case)
this.addCommand({
id: "toggle-table-layout",
name: this.t("cmd_toggle"),
callback: () => {
this.settings.enabled = !this.settings.enabled;
this.saveSettings();
this.injectStyle();
new Notice(
`XU Table Layout Helper: ${this.settings.enabled ? this.t("notice_enabled") : this.t("notice_disabled")}`,
2000
);
},
});
this.addCommand({
id: "reload-table-style",
name: this.t("cmd_reload"),
callback: () => {
this.injectStyle();
new Notice(`XU Table Layout Helper: ${this.t("notice_reloaded")}`, 2000);
},
});
}
onunload() {
const styleEl = document.getElementById(STYLE_ID);
if (styleEl) {
styleEl.remove();
}
}
injectStyle() {
let styleEl = document.getElementById(STYLE_ID);
if (!styleEl) {
styleEl = document.createElement("style");
styleEl.id = STYLE_ID;
document.head.appendChild(styleEl);
}
styleEl.textContent = generateCSS(this.settings);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
}
// ================================================================
// Settings Tab
// ================================================================
class TableLayoutHelperSettingTab extends PluginSettingTab {
plugin;
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
// i18n helper
t(key) {
return this.plugin.t(key);
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "XU Table Layout Helper" });
// ---------- Language switcher (top) ----------
new Setting(containerEl)
.setName(this.t("setting_language"))
.setDesc(this.t("setting_language_desc"))
.addDropdown((dropdown) =>
dropdown
.addOption("zh", this.t("lang_zh"))
.addOption("en", this.t("lang_en"))
.setValue(this.plugin.settings.language)
.onChange(async (value) => {
this.plugin.settings.language = value;
await this.plugin.saveSettings();
// Re-register commands with new language
this.plugin.registerCommands();
// Re-render settings panel
this.display();
})
);
containerEl.createEl("hr");
// ---------- Basic ----------
new Setting(containerEl)
.setName(this.t("setting_enable"))
.setDesc(this.t("setting_enable_desc"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.enabled)
.onChange(async (value) => {
this.plugin.settings.enabled = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
containerEl.createEl("hr");
// ---------- Table layout ----------
containerEl.createEl("h3", { text: this.t("sec_table_layout") });
new Setting(containerEl)
.setName(this.t("setting_table_layout"))
.setDesc(this.t("setting_table_layout_desc"))
.addDropdown((dropdown) =>
dropdown
.addOption("fixed", "fixed")
.addOption("auto", "auto")
.setValue(this.plugin.settings.tableLayout)
.onChange(async (value) => {
this.plugin.settings.tableLayout = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_table_width"))
.setDesc(this.t("setting_table_width_desc"))
.addText((text) =>
text
.setPlaceholder("100%")
.setValue(this.plugin.settings.tableWidth)
.onChange(async (value) => {
this.plugin.settings.tableWidth = value || "100%";
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
// ---------- Column width ----------
containerEl.createEl("h3", { text: this.t("sec_column_width") });
new Setting(containerEl)
.setName(this.t("setting_first_col_width"))
.setDesc(this.t("setting_first_col_width_desc"))
.addText((text) =>
text
.setPlaceholder("200")
.setValue(String(this.plugin.settings.firstColumnWidth))
.onChange(async (value) => {
this.plugin.settings.firstColumnWidth = value.replace(/[^\d]/g, "");
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_first_col_nowrap"))
.setDesc(this.t("setting_first_col_nowrap_desc"))
.addToggle((toggle) =>
toggle
.setValue(!this.plugin.settings.firstColumnWrap)
.onChange(async (value) => {
this.plugin.settings.firstColumnWrap = !value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_content_wrap"))
.setDesc(this.t("setting_content_wrap_desc"))
.addDropdown((dropdown) =>
dropdown
.addOption("break-word", "break-word")
.addOption("break-all", "break-all")
.addOption("normal", "normal")
.setValue(this.plugin.settings.contentColumnWrap)
.onChange(async (value) => {
this.plugin.settings.contentColumnWrap = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
// ---------- Alignment & line height ----------
containerEl.createEl("h3", { text: this.t("sec_alignment") });
new Setting(containerEl)
.setName(this.t("setting_vertical_align"))
.setDesc(this.t("setting_vertical_align_desc"))
.addDropdown((dropdown) =>
dropdown
.addOption("top", "top")
.addOption("middle", "middle")
.addOption("bottom", "bottom")
.setValue(this.plugin.settings.verticalAlign)
.onChange(async (value) => {
this.plugin.settings.verticalAlign = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_line_height"))
.setDesc(this.t("setting_line_height_desc"))
.addText((text) =>
text
.setPlaceholder("1.6")
.setValue(this.plugin.settings.lineHeight)
.onChange(async (value) => {
this.plugin.settings.lineHeight = value || "1.6";
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
// ---------- Overflow ----------
containerEl.createEl("h3", { text: this.t("sec_overflow") });
new Setting(containerEl)
.setName(this.t("setting_overflow_mode"))
.setDesc(this.t("setting_overflow_mode_desc"))
.addDropdown((dropdown) =>
dropdown
.addOption("visible", "visible")
.addOption("hidden", "hidden")
.addOption("scroll", "scroll")
.addOption("auto", "auto")
.setValue(this.plugin.settings.overflowMode)
.onChange(async (value) => {
this.plugin.settings.overflowMode = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_max_height"))
.setDesc(this.t("setting_max_height_desc"))
.addText((text) =>
text
.setPlaceholder("400px")
.setValue(this.plugin.settings.maxHeight)
.onChange(async (value) => {
this.plugin.settings.maxHeight = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_sticky_header"))
.setDesc(this.t("setting_sticky_header_desc"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.stickyHeader)
.onChange(async (value) => {
this.plugin.settings.stickyHeader = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
// ---------- Styling ----------
containerEl.createEl("h3", { text: this.t("sec_styling") });
new Setting(containerEl)
.setName(this.t("setting_zebra"))
.setDesc(this.t("setting_zebra_desc"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.zebraStripes)
.onChange(async (value) => {
this.plugin.settings.zebraStripes = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_zebra_color"))
.setDesc(this.t("setting_zebra_color_desc"))
.addColorPicker((color) =>
color
.setValue(this.plugin.settings.zebraColor)
.onChange(async (value) => {
this.plugin.settings.zebraColor = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_link_nowrap"))
.setDesc(this.t("setting_link_nowrap_desc"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.linkNoWrap)
.onChange(async (value) => {
this.plugin.settings.linkNoWrap = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
// ---------- Border ----------
containerEl.createEl("h3", { text: this.t("sec_border") });
new Setting(containerEl)
.setName(this.t("setting_border_width"))
.setDesc(this.t("setting_border_width_desc"))
.addText((text) =>
text
.setPlaceholder("1")
.setValue(String(this.plugin.settings.borderWidth))
.onChange(async (value) => {
this.plugin.settings.borderWidth = value.replace(/[^\d]/g, "") || "0";
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_border_color"))
.setDesc(this.t("setting_border_color_desc"))
.addColorPicker((color) =>
color
.setValue(this.plugin.settings.borderColor)
.onChange(async (value) => {
this.plugin.settings.borderColor = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
new Setting(containerEl)
.setName(this.t("setting_border_collapse"))
.setDesc(this.t("setting_border_collapse_desc"))
.addDropdown((dropdown) =>
dropdown
.addOption("collapse", "collapse")
.addOption("separate", "separate")
.setValue(this.plugin.settings.borderCollapse)
.onChange(async (value) => {
this.plugin.settings.borderCollapse = value;
await this.plugin.saveSettings();
this.plugin.injectStyle();
})
);
containerEl.createEl("hr");
// ---------- Usage tips ----------
const tip = containerEl.createEl("div", { cls: "tlh-tip" });
tip.innerHTML = `
<b>${this.t("tip_title")}</b><br>
${this.t("tip_1")}<br>
${this.t("tip_2")}<br>
${this.t("tip_3")}<br>
${this.t("tip_4")}<br>
<br>
<b>${this.t("tip_css_title")}</b><br>
${this.t("tip_css_desc")}
`;
// ---------- Reset ----------
new Setting(containerEl)
.setName(this.t("setting_reset"))
.setDesc(this.t("setting_reset_desc"))
.addButton((button) =>
button
.setButtonText(this.t("btn_reset"))
.setWarning()
.onClick(async () => {
const savedLang = this.plugin.settings.language;
this.plugin.settings = Object.assign({}, DEFAULT_SETTINGS);
this.plugin.settings.language = savedLang;
await this.plugin.saveSettings();
this.plugin.injectStyle();
this.plugin.registerCommands();
this.display();
new Notice(this.t("notice_reset"), 2000);
})
);
}
}
module.exports = TableLayoutHelperPlugin;
module.exports.default = TableLayoutHelperPlugin;