```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.valye = 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()
```
Preview:
downloadDownload PNG
downloadDownload JPEG
downloadDownload SVG
Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!
Click to optimize width for Twitter