touch & sort r5
Wed Jul 30 2025 03:39:43 GMT+0000 (Coordinated Universal Time)
Saved by @touchSort
```dataviewjs class PersistentData { static #constructKey = Symbol() static #instance static _dataFilePath static _defaultData _dataFilePath _defaultData _data constructor(constructKey, dataFilePath, defaultData) { if (constructKey !== PersistentData.#constructKey) throw new Error('constructor is private') this._dataFilePath = dataFilePath this._defaultData = defaultData } static setConfig(dataFilePath, dafaultData){ this._dataFilePath = dataFilePath this._defaultData = dafaultData } static async getInstance(){ if (!this.#instance){ this.#instance = new PersistentData( this.#constructKey, this._dataFilePath, this._defaultData ) await this.#instance.initialize() } return this.#instance } async initialize(){ const exists = await app.vault.adapter.exists(this._dataFilePath) if (exists) { const str = await app.vault.adapter.read(this._dataFilePath) const data = JSON.parse(str) // 保存されているデータとdefaultDataのキーが不一致の場合、データの仕様が変わったと判断し、保存データをdefaultDataにリセットする if (this._haveSameKeys(data, this._defaultData)){ this._data = data return } } this._data = this._defaultData await this.saveData() } getData(key){ return this._data[key] } async setData(key, value){ this._data[key] = value await this.saveData() } async saveData(){ await app.vault.adapter.write(this._dataFilePath, this._toJSON(this._data)) } _haveSameKeys(obj1, obj2) { const keys1 = Object.keys(obj1).sort() const keys2 = Object.keys(obj2).sort() return keys1.length === keys2.length && keys1.every((key, index) => key === keys2[index]) } _toJSON(obj){ return JSON.stringify(obj, null, 2) } } class DomUtils { static #registry = new Map() static setRegistry(key, element){ if (this.#registry.has(key)) throw new Error(`Element with key "${key}" is already registered`) if (!(element instanceof HTMLElement)) throw new Error('Invalid element') this.#registry.set(key, element) } static getRegistry(key){ return this.#registry.get(key) || null } static createElement(tagName, attributes = {}){ const elm = document.createElement(tagName) if (attributes.container instanceof HTMLElement) attributes.container.appendChild(elm) if (attributes.data){ Object.entries(attributes.data).forEach(([key, value]) => { elm.setAttribute(`data-${key}`, value) }) } if (attributes.registryKey) this.setRegistry(attributes.registryKey, elm) if (attributes.onClick) elm.addEventListener('click', attributes.onClick) if (attributes.onChange) elm.addEventListener('change', attributes.onChange) if (attributes.onInput) elm.addEventListener('input', attributes.onInput) return elm } static createSelectElement(options = [], defaultValue = null, attributes = {}){ const elm = this.createElement('select', attributes) const optionElms = this.createSelectOptions(options, defaultValue) elm.appendChild(optionElms) return elm } static createSelectOptions(options = [], defaultValue = null){ const fragment = document.createDocumentFragment() options.forEach(option => { const optionElm = document.createElement('option') optionElm.value = option.value optionElm.textContent = option.text if (option.value === defaultValue) optionElm.selected = true fragment.appendChild(optionElm) }) return fragment } static replaceSelectOptions(selectElement, options, defaultValue = null){ const optionElms = this.createSelectOptions(options, defaultValue) selectElement.replaceChildren(optionElms) } static createInputElement(type = 'text', value = '', attributes = {}){ const elm = this.createElement('input', attributes) elm.type = type elm.value = value if (attributes.placeholder) elm.placeholder = attributes.placeholder return elm } static createAnchorElement(href = '#', text = '', attributes = {}){ const elm = this.createElement('a', attributes) elm.href = href elm.textContent = text if (attributes.target) elm.target = attributes.target return elm } } class ViewUtils { static clearScreen(){ dv.container.replaceChildren() } static createSelectDays(){ DomUtils.createSelectElement( config.selectDaysOptions.map(v => ({value:v, text:`${v}日`})), (v => { return config.selectDaysOptions.includes(v) ? v : config.persistentData.defaultData.selectDaysValue })(persistentData.getData('selectDaysValue')), { container: dv.container, registryKey: 'selectDays', onChange: async (e) => { persistentData.setData('selectDaysValue', e.target.value), ViewUtils.updateList() } } ) } static createSelectFolder(){ DomUtils.createSelectElement( [], null, { container: dv.container, registryKey: 'selectFolder', onChange: async (e) => { persistentData.setData('currentFolder', e.target.value) ViewUtils.updateList() } } ) } static createInputSearch(){ DomUtils.createInputElement( 'text', persistentData.getData('searchString'), { placeholder: '検索', container: dv.container, registryKey: 'searchInput', onInput: debounce(async (e) => { persistentData.setData('searchString', e.target.value.trim()) ViewUtils.updateList() }, 1000) } ) } static createListContainer(){ DomUtils.createElement( 'div', { container: dv.container, registryKey: 'listContainer', onClick: (e) => { if (e.target.tagName === 'A') app.workspace.openLinkText(e.target.dataset.url, '', true) } } ) } static async updateList() { const daysBack = persistentData.getData('selectDaysValue') let currentFolder = persistentData.getData('currentFolder') const searchString = persistentData.getData('searchString') const now = dv.date('now') const startDate = now.minus({days: parseInt(daysBack) - 1}).startOf('day') let pages = dv.pages() .where(p => { const mtime = dv.date(p.file.mtime) return mtime >= startDate && mtime <= now }) if (searchString) pages = pages.where(p => p.file.name.toLowerCase().includes(searchString.toLowerCase())) const folderCounts = {} pages.forEach(page => { const folder = page.file.folder || '' folderCounts[folder] = (folderCounts[folder] || 0) + 1 }) const folders = Array.from(pages.file.folder.distinct()).sort() folders.unshift('すべてのフォルダ') if (!folders.includes(currentFolder)){ currentFolder = 'すべてのフォルダ' persistentData.setData('currentFolder', currentFolder) } DomUtils.replaceSelectOptions( DomUtils.getRegistry('selectFolder'), folders.map(folder => { let count, text if (folder === 'すべてのフォルダ'){ count = pages.length text = `${folder} (${count})` } else { count = folderCounts[folder] text = `/${folder} (${count})` } return {value:folder, text:text} }), currentFolder ) if (currentFolder !== 'すべてのフォルダ') pages = pages.where(p => p.file.folder === currentFolder) const ul = DomUtils.createElement('ul') pages .sort(p => p.file.mtime, 'desc') .forEach(page => { const li = DomUtils.createElement('li') DomUtils.createAnchorElement('#', page.file.name, {container: li, data:{url:page.file.name}}) ul.appendChild(li) }) DomUtils.getRegistry('listContainer').replaceChildren(ul) } } function debounce(func, wait) { let timeout = null return function(...args) { if (timeout) clearTimeout(timeout) timeout = setTimeout(() => { func(...args) timeout = null }, wait) } } const config = { persistentData: { // 永続データ保存ファイルのVault内パス // 親ディレクトリは前もって作成しておく dataFilePath: '/Scripts/Dataview/PersistentData/touchSort.json', // 永続データのデフォルト値 defaultData: {selectDaysValue: '10', currentFolder: 'すべてのフォルダ', searchString: ''} }, // 日数の選択肢 selectDaysOptions: ['1', '2', '3', '5', '7', '10', '14', '20', '30', '60', '90', '120'] } PersistentData.setConfig( config.persistentData.dataFilePath, config.persistentData.defaultData ) const persistentData = await PersistentData.getInstance() // 稀に古い要素が残るので明示的に画面をクリアする ViewUtils.clearScreen() ViewUtils.createSelectDays() ViewUtils.createSelectFolder() ViewUtils.createInputSearch() ViewUtils.createListContainer() ViewUtils.updateList() ```
Comments