zhangkun9038@dingtalk.com 4380e95e00 2025-04-07 13:07:53: ...
2025-04-07 13:08:02 +08:00

480 lines
22 KiB
JavaScript

'use strict';
var obsidian = require('obsidian');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
const DEFAULT_SETTINGS = {
statusPropertyName: 'status',
showNotifications: false,
debugMode: false // Default to false for better performance
};
class KanbanStatusUpdaterPlugin extends obsidian.Plugin {
constructor() {
super(...arguments);
// Track active observers to disconnect them when not needed
this.currentObserver = null;
this.isProcessing = false;
this.activeKanbanBoard = null;
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
console.log('Loading Kanban Status Updater plugin');
// Load settings
yield this.loadSettings();
// Display startup notification
if (this.settings.showNotifications) {
new obsidian.Notice('Kanban Status Updater activated');
}
this.log('Plugin loaded');
// Register DOM event listener for drag events - but only process if active leaf is Kanban
this.registerDomEvent(document, 'dragend', this.onDragEnd.bind(this));
this.log('Registered drag event listener');
// Watch for active leaf changes to only observe the current Kanban board
this.registerEvent(this.app.workspace.on('active-leaf-change', this.onActiveLeafChange.bind(this)));
// Initial check for active Kanban board
this.app.workspace.onLayoutReady(() => {
this.checkForActiveKanbanBoard();
});
// Add settings tab
this.addSettingTab(new KanbanStatusUpdaterSettingTab(this.app, this));
});
}
onunload() {
// Disconnect any active observers to prevent memory leaks
this.disconnectObservers();
this.log('Plugin unloaded');
}
loadSettings() {
return __awaiter(this, void 0, void 0, function* () {
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
});
}
saveSettings() {
return __awaiter(this, void 0, void 0, function* () {
yield this.saveData(this.settings);
});
}
// Log helper with debug mode check
log(message) {
if (this.settings.debugMode) {
console.log(`[KSU] ${message}`);
}
}
// Clean up observers when switching away from a Kanban board
disconnectObservers() {
if (this.currentObserver) {
this.log('Disconnecting observer for performance');
this.currentObserver.disconnect();
this.currentObserver = null;
}
this.activeKanbanBoard = null;
}
// Check if the active leaf is a Kanban board
onActiveLeafChange(leaf) {
this.checkForActiveKanbanBoard();
}
checkForActiveKanbanBoard() {
var _a;
// First disconnect any existing observers
this.disconnectObservers();
// Get the active leaf using the non-deprecated API
const activeLeaf = this.app.workspace.getLeaf(false);
if (!activeLeaf)
return;
try {
// Find the content element safely
let contentEl = null;
// Use type assertions to avoid TypeScript errors
if (activeLeaf.view) {
// Try to access the contentEl property using type assertion
contentEl = activeLeaf.view.contentEl || null;
}
// If that didn't work, try another approach
if (!contentEl) {
// Try to get the Kanban board directly from the DOM
// Leaf containers have 'view-content' elements that contain the actual view
const viewContent = (_a = activeLeaf.containerEl) === null || _a === void 0 ? void 0 : _a.querySelector('.view-content');
if (viewContent) {
contentEl = viewContent;
}
else {
// Last resort - look for Kanban boards anywhere in the workspace
contentEl = document.querySelector('.workspace-leaf.mod-active .view-content');
}
}
if (!contentEl) {
this.log('Could not access content element for active leaf');
return;
}
// Check if this is a Kanban board
const kanbanBoard = contentEl.querySelector('.kanban-plugin__board');
if (kanbanBoard) {
this.log('Found active Kanban board, setting up observer');
// Store reference to active board
this.activeKanbanBoard = kanbanBoard;
// Set up observer only for this board
this.setupObserverForBoard(kanbanBoard);
}
else {
this.log('Active leaf is not a Kanban board');
}
}
catch (error) {
this.log(`Error detecting Kanban board: ${error.message}`);
}
}
setupObserverForBoard(boardElement) {
// Create a new observer for this specific board
this.currentObserver = new MutationObserver((mutations) => {
if (this.isProcessing)
return;
// Simple debounce to prevent rapid-fire processing
this.isProcessing = true;
setTimeout(() => {
this.handleMutations(mutations);
this.isProcessing = false;
}, 300);
});
// Observe only this board with minimal options needed
this.currentObserver.observe(boardElement, {
childList: true,
subtree: true,
attributes: false // Don't need attribute changes for performance
});
this.log('Observer set up for active Kanban board');
}
handleMutations(mutations) {
if (!this.activeKanbanBoard)
return;
try {
const max_mutations = 10;
// Only process a sample of mutations for performance
const mutationsToProcess = mutations.length > max_mutations ?
mutations.slice(0, max_mutations) : mutations;
this.log(`Got ${mutationsToProcess.length} mutations of ${mutations.length}`);
// Look for Kanban items in mutation
let i = 0;
for (const mutation of mutationsToProcess) {
this.log(`Mutation #${++i} - Type: ${mutation.type}`);
if (mutation.type === 'childList') {
// Check added nodes for Kanban items
for (const node of Array.from(mutation.addedNodes)) {
try {
// Check if node is any kind of Element (HTML or SVG)
if (node instanceof Element) {
this.log(`Processing Element of type: ${node.tagName}`);
// Handle the node according to its type
if (node instanceof HTMLElement || node instanceof HTMLDivElement) {
// Direct processing for HTML elements
this.log(`Found HTML element of type ${node.className}`);
this.processElement(node);
}
else if (node instanceof SVGElement) {
// For SVG elements, look for parent HTML element
const parentElement = node.closest('.kanban-plugin__item');
if (parentElement) {
this.log('Found Kanban item parent of SVG element');
this.processElement(parentElement);
}
else {
// Look for any kanban items in the document that might have changed
// This is for cases where the SVG update is related to a card movement
const items = this.activeKanbanBoard.querySelectorAll('.kanban-plugin__item');
if (items.length > 0) {
// Process only the most recently modified item
const recentItems = Array.from(items).slice(-1);
for (const item of recentItems) {
this.log('Processing recent item after SVG change');
this.processElement(item);
}
}
}
}
}
else if (node.nodeType === Node.TEXT_NODE) {
// For text nodes, check the parent element
const parentElement = node.parentElement;
if (parentElement && (parentElement.classList.contains('kanban-plugin__item-title') ||
parentElement.closest('.kanban-plugin__item'))) {
this.log('Found text change in Kanban item');
const itemElement = parentElement.closest('.kanban-plugin__item');
if (itemElement) {
this.processElement(itemElement);
}
}
}
else {
this.log(`Skipping node type: ${node.nodeType}`);
}
}
catch (nodeError) {
this.log(`Error processing node: ${nodeError.message}`);
// Continue with next node even if this one fails
}
}
}
else {
this.log('Ignoring mutation type: ' + mutation.type);
}
}
}
catch (error) {
this.log(`Error in handleMutations: ${error.message}`);
}
}
onDragEnd(event) {
// Only process if we have an active Kanban board
if (!this.activeKanbanBoard || this.isProcessing) {
this.log('Drag end detected but no active Kanban board or already processing');
this.log('activeKanbanBoard: ' + (this.activeKanbanBoard ? 'Yes' : 'No'));
this.log('isProcessing: ' + (this.isProcessing ? 'Yes' : 'No'));
return;
}
try {
this.log('Drag end detected');
// Set processing flag to prevent multiple processing
this.isProcessing = true;
const target = event.target;
if (!target)
return;
this.processElement(target);
}
catch (error) {
this.log(`Error in onDragEnd: ${error.message}`);
}
finally {
// Reset processing flag after a delay to debounce
setTimeout(() => {
this.isProcessing = false;
}, 300);
}
}
processElement(element) {
try {
// Only process if inside our active Kanban board
if (!this.activeKanbanBoard || !element.closest('.kanban-plugin__board')) {
this.log('Element NOT in active Kanban board. Skipping.');
return;
}
// Use different strategies to find the Kanban item
this.log("👀 Looking for Kanban item element");
// Check if element is a Kanban item or contains one
const kanbanItem = element.classList.contains('kanban-plugin__item')
? element
: element.querySelector('.kanban-plugin__item');
if (kanbanItem) {
this.log(`✅ Found Kanban item: ${kanbanItem}`);
this.log('classList of kanbanItem: ' + kanbanItem.classList);
this.processKanbanItem(kanbanItem);
return;
}
this.log('Not a Kanban item, checking for parent');
// If element is inside a Kanban item, find the parent
const parentItem = element.closest('.kanban-plugin__item');
this.log(`Parent item: ${parentItem ? parentItem : 'Not found'}`);
if (parentItem) {
this.processKanbanItem(parentItem);
return;
}
}
catch (error) {
this.log(`Error in processElement: ${error.message}`);
}
}
processKanbanItem(itemElement) {
try {
// TODO: Select the title
const internalLink = itemElement.querySelector('.kanban-plugin__item-title .kanban-plugin__item-markdown a.internal-link');
if (!internalLink) {
this.log('🚫 No internal link found in item');
return;
}
this.log(`Found internal link: ${internalLink.textContent}`);
// Get the link path from data-href or href attribute
const linkPath = internalLink.getAttribute('data-href') ||
internalLink.getAttribute('href');
if (!linkPath)
return;
this.log(`🔗 Link path: ${linkPath}`);
// Find the lane (column) this item is in
const lane = itemElement.closest('.kanban-plugin__lane');
if (!lane) {
this.log('🚫 No lane found for item');
return;
}
// Get column name from the lane header
const laneHeader = lane.querySelector('.kanban-plugin__lane-header-wrapper .kanban-plugin__lane-title');
if (!laneHeader) {
this.log('🚫 No laneHeader found for item');
return;
}
const columnName = laneHeader.textContent.trim();
this.log(`✅ Got lane name: ${columnName}`);
this.log(`Processing card with link to "${linkPath}" in column "${columnName}"`);
// Update the linked note's status
this.updateNoteStatus(linkPath, columnName);
}
catch (error) {
this.log(`Error in processKanbanItem: ${error.message}`);
}
}
updateNoteStatus(notePath, status) {
return __awaiter(this, void 0, void 0, function* () {
try {
// Find the linked file
const file = this.app.metadataCache.getFirstLinkpathDest(notePath, '');
if (!file) {
if (this.settings.showNotifications) {
new obsidian.Notice(`⚠️ Note "${notePath}" not found`, 3000);
}
return;
}
// Get current status if it exists
const metadata = this.app.metadataCache.getFileCache(file);
let oldStatus = null;
if ((metadata === null || metadata === void 0 ? void 0 : metadata.frontmatter) && metadata.frontmatter[this.settings.statusPropertyName]) {
oldStatus = metadata.frontmatter[this.settings.statusPropertyName];
}
// Only update if status has changed
if (oldStatus !== status) {
// Use the processFrontMatter API to update the frontmatter
yield this.app.fileManager.processFrontMatter(file, (frontmatter) => {
// Set the status property
frontmatter[this.settings.statusPropertyName] = status;
});
// Show notification if enabled
if (this.settings.showNotifications) {
if (oldStatus) {
new obsidian.Notice(`Updated ${this.settings.statusPropertyName}: "${oldStatus}" → "${status}" for ${file.basename}`, 3000);
}
else {
new obsidian.Notice(`Set ${this.settings.statusPropertyName}: "${status}" for ${file.basename}`, 3000);
}
}
this.log(`Updated status for ${file.basename} to "${status}"`);
}
else {
this.log(`Status already set to "${status}" for ${file.basename}, skipping update`);
}
}
catch (error) {
this.log(`Error updating note status: ${error.message}`);
if (this.settings.showNotifications) {
new obsidian.Notice(`⚠️ Error updating status: ${error.message}`, 3000);
}
}
});
}
// Method for the test button to use
runTest() {
this.log('Running test...');
// Make sure we're using the current active board
this.checkForActiveKanbanBoard();
if (!this.activeKanbanBoard) {
new obsidian.Notice('⚠️ No active Kanban board found - open a Kanban board first', 5000);
return;
}
// Find items in the active board
const items = this.activeKanbanBoard.querySelectorAll('.kanban-plugin__item');
const count = items.length;
new obsidian.Notice(`Found ${count} cards in active Kanban board`, 3000);
if (count > 0) {
// Process the first item with a link
for (let i = 0; i < count; i++) {
const item = items[i];
if (item.querySelector('a.internal-link')) {
new obsidian.Notice(`Testing with card: "${item.textContent.substring(0, 20)}..."`, 3000);
this.processKanbanItem(item);
break;
}
}
}
}
}
class KanbanStatusUpdaterSettingTab extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
new obsidian.Setting(containerEl)
.setName('Status property name')
.setDesc('The name of the property to update when a card is moved')
.addText(text => text
.setPlaceholder('status')
.setValue(this.plugin.settings.statusPropertyName)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.statusPropertyName = value;
yield this.plugin.saveSettings();
})));
new obsidian.Setting(containerEl)
.setName('Show notifications')
.setDesc('Show a notification when a status is updated')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showNotifications)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.showNotifications = value;
yield this.plugin.saveSettings();
})));
new obsidian.Setting(containerEl)
.setName('Debug mode')
.setDesc('Enable detailed logging (reduces performance)')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.debugMode)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.debugMode = value;
yield this.plugin.saveSettings();
if (value) {
new obsidian.Notice('Debug mode enabled - check console for logs', 3000);
}
else {
new obsidian.Notice('Debug mode disabled', 3000);
}
})));
// Add a test button
new obsidian.Setting(containerEl)
.setName('Test plugin')
.setDesc('Test with current Kanban board')
.addButton(button => button
.setButtonText('Run Test')
.onClick(() => {
this.plugin.runTest();
}));
}
}
module.exports = KanbanStatusUpdaterPlugin;
/* nosourcemap */