<%* /**
- Template for convo notes
- @author Ljavuras [email protected] */ tR += await tp.file.include("[[system.common]]");
customJS.Plugins.templater.addTags(tp, ['note/convo']); -%>
| /** Clock/view.css */ | |
| /** | |
| * Clock widget built upon Dataview plugin | |
| * | |
| * @author ljavuras <[email protected]> | |
| */ | |
| .widget__clock { | |
| margin: var(--size-4-2); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| line-height: 1; | |
| } | |
| .widget__clock__time { | |
| font-size: 48px; | |
| } | |
| .widget__clock__date .internal-link, | |
| .widget__clock__date .internal-link.is-unresolved { | |
| color: var(--text-normal); | |
| } |
| /** Clock/view.js */ | |
| /** | |
| * Clock widget built upon Dataview plugin | |
| * | |
| * @author ljavuras <[email protected]> | |
| */ | |
| const containerEl = input.containerEl.createDiv({ cls: "widget__clock" }); | |
| const timeEl = containerEl.createDiv({ cls: "widget__clock__time" }); | |
| function updateTime() { | |
| timeEl.innerHTML = moment().format("HH:mm"); | |
| setTimeout(() => { | |
| updateTime(); | |
| }, moment().endOf('minute').diff(moment()) + 1); | |
| } | |
| updateTime(); | |
| const dateEl = containerEl.createDiv({ cls: "widget__clock__date" }); | |
| function updateDate() { | |
| customJS.Obsidian.renderMarkdown( | |
| `[[${moment().format("YYYY-MM-DD"}|${moment().format("MMM Do, dddd")}]]`, | |
| dateEl, | |
| dv.currentFilePath, | |
| dv.component | |
| ); | |
| setTimeout(() => { | |
| updateDate(); | |
| }, moment().endOf('day').diff(moment()) + 1); | |
| } | |
| updateDate(); |
| /** Convo/view.css */ | |
| /** | |
| * A social media like widget to display conversations | |
| * | |
| * @author Ljavuras <[email protected]> | |
| */ | |
| .convoWidget { | |
| display: flex; | |
| flex-direction: column; | |
| row-gap: var(--size-4-2); | |
| } | |
| .newConvo { | |
| padding: var(--size-4-2) var(--size-4-1) var(--size-4-1) var(--size-4-3); | |
| border: 1px solid var(--background-modifier-border); | |
| border-radius: var(--radius-m); | |
| cursor: text; | |
| } | |
| .newConvo:has(:focus) { | |
| border-color: var(--background-modifier-border-focus); | |
| } | |
| .newConvo__input { | |
| margin-top: var(--size-4-1); | |
| padding: unset; | |
| width: 100%; | |
| resize: none; | |
| overflow: hidden; | |
| background-color: var(--background-primary); | |
| border: none; | |
| } | |
| .newConvo__input:focus { | |
| box-shadow: none; | |
| } | |
| .newConvo__submit { | |
| display: block; | |
| height: unset; | |
| line-height: 1; | |
| padding: var(--size-4-2) var(--size-4-3); | |
| margin: 0 var(--size-4-1) var(--size-4-1) auto; | |
| border-radius: var(--radius-s); | |
| } | |
| .convo { | |
| /* padding: var(--size-4-2) var(--size-4-3); */ | |
| cursor: default; | |
| } | |
| .convo.top-level { | |
| border: 1px solid var(--background-modifier-border); | |
| border-radius: var(--radius-m); | |
| } | |
| .convo.top-level:has(> .convo__comment-section.hide), | |
| .convo__comment-section.last-comment-section { | |
| padding-bottom: var(--size-4-2); | |
| } | |
| .convo__content { | |
| padding-top: var(--size-4-2); | |
| } | |
| .convo__bottom { | |
| display: flex; | |
| justify-content: flex-end; | |
| } | |
| .convo__content, .convo__bottom { | |
| padding-left: var(--size-4-3); | |
| padding-right: var(--size-4-3); | |
| } | |
| .convo__time { | |
| margin-right: auto; | |
| display: inline-flex; | |
| align-items: center; | |
| font-size: var(--font-smaller); | |
| color: var(--text-muted); | |
| } | |
| .convo__show-comment { | |
| gap: var(--size-4-1); | |
| } | |
| .convo__link.internal-link { | |
| color: unset; | |
| } | |
| .convo__link.internal-link:hover { | |
| color: unset; | |
| } | |
| .convo__comment-section { | |
| margin-left: var(--size-4-3); | |
| border-left: 1px solid var(--background-modifier-border); | |
| } | |
| .convo__comment-section .newConvo { | |
| padding-right: var(--size-4-2); | |
| padding-bottom: 0; | |
| border: none; | |
| } | |
| .convo__comment-section .newConvo__submit { | |
| margin-right: 0; | |
| margin-bottom: 0; | |
| } | |
| .icon-wrap { | |
| display: flex; | |
| } | |
| .hide { | |
| display: none; | |
| } |
| /** Convo/view.js */ | |
| /** | |
| * A social media like widget to display conversations | |
| * | |
| * @author Ljavuras <[email protected]> | |
| */ | |
| let obsidian; | |
| try { | |
| obsidian = require('obsidian'); // enabled by fix require modules plugin | |
| } catch { | |
| obsidian = input.obsidian // provide obsidian api through `dv.view()` | |
| || customJS.obsidian // get obsidian api from customjs plugin | |
| // get obsidian api from templater plugin | |
| || app.plugins.plugins['templater-obsidian'].templater.current_functions_object.obsidian | |
| ; | |
| } | |
| if (!obsidian) { /* Handle error */ } | |
| let widgetOptions = { | |
| locale: input.locale ?? localStorage.getItem('language') ?? 'en', | |
| }; | |
| const containerEl = (input.containerEl || dv.container).createEl('div', { cls: "convoWidget" }); | |
| let newConvoEl = containerEl.createEl('div', { cls:"newConvo" }); | |
| newConvoEl.onclick = (event) => { | |
| newConvoInputEl.focus(); | |
| } | |
| let newConvoInputEl = newConvoEl.createEl('textarea', { | |
| cls: "newConvo__input", | |
| attr: { placeholder: "Write something", rows: 1}, | |
| }); | |
| newConvoInputEl.oninput = (event) => { | |
| newConvoInputEl.style.height = '1px'; | |
| newConvoInputEl.style.height = newConvoInputEl.scrollHeight + 'px'; | |
| } | |
| let newConvoSubmitEl = newConvoEl.createEl('button', { | |
| cls: "newConvo__submit", | |
| text: "Post" | |
| }); | |
| newConvoSubmitEl.onclick = async (event) => { | |
| let convoFile = await customJS.Plugins.templater.createNewFileFromTemplate( | |
| //TODO: handle filename exist error | |
| `convo.${moment().format('YYYY-MMDD-HHss')}.md`, | |
| "note.convo" | |
| ); | |
| app.vault.append(convoFile, newConvoInputEl.value); | |
| }; | |
| //TODO: Filter based on date, note count | |
| let convos = dv.pages("#note/convo") | |
| .filter(page => !page['convo-replies-to']) | |
| .filter(page => { | |
| return DataviewAPI.luxon.DateTime.now().diff(page.created, 'days').days < 5 | |
| }) | |
| .sort(page => page.created, 'desc'); | |
| for (const convo of convos) { | |
| renderConvo(convo, containerEl, topLevel = true); | |
| } | |
| let shownNewCommentEl; | |
| async function renderConvo(convo, containerEl, topLevel = false, | |
| convoRenderOptions = { | |
| expandAllComment: true, | |
| }, | |
| lastCommentSectionList = [], | |
| isLast = true) { | |
| let commentConvos = dv.pages("#note/convo") | |
| .filter((commentConvo) => { | |
| return convo.file.link.equals(commentConvo['convo-replies-to']); | |
| }) | |
| .sort(page => page.created, 'asc'); | |
| // Better solution, but doesn't work: | |
| // dv.pages(`"${convo.file.link.markdown()}"`).filter(/* filter convo-replies-to */); | |
| let convoEl = containerEl.createEl('div', { | |
| cls: topLevel? "convo top-level" : "convo" | |
| }); | |
| let convoContentEl = convoEl.createEl('div', { cls: "convo__content" }); | |
| customJS.Obsidian.renderMarkdown( | |
| await customJS.Obsidian.vault.getFileContent(convo.file.path), | |
| convoContentEl, | |
| dv.current().file.path, | |
| dv.component | |
| ); | |
| let convoBottomEl = convoEl.createEl('div', { cls: "convo__bottom" }); | |
| let convoTimeEl = convoBottomEl.createEl('span', { | |
| cls: "convo__time", | |
| text: convo.created.setLocale(widgetOptions.locale).toRelative() + ', ' + | |
| convo.created.setLocale(widgetOptions.locale).toFormat('EEE') | |
| }); | |
| convoTimeEl.onmouseenter = (event) => { | |
| convoTimeEl.textContent = convo.created.setLocale(widgetOptions.locale) | |
| .toLocaleString(DataviewAPI.luxon.DateTime.DATETIME_SHORT); | |
| }; | |
| convoTimeEl.onmouseleave = (event) => { | |
| convoTimeEl.textContent = convo.created.setLocale(widgetOptions.locale).toRelative(); | |
| } | |
| let convoShowNewCommentEl = convoBottomEl.createEl('button', { | |
| cls: "clickable-icon", | |
| }); | |
| obsidian.setIcon(convoShowNewCommentEl, 'reply'); | |
| obsidian.setTooltip(convoShowNewCommentEl, "Reply"); | |
| convoShowNewCommentEl.onclick = (event) => { | |
| convoCommentSectionEl.show(); | |
| convoNewCommentEl.show(); | |
| } | |
| let convoShowCommentEl = convoBottomEl.createEl('button', { | |
| cls: "convo__show-comment clickable-icon", | |
| text: (commentConvos.length)? commentConvos.length : "", | |
| }); | |
| obsidian.setIcon( | |
| convoShowCommentEl.createEl('div', { cls: "icon-wrap" }), | |
| 'message-circle' | |
| ); | |
| obsidian.setTooltip(convoShowCommentEl, "Comments"); | |
| convoShowCommentEl.onclick = (event) => { | |
| if (convoCommentSectionEl.hasClass("hide")) { | |
| convoCommentSectionEl.show(); | |
| convoNewCommentEl.show(); | |
| } else { | |
| convoCommentSectionEl.hide(); | |
| convoNewCommentEl.hide(); | |
| } | |
| } | |
| let convoCopyLinkEl = convoBottomEl.createEl('button', { | |
| cls: "clickable-icon" | |
| }); | |
| obsidian.setIcon(convoCopyLinkEl, 'copy'); | |
| obsidian.setTooltip(convoCopyLinkEl, "Copy link") | |
| convoCopyLinkEl.onclick = (event) => { | |
| navigator.clipboard.writeText(convo.file.link.markdown()); | |
| new Notice("Markdown link copied to clipboard."); | |
| } | |
| let convoLinkEl = convoBottomEl.createEl('a', { | |
| cls: "convo__link internal-link clickable-icon", | |
| attr: { | |
| 'data-tooltip-position': 'top', | |
| 'aria-label': convo.file.name, | |
| 'data-href': convo.file.name, | |
| href: convo.file.name, | |
| target: '_blank', | |
| rel: 'noopener', | |
| } | |
| }) | |
| obsidian.setIcon(convoLinkEl, 'link'); | |
| let convoCommentSectionEl = convoEl.createEl('div', { | |
| cls: "convo__comment-section", | |
| }); | |
| if (!convoRenderOptions.expandAllComment) { convoCommentSectionEl.addClass("hide"); } | |
| if (!commentConvos.length) { convoCommentSectionEl.addClass("hide"); } | |
| convoCommentSectionEl.show = () => { | |
| convoCommentSectionEl.removeClass("hide"); | |
| findLastCommentSection(); | |
| }; | |
| convoCommentSectionEl.hide = () => { | |
| convoCommentSectionEl.addClass("hide"); | |
| findLastCommentSection(); | |
| }; | |
| function findLastCommentSection() { | |
| const CLS_LAST_COMMENT_SECTION = "last-comment-section"; | |
| let found = false; | |
| for (let i = 0; i < lastCommentSectionList.length; i++) { | |
| let commentSection = lastCommentSectionList[i]; | |
| if (found) { | |
| commentSection.removeClass(CLS_LAST_COMMENT_SECTION); | |
| } else if (i == lastCommentSectionList.length - 1) { | |
| commentSection.hasClass("hide") | |
| ? commentSection.removeClass(CLS_LAST_COMMENT_SECTION) | |
| : commentSection.addClass(CLS_LAST_COMMENT_SECTION); | |
| } else { | |
| let nextCommentSection = lastCommentSectionList[i + 1]; | |
| if (!commentSection.hasClass("hide") && nextCommentSection.hasClass("hide")) { | |
| commentSection.addClass(CLS_LAST_COMMENT_SECTION); | |
| found = true; | |
| } else { | |
| commentSection.removeClass(CLS_LAST_COMMENT_SECTION); | |
| } | |
| } | |
| } | |
| } | |
| let convoCommentsEl = convoCommentSectionEl.createEl('div', { | |
| cls: "convo__comments" | |
| }); | |
| if (isLast) { lastCommentSectionList.push(convoCommentSectionEl); } | |
| commentConvos.forEach((commentConvo, i, commentConvos) => { | |
| renderConvo( | |
| commentConvo, | |
| convoCommentsEl, | |
| topLevel = false, | |
| convoRenderOptions, | |
| lastCommentSectionList = lastCommentSectionList, | |
| isLast = (i === commentConvos.length - 1), | |
| ); | |
| }); | |
| findLastCommentSection(); | |
| let convoNewCommentEl = convoCommentSectionEl.createEl('div', { | |
| cls: "newConvo hide", | |
| }); | |
| convoNewCommentEl.show = () => { | |
| shownNewCommentEl?.hide(); | |
| convoCommentSectionEl.show(); | |
| convoNewCommentEl.removeClass("hide"); | |
| convoNewCommentInputEl.focus(); | |
| shownNewCommentEl = convoNewCommentEl; | |
| }; | |
| convoNewCommentEl.hide = () => { | |
| convoNewCommentInputEl.blur(); | |
| convoNewCommentEl.addClass("hide"); | |
| if (!commentConvos.length) { | |
| convoCommentSectionEl.hide(); | |
| } | |
| }; | |
| convoNewCommentEl.onclick = (event) => { | |
| convoNewCommentInputEl.focus(); | |
| }; | |
| let convoNewCommentInputEl = convoNewCommentEl.createEl('textarea', { | |
| cls: "newConvo__input", | |
| attr: { placeholder: "Write a comment", rows: 1}, | |
| }); | |
| convoNewCommentInputEl.oninput = (event) => { | |
| convoNewCommentInputEl.style.height = '1px'; | |
| convoNewCommentInputEl.style.height = convoNewCommentInputEl.scrollHeight + 'px'; | |
| } | |
| let convoNewCommentSubmitEl = convoNewCommentEl.createEl('button', { | |
| cls: "newConvo__submit", | |
| text: "Reply" | |
| }); | |
| convoNewCommentSubmitEl.onclick = async (event) => { | |
| let convoFile = await customJS.Plugins.templater.createNewFileFromTemplate( | |
| //TODO: handle filename exist error | |
| `convo.${moment().format('YYYY-MMDD-HHss')}.md`, | |
| "note.convo" | |
| ); | |
| customJS.Obsidian.frontmatter.set( | |
| convoFile, | |
| { 'convo-replies-to': convo.file.link.markdown() } | |
| ); | |
| app.vault.append(convoFile, convoNewCommentInputEl.value); | |
| }; | |
| } |
| /** Home/view.css */ | |
| /** | |
| * Home dashboard in Obsidian, built upon Dataview plugin | |
| * | |
| * @author ljavuras <[email protected]> | |
| */ | |
| #home__banner { | |
| margin-bottom: var(--size-4-8); | |
| } | |
| #home__tasks { | |
| margin: var(--size-4-2) 0; | |
| display: flex; | |
| flex-direction: row; | |
| gap: var(--size-4-2); | |
| white-space: nowrap; | |
| overflow-x: auto; | |
| } | |
| #home__tasks > * { | |
| margin: unset; | |
| flex-grow: 1; | |
| } |
| /** | |
| * Home dashboard in Obsidian, built upon Dataview plugin | |
| * | |
| * @author ljavuras <[email protected]> | |
| */ | |
| const options = { | |
| locale: 'en-US', | |
| } | |
| const containerEl = dv.container; | |
| // Unload all child components created by TaskList widgets | |
| //TODO: TaskLists should garbage collect themselves | |
| for (const childComponent of dv.component._children) { | |
| dv.component.removeChild(childComponent); | |
| } | |
| const bannerEl = containerEl.createDiv({ attr: { id: "home__banner" }}); | |
| dv.view("System/scripts/Dataview/widget/Clock", { | |
| containerEl: bannerEl, | |
| locale: options.locale, | |
| }); | |
| const tasksEl = containerEl.createDiv({ attr: { id: "home__tasks" }}); | |
| dv.view("System/scripts/Dataview/widget/TaskList", { | |
| containerEl: tasksEl, | |
| tasks: dv.page(moment().format("YYYY-MM-DD")) | |
| ?.file.tasks | |
| .filter((t) => (t.header.subpath == "Daily")), | |
| title: "Daily", | |
| errorMarkdown: `[[${moment().format("YYYY-MM-DD")}]] is not created yet.`, | |
| }); | |
| dv.view("System/scripts/Dataview/widget/TaskList", { | |
| containerEl: tasksEl, | |
| tasks: dv.page(moment().format("YYYY-MM-DD"))?.file.tasks | |
| .filter((t) => (t.header.subpath == "Today")), | |
| title: "Today", | |
| errorMarkdown: `[[${moment().format("YYYY-MM-DD")}]] is not created yet.`, | |
| }); | |
| const convoEl = containerEl.createDiv(); | |
| dv.view("System/scripts/Dataview/widget/Convo", { | |
| containerEl: convoEl, | |
| locale: options.locale, | |
| }); |
<%* /**
customJS.Plugins.templater.addTags(tp, ['note/convo']); -%>
| /** CustomJS/Obsidian.js */ | |
| class Obsidian { | |
| vault = { | |
| /** | |
| * Get contents of a file | |
| * @param {string | TFile} file - Path or object of a file | |
| * @param {Boolean} stripYAML - Strip frontmatter if true | |
| * @returns {string} Contents of the file | |
| */ | |
| async getFileContent(fileId, stripYAML = true) { | |
| let tfile; | |
| // fileId is a path | |
| if (typeof fileId == 'string') { | |
| tfile = this.getFile(fileId); | |
| // fileId is TFile | |
| } else if (fileId instanceof obsidian.TFile) { | |
| tfile = fileId; | |
| // Handle error | |
| } else { | |
| customJS.Error.log( | |
| "Cannot get file content.\n" + | |
| "Invalid parameter passed to Obsidian.getFileContent" | |
| ) | |
| return; | |
| } | |
| let fileContent = await app.vault.cachedRead(tfile); | |
| if (stripYAML) { | |
| fileContent = fileContent.replace(/^---.*?\n---\n/s, ""); | |
| } | |
| return fileContent; | |
| }, | |
| }; | |
| frontmatter = { | |
| /** | |
| * Set frontmatter of a file | |
| * | |
| * @param {TFile} file - Target file | |
| * @param {Object.<string, string>} properties - Dictionary of properties | |
| * | |
| * @example | |
| * // Add delay or hook to prevent race condition between Templater and | |
| * // Obsidian API. | |
| * tp.hooks.on_all_templates_executed(async () => { | |
| * customJS.Obsidian.frontmatter.set(tp.config.target_file, { | |
| * 'property1': 'value1', | |
| * 'property2': 'value2', | |
| * }); | |
| * }); | |
| * | |
| * @todo handle exceptions | |
| */ | |
| set(file, properties) { | |
| app.fileManager.processFrontMatter( | |
| file, | |
| (frontmatter) => { | |
| for (const [property, value] of Object.entries(properties)) { | |
| frontmatter[property] = value; | |
| } | |
| } | |
| ); | |
| }, | |
| /** | |
| * Add tags to frontmatter of a file | |
| * @param {TFile} file - Target file | |
| * @param {Array.<string>} tags - Tags to add in frontmatter | |
| * | |
| * @example | |
| * // Add delay or hook to prevent race condition between Templater and | |
| * // Obsidian API. | |
| * tp.hooks.on_all_templates_executed(async () => { | |
| * customJS.Obsidian.frontmatter.add( | |
| * tp.config.target_file, | |
| * ['tag1', 'tag2'] | |
| * ); | |
| * }); | |
| */ | |
| addTags(file, tags) { | |
| app.fileManager.processFrontMatter( | |
| file, | |
| (frontmatter) => { | |
| if (!frontmatter.tags) { | |
| frontmatter.tags = tags; | |
| } else if (Object.prototype.toString.call(frontmatter.tags) === "[object String]") { | |
| frontmatter.tags = [frontmatter.tags, ...tags]; | |
| } else if (Array.isArray(frontmatter.tags)) { | |
| frontmatter.tags = [...frontmatter.tags, ...tags]; | |
| } else { | |
| //TODO: Handle error | |
| } | |
| } | |
| ) | |
| }, | |
| }; | |
| /** | |
| * Renders markdown to a container | |
| * @param {String} markdown - Markdown content | |
| * @param {HTMLElement} containerEl - Container of the rendered markdown | |
| * @param {String} sourcePath - Path used to resolve relative internal links | |
| * @param {obsidian.Component} component - Parent component to manage the | |
| * lifecycle of the rendered child components. | |
| * @param {Boolean} inline - Remove margin if rendered inline | |
| */ | |
| renderMarkdown(markdown, containerEl, sourcePath, component, inline = true) { | |
| if (!containerEl) return; | |
| containerEl.innerHTML = ""; | |
| obsidian.MarkdownRenderer.renderMarkdown(markdown, containerEl, sourcePath, component) | |
| .then(() => { | |
| if (!containerEl || !inline) return; | |
| // Unwrap any created paragraph elements if we are inline. | |
| let paragraph = containerEl.querySelector("p"); | |
| while (paragraph) { | |
| let children = paragraph.childNodes; | |
| paragraph.replaceWith(...Array.from(children)); | |
| paragraph = containerEl.querySelector("p"); | |
| } | |
| }); | |
| } | |
| } |
| /** CustomJS/Plugins.js */ | |
| class Plugins { | |
| /** | |
| * Templater plugin and its API | |
| */ | |
| templater = { | |
| plugin: app.plugins.plugins["templater-obsidian"], | |
| settings: { | |
| // Folder where templates are stored | |
| folder: app.plugins.plugins["templater-obsidian"] | |
| .settings.templates_folder, | |
| }, | |
| /** | |
| * Creates a new file from template | |
| * @param {string} filePath - Path of the new file | |
| * @param {string} templateName - Name of the template | |
| * @returns {TFile} The created new file | |
| */ | |
| async createNewFileFromTemplate(filePath, templateName) { | |
| // Sanitize template path | |
| if (!templateName.endsWith(".md")) { | |
| templateName += ".md"; | |
| } | |
| let templatePath = obsidian.normalizePath( | |
| `${this.settings.folder}/${templateName}` | |
| ); | |
| // Create new file with template as its contents | |
| const newFile = await app.vault.create( | |
| filePath, | |
| await customJS.Obsidian.vault.getFileContent(templatePath, false) | |
| ); | |
| // Parse template with templater | |
| await this.plugin.templater.overwrite_file_commands(newFile); | |
| return newFile; | |
| }, | |
| /** | |
| * Set frontmatter of target file | |
| * @param {Object} tp - Templater object, accessible within templates | |
| * @param {Object<string, string>} properties - Dictionary of properties | |
| */ | |
| setFrontMatter(tp, properties) { | |
| // Prevent race condition between Templater and Obsidian API | |
| tp.hooks.on_all_templates_executed(() => { | |
| customJS.Obsidian.frontmatter.set( | |
| tp.config.target_file, | |
| properties | |
| ); | |
| }); | |
| }, | |
| /** | |
| * Add tags to target file's frontmatter | |
| * @param {Object} tp - Templater object, accessible when replaceing templates | |
| * @param {Array.<string>} tags - Tags to add in frontmatter | |
| */ | |
| addTags(tp, tags) { | |
| // Prevent race condition between Templater and Obsidian API | |
| tp.hooks.on_all_templates_executed(() => { | |
| customJS.Obsidian.frontmatter.addTags( | |
| tp.config.target_file, | |
| tags | |
| ); | |
| }); | |
| }, | |
| }; | |
| } |
<%* /**
customJS.Plugins.templater.setFrontMatter(tp, { 'created': tp.file.creation_date("YYYY-MM-DDTHH:mm:ss"), }); -%>
| /** TaskList/view.css */ | |
| /** | |
| * TaskList widget built upon Dataview plugin | |
| * | |
| * @author ljavuras <[email protected]> | |
| */ | |
| .widget__taskList { | |
| display: inline-flex; | |
| flex-direction: column; | |
| padding: var(--size-4-2) var(--size-4-4); | |
| margin: var(--size-4-2); | |
| border: var(--border-width) solid var(--background-modifier-border); | |
| border-radius: var(--radius-m); | |
| } | |
| .widget__taskList .widget__taskList__title { | |
| margin-block: unset; | |
| padding-inline: var(--size-4-1); | |
| } | |
| .widget__taskList .widget__taskList__content { | |
| margin-block: unset; | |
| } | |
| .widget__taskList ul.contains-task-list { | |
| margin-block: unset; | |
| } | |
| .widget__taskList li.dataview.task-list-item { | |
| margin: 0; | |
| margin-block: unset; | |
| margin-inline: unset; | |
| } | |
| .widget__taskList li.dataview.task-list-item:hover { | |
| box-shadow: none; | |
| } | |
| .widget__taskList li.dataview.task-list-item input { | |
| margin-inline-start: unset; | |
| } |
| /** TaskList/view.js */ | |
| /** | |
| * TaskLisk widget built upon Dataview plugin | |
| * | |
| * @author ljavuras <[email protected]> | |
| */ | |
| /** | |
| * @name input | |
| * @type {Object} | |
| * @property {HTMLElement} containerEl - Container to render within | |
| * @property {Grouping<SListItem>} tasks - Dataview tasks to render | |
| * @property {String} title - Tasklist title | |
| * @property {String} content - Additional content to display | |
| * @property {String} errorMarkdown - Error message in markdown | |
| */ | |
| let containerEl = input.containerEl.createEl("fieldset", { cls: "widget__taskList" }); | |
| // Render within containerEl, creates local DataviewContext | |
| local_dv = app.plugins.plugins['dataview'].localApi( | |
| dv.current().file.path, | |
| dv.component, | |
| containerEl | |
| ); | |
| // Render TaskList | |
| if (input.title) { | |
| local_dv.el("legend", input.title, { cls: "widget__taskList__title" }); | |
| } | |
| if (input.content) { | |
| local_dv.paragraph(input.content, { cls: "widget__taskList__content" }); | |
| } | |
| if (input.tasks) { | |
| local_dv.taskList(input.tasks, false); | |
| } else { | |
| let errorEl = containerEl.createDiv({ cls: "widget__taskList__error" }); | |
| customJS.Obsidian.renderMarkdown( | |
| input.errorMarkdown, | |
| errorEl, | |
| local_dv.current().file.path, | |
| local_dv.component | |
| ); | |
| } |
Copy renderMarkdown() to both Clock/view.js and TaskList/view.js, and replace the function call, if you don't want to mess with CustomJS
Home.md