You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Desktop: Resolves ##5389: Add support for note list plugins (#8897)
This commit is contained in:
		| @@ -89,6 +89,14 @@ interface DatabaseTables { | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -555,6 +563,4 @@ export const databaseSchema: DatabaseTables = { | ||||
| 		updated_time: { type: 'number' }, | ||||
| 		type_: { type: 'number' }, | ||||
| 	}, | ||||
| }; | ||||
|  | ||||
| export type ItemRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; | ||||
| }; | ||||
							
								
								
									
										170
									
								
								packages/lib/services/noteList/defaultLeftToRightListRenderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								packages/lib/services/noteList/defaultLeftToRightListRenderer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | ||||
| import { _ } from '../../locale'; | ||||
| import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer'; | ||||
| import { ItemFlow, ListRenderer } from '../plugins/api/noteListType'; | ||||
|  | ||||
| interface Props { | ||||
| 	note: { | ||||
| 		id: string; | ||||
| 		title: string; | ||||
| 		is_todo: number; | ||||
| 		todo_completed: number; | ||||
| 		body: string; | ||||
| 	}; | ||||
| 	item: { | ||||
| 		size: { | ||||
| 			width: number; | ||||
| 			height: number; | ||||
| 		}; | ||||
| 		selected: boolean; | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| const defaultLeftToRightItemRenderer: ListRenderer = { | ||||
| 	id: 'detailed', | ||||
|  | ||||
| 	label: async () => _('Detailed'), | ||||
|  | ||||
| 	flow: ItemFlow.LeftToRight, | ||||
|  | ||||
| 	itemSize: { | ||||
| 		width: 150, | ||||
| 		height: 150, | ||||
| 	}, | ||||
|  | ||||
| 	dependencies: [ | ||||
| 		'item.selected', | ||||
| 		'item.size.width', | ||||
| 		'item.size.height', | ||||
| 		'note.body', | ||||
| 		'note.id', | ||||
| 		'note.is_shared', | ||||
| 		'note.is_todo', | ||||
| 		'note.isWatched', | ||||
| 		'note.titleHtml', | ||||
| 		'note.todo_completed', | ||||
| 	], | ||||
|  | ||||
| 	itemCss: // css | ||||
| 		`			 | ||||
| 		&:before { | ||||
| 			content: ''; | ||||
| 			border-bottom: 1px solid var(--joplin-divider-color); | ||||
| 			width: 90%; | ||||
| 			position: absolute; | ||||
| 			bottom: 0; | ||||
| 			left: 5%; | ||||
| 		} | ||||
| 	 | ||||
| 		> .content.-selected { | ||||
| 			background-color: var(--joplin-selected-color); | ||||
| 		} | ||||
|  | ||||
| 		&:hover { | ||||
| 			background-color: var(--joplin-background-color-hover3); | ||||
| 		} | ||||
| 	 | ||||
| 		> .content { | ||||
| 			display: flex; | ||||
| 			box-sizing: border-box; | ||||
| 			position: relative; | ||||
| 			width: 100%; | ||||
| 			padding: 16px; | ||||
| 			align-items: flex-start; | ||||
| 			overflow-y: hidden; | ||||
| 			flex-direction: column; | ||||
| 			user-select: none; | ||||
| 	 | ||||
| 			> .checkbox { | ||||
| 				display: flex; | ||||
| 				align-items: center; | ||||
|  | ||||
| 				> input { | ||||
| 					margin: 0px 10px 1px 0px; | ||||
| 				} | ||||
| 			} | ||||
| 	 | ||||
| 			> .title { | ||||
| 				font-family: var(--joplin-font-family); | ||||
| 				font-size: var(--joplin-font-size); | ||||
| 				color: var(--joplin-color); | ||||
| 				cursor: default; | ||||
| 				flex: 0; | ||||
| 				display: flex; | ||||
| 				align-items: flex-start; | ||||
| 				margin-bottom: 8px; | ||||
|  | ||||
| 				> .checkbox { | ||||
| 					margin: 0 6px 0 0; | ||||
| 				} | ||||
|  | ||||
| 				> .watchedicon { | ||||
| 					display: none; | ||||
| 					padding-right: 4px; | ||||
| 					color: var(--joplin-color); | ||||
| 				} | ||||
|  | ||||
| 				> .titlecontent { | ||||
| 					word-break: break-all; | ||||
| 					overflow: hidden; | ||||
| 					text-overflow: ellipsis; | ||||
| 					text-wrap: nowrap; | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			> .preview { | ||||
| 				overflow-y: hidden; | ||||
| 				font-family: var(--joplin-font-family); | ||||
| 				font-size: var(--joplin-font-size); | ||||
| 				color: var(--joplin-color); | ||||
| 				cursor: default; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .content.-shared { | ||||
| 			> .title { | ||||
| 				color: var(--joplin-color-warn3); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .content.-completed { | ||||
| 			> .title { | ||||
| 				opacity: 0.5; | ||||
| 				text-decoration: line-through; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .content.-watched { | ||||
| 			> .title { | ||||
| 				> .watchedicon { | ||||
| 					display: inline; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	`, | ||||
|  | ||||
| 	itemTemplate: // html | ||||
| 		` | ||||
| 		<div class="content {{#item.selected}}-selected{{/item.selected}} {{#note.is_shared}}-shared{{/note.is_shared}} {{#note.todo_completed}}-completed{{/note.todo_completed}} {{#note.isWatched}}-watched{{/note.isWatched}}"> | ||||
| 			<div style="width: {{titleWidth}}px;" class="title" data-id="{{note.id}}"> | ||||
| 				{{#note.is_todo}} | ||||
| 					<input class="checkbox" data-id="todo-checkbox" type="checkbox" {{#note.todo_completed}}checked="checked"{{/note.todo_completed}}> | ||||
| 				{{/note.is_todo}} | ||||
| 				<i class="watchedicon fa fa-share-square"></i> | ||||
| 				<div class="titlecontent">{{{note.titleHtml}}}</div> | ||||
| 			</div> | ||||
| 			<div class="preview">{{notePreview}}</div> | ||||
| 		</div> | ||||
| 	`, | ||||
|  | ||||
| 	onRenderNote: async (props: Props) => { | ||||
| 		const markupToHtml_ = new MarkupToHtml(); | ||||
|  | ||||
| 		return { | ||||
| 			...props, | ||||
| 			notePreview: markupToHtml_.stripMarkup(MarkupLanguage.Markdown, props.note.body).substring(0, 200), | ||||
| 			titleWidth: props.item.size.width - 32, | ||||
| 		}; | ||||
| 	}, | ||||
| }; | ||||
|  | ||||
| export default defaultLeftToRightItemRenderer; | ||||
							
								
								
									
										139
									
								
								packages/lib/services/noteList/defaultListRenderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								packages/lib/services/noteList/defaultListRenderer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| import { _ } from '../../locale'; | ||||
| import { ItemFlow, ListRenderer } from '../plugins/api/noteListType'; | ||||
|  | ||||
| interface Props { | ||||
| 	note: { | ||||
| 		id: string; | ||||
| 		title: string; | ||||
| 		is_todo: number; | ||||
| 		todo_completed: number; | ||||
| 	}; | ||||
| 	item: { | ||||
| 		size: { | ||||
| 			height: number; | ||||
| 		}; | ||||
| 		selected: boolean; | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| const defaultListRenderer: ListRenderer = { | ||||
| 	id: 'compact', | ||||
|  | ||||
| 	label: async () => _('Compact'), | ||||
|  | ||||
| 	flow: ItemFlow.TopToBottom, | ||||
|  | ||||
| 	itemSize: { | ||||
| 		width: 0, | ||||
| 		height: 34, | ||||
| 	}, | ||||
|  | ||||
| 	dependencies: [ | ||||
| 		'item.selected', | ||||
| 		'item.size.height', | ||||
| 		'note.id', | ||||
| 		'note.is_shared', | ||||
| 		'note.is_todo', | ||||
| 		'note.isWatched', | ||||
| 		'note.titleHtml', | ||||
| 		'note.todo_completed', | ||||
| 	], | ||||
|  | ||||
| 	itemCss: // css | ||||
| 		`	 | ||||
| 		&:before { | ||||
| 			content: ''; | ||||
| 			border-bottom: 1px solid var(--joplin-divider-color); | ||||
| 			width: 90%; | ||||
| 			position: absolute; | ||||
| 			bottom: 0; | ||||
| 			left: 5%; | ||||
| 		} | ||||
| 	 | ||||
| 		> .content.-selected { | ||||
| 			background-color: var(--joplin-selected-color); | ||||
| 		} | ||||
|  | ||||
| 		&:hover { | ||||
| 			background-color: var(--joplin-background-color-hover3); | ||||
| 		} | ||||
| 	 | ||||
| 		> .content { | ||||
| 			display: flex; | ||||
| 			box-sizing: border-box; | ||||
| 			position: relative; | ||||
| 			width: 100%; | ||||
| 			padding-left: 16px; | ||||
| 	 | ||||
| 			> .checkbox { | ||||
| 				display: flex; | ||||
| 				align-items: center; | ||||
|  | ||||
| 				> input { | ||||
| 					margin: 0px 10px 1px 0px; | ||||
| 				} | ||||
| 			} | ||||
| 	 | ||||
| 			> .title { | ||||
| 				font-family: var(--joplin-font-family); | ||||
| 				font-size: var(--joplin-font-size); | ||||
| 				text-decoration: none; | ||||
| 				color: var(--joplin-color); | ||||
| 				cursor: default; | ||||
| 				white-space: nowrap; | ||||
| 				flex: 1 1 0%; | ||||
| 				display: flex; | ||||
| 				align-items: center; | ||||
| 				overflow: hidden; | ||||
|  | ||||
| 				> .watchedicon { | ||||
| 					display: none; | ||||
| 					padding-right: 4px; | ||||
| 					color: var(--joplin-color); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .content.-shared { | ||||
| 			> .title { | ||||
| 				color: var(--joplin-color-warn3); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .content.-completed { | ||||
| 			> .title { | ||||
| 				opacity: 0.5; | ||||
| 				text-decoration: line-through; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		> .content.-watched { | ||||
| 			> .title { | ||||
| 				> .watchedicon { | ||||
| 					display: inline; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	`, | ||||
|  | ||||
| 	itemTemplate: // html | ||||
| 		` | ||||
| 		<div class="content {{#item.selected}}-selected{{/item.selected}} {{#note.is_shared}}-shared{{/note.is_shared}} {{#note.todo_completed}}-completed{{/note.todo_completed}} {{#note.isWatched}}-watched{{/note.isWatched}}"> | ||||
| 			{{#note.is_todo}} | ||||
| 				<div class="checkbox"> | ||||
| 					<input data-id="todo-checkbox" type="checkbox" {{#note.todo_completed}}checked="checked"{{/note.todo_completed}}> | ||||
| 				</div> | ||||
| 			{{/note.is_todo}}	 | ||||
| 			<div class="title" data-id="{{note.id}}"> | ||||
| 				<i class="watchedicon fa fa-share-square"></i> | ||||
| 				<span>{{{note.titleHtml}}}</span> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	`, | ||||
|  | ||||
| 	onRenderNote: async (props: Props) => { | ||||
| 		return props; | ||||
| 	}, | ||||
| }; | ||||
|  | ||||
| export default defaultListRenderer; | ||||
							
								
								
									
										30
									
								
								packages/lib/services/noteList/renderers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								packages/lib/services/noteList/renderers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { ListRenderer } from '../plugins/api/noteListType'; | ||||
| // import defaultLeftToRightItemRenderer from '../noteList/defaultLeftToRightListRenderer'; | ||||
| import defaultListRenderer from '../noteList/defaultListRenderer'; | ||||
| import { Store } from 'redux'; | ||||
|  | ||||
| const renderers_: ListRenderer[] = [ | ||||
| 	defaultListRenderer, | ||||
| 	// defaultLeftToRightItemRenderer, | ||||
| ]; | ||||
|  | ||||
| export const getListRendererIds = () => { | ||||
| 	return renderers_.map(r => r.id); | ||||
| }; | ||||
|  | ||||
| export const getDefaultListRenderer = () => { | ||||
| 	return renderers_[0]; | ||||
| }; | ||||
|  | ||||
| export const getListRendererById = (id: string) => { | ||||
| 	return renderers_.find(r => r.id === id); | ||||
| }; | ||||
|  | ||||
| export const registerRenderer = async (store: Store, renderer: ListRenderer) => { | ||||
| 	renderers_.push(renderer); | ||||
|  | ||||
| 	store.dispatch({ | ||||
| 		type: 'NOTE_LIST_RENDERER_ADD', | ||||
| 		value: renderer.id, | ||||
| 	}); | ||||
| }; | ||||
| @@ -6,6 +6,7 @@ import JoplinViewsMenuItems from './JoplinViewsMenuItems'; | ||||
| import JoplinViewsMenus from './JoplinViewsMenus'; | ||||
| import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons'; | ||||
| import JoplinViewsPanels from './JoplinViewsPanels'; | ||||
| import JoplinViewsNoteList from './JoplinViewsNoteList'; | ||||
|  | ||||
| /** | ||||
|  * This namespace provides access to view-related services. | ||||
| @@ -18,11 +19,12 @@ export default class JoplinViews { | ||||
| 	private store: any; | ||||
| 	private plugin: Plugin; | ||||
|  | ||||
| 	private dialogs_: JoplinViewsDialogs = null; | ||||
| 	private panels_: JoplinViewsPanels = null; | ||||
| 	private menuItems_: JoplinViewsMenuItems = null; | ||||
| 	private menus_: JoplinViewsMenus = null; | ||||
| 	private toolbarButtons_: JoplinViewsToolbarButtons = null; | ||||
| 	private dialogs_: JoplinViewsDialogs = null; | ||||
| 	private noteList_: JoplinViewsNoteList = null; | ||||
| 	private implementation_: any = null; | ||||
|  | ||||
| 	public constructor(implementation: any, plugin: Plugin, store: any) { | ||||
| @@ -31,29 +33,34 @@ export default class JoplinViews { | ||||
| 		this.implementation_ = implementation; | ||||
| 	} | ||||
|  | ||||
| 	public get dialogs(): JoplinViewsDialogs { | ||||
| 	public get dialogs() { | ||||
| 		if (!this.dialogs_) this.dialogs_ = new JoplinViewsDialogs(this.implementation_.dialogs, this.plugin, this.store); | ||||
| 		return this.dialogs_; | ||||
| 	} | ||||
|  | ||||
| 	public get panels(): JoplinViewsPanels { | ||||
| 	public get panels() { | ||||
| 		if (!this.panels_) this.panels_ = new JoplinViewsPanels(this.plugin, this.store); | ||||
| 		return this.panels_; | ||||
| 	} | ||||
|  | ||||
| 	public get menuItems(): JoplinViewsMenuItems { | ||||
| 	public get menuItems() { | ||||
| 		if (!this.menuItems_) this.menuItems_ = new JoplinViewsMenuItems(this.plugin, this.store); | ||||
| 		return this.menuItems_; | ||||
| 	} | ||||
|  | ||||
| 	public get menus(): JoplinViewsMenus { | ||||
| 	public get menus() { | ||||
| 		if (!this.menus_) this.menus_ = new JoplinViewsMenus(this.plugin, this.store); | ||||
| 		return this.menus_; | ||||
| 	} | ||||
|  | ||||
| 	public get toolbarButtons(): JoplinViewsToolbarButtons { | ||||
| 	public get toolbarButtons() { | ||||
| 		if (!this.toolbarButtons_) this.toolbarButtons_ = new JoplinViewsToolbarButtons(this.plugin, this.store); | ||||
| 		return this.toolbarButtons_; | ||||
| 	} | ||||
|  | ||||
| 	public get noteList() { | ||||
| 		if (!this.noteList_) this.noteList_ = new JoplinViewsNoteList(this.plugin, this.store); | ||||
| 		return this.noteList_; | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										40
									
								
								packages/lib/services/plugins/api/JoplinViewsNoteList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								packages/lib/services/plugins/api/JoplinViewsNoteList.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| /* eslint-disable multiline-comment-style */ | ||||
|  | ||||
| import { Store } from 'redux'; | ||||
| import { registerRenderer } from '../../noteList/renderers'; | ||||
| import Plugin from '../Plugin'; | ||||
| import { ListRenderer } from './noteListType'; | ||||
|  | ||||
| /** | ||||
|  * This API allows you to customise how each note in the note list is rendered. | ||||
|  * The renderer you implement follows a unidirectional data flow. | ||||
|  * | ||||
|  * The app provides the required dependencies whenever a note is updated - you | ||||
|  * process these dependencies, and return some props, which are then passed to | ||||
|  * your template and rendered. See [[[ListRenderer]]] for a detailed description | ||||
|  * of each property of the renderer. | ||||
|  * | ||||
|  * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/note_list_renderer) | ||||
|  * | ||||
|  * The default list renderer is implemented using the same API, so it worth checking it too: | ||||
|  * | ||||
|  * [Default list renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultListRenderer.ts) | ||||
|  */ | ||||
| export default class JoplinViewsNoteList { | ||||
|  | ||||
| 	private plugin_: Plugin; | ||||
| 	private store_: Store; | ||||
|  | ||||
| 	public constructor(plugin: Plugin, store: Store) { | ||||
| 		this.plugin_ = plugin; | ||||
| 		this.store_ = store; | ||||
| 	} | ||||
|  | ||||
| 	public async registerRenderer(renderer: ListRenderer) { | ||||
| 		await registerRenderer(this.store_, { | ||||
| 			...renderer, | ||||
| 			id: `${this.plugin_.id}:${renderer.id}`, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										159
									
								
								packages/lib/services/plugins/api/noteListType.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								packages/lib/services/plugins/api/noteListType.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| import { Size } from './types'; | ||||
|  | ||||
| // AUTO-GENERATED by generate-database-type | ||||
| type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; | ||||
| // AUTO-GENERATED by generate-database-type | ||||
|  | ||||
| export enum ItemFlow { | ||||
| 	TopToBottom = 'topToBottom', | ||||
| 	LeftToRight = 'leftToRight', | ||||
| } | ||||
|  | ||||
| export type RenderNoteView = Record<string, any>; | ||||
|  | ||||
| export interface OnChangeEvent { | ||||
| 	elementId: string; | ||||
| 	value: any; | ||||
| 	noteId: string; | ||||
| } | ||||
|  | ||||
| export type OnRenderNoteHandler = (props: any)=> Promise<RenderNoteView>; | ||||
| export type OnChangeHandler = (event: OnChangeEvent)=> Promise<void>; | ||||
|  | ||||
| // Most of these are the built-in note properties, such as `note.title`, | ||||
| // `note.todo_completed`, etc. | ||||
| // | ||||
| // Additionally, the `item.*` properties are specific to the rendered item. The | ||||
| // most important being `item.selected`, which you can use to display the | ||||
| // selected note in a different way. | ||||
| // | ||||
| // Finally some special properties are provided to make it easier to render | ||||
| // notes. In particular, if possible prefer `note.titleHtml` to `note.title` | ||||
| // since some important processing has already been done on the string, such as | ||||
| // handling the search highlighter and escaping. Since it's HTML and already | ||||
| // escaped you would insert it using `{{{titleHtml}}}` (triple-mustache syntax, | ||||
| // which disables escaping). | ||||
| // | ||||
| // `notes.tag` gives you the list of tags associated with the note. | ||||
| // | ||||
| // `note.isWatched` tells you if the note is currently opened in an external | ||||
| // editor. In which case you would generally display some indicator. | ||||
| export type ListRendererDepependency = | ||||
| 	ListRendererDatabaseDependency | | ||||
| 	'item.size.width' | | ||||
| 	'item.size.height' | | ||||
| 	'item.selected' | | ||||
| 	'note.titleHtml' | | ||||
| 	'note.isWatched' | | ||||
| 	'note.tags'; | ||||
|  | ||||
| export interface ListRenderer { | ||||
| 	// It must be unique to your plugin. | ||||
| 	id: string; | ||||
|  | ||||
| 	// Can be top to bottom or left to right. Left to right gives you more | ||||
| 	// option to set the size of the items since you set both its width and | ||||
| 	// height. | ||||
| 	flow: ItemFlow; | ||||
|  | ||||
| 	// The size of each item must be specified in advance for performance | ||||
| 	// reasons, and cannot be changed afterwards. If the item flow is top to | ||||
| 	// bottom, you only need to specificy the item height (the width will be | ||||
| 	// ignored). | ||||
| 	itemSize: Size; | ||||
|  | ||||
| 	// The CSS is relative to the list item container. What will appear in the | ||||
| 	// page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use | ||||
| 	// child combinator with guarantee it will only apply to your own items. In | ||||
| 	// this example, the styling will apply to `.note-list-item > .content`: | ||||
| 	// | ||||
| 	// ```css | ||||
| 	// > .content { | ||||
| 	//      padding: 10px; | ||||
| 	// } | ||||
| 	// ``` | ||||
| 	// | ||||
| 	// In order to get syntax highlighting working here, it's recommended | ||||
| 	// installing an editor extension such as [es6-string-html VSCode | ||||
| 	// extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) | ||||
| 	itemCss?: string; | ||||
|  | ||||
| 	// List the dependencies that your plugin needs to render the note list | ||||
| 	// items. Only these will be passed to your `onRenderNote` handler. Ensure | ||||
| 	// that you do not add more than what you need since there is a performance | ||||
| 	// penalty for each property. | ||||
| 	dependencies: ListRendererDepependency[]; | ||||
|  | ||||
| 	// This is the HTML template that will be used to render the note list item. | ||||
| 	// This is a [Mustache template](https://github.com/janl/mustache.js) and it | ||||
| 	// will receive the variable you return from `onRenderNote` as tags. For | ||||
| 	// example, if you return a property named `formattedDate` from | ||||
| 	// `onRenderNote`, you can insert it in the template using `Created date: | ||||
| 	// {{formattedDate}}`. | ||||
| 	// | ||||
| 	// In order to get syntax highlighting working here, it's recommended | ||||
| 	// installing an editor extension such as [es6-string-html VSCode | ||||
| 	// extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) | ||||
| 	itemTemplate: string; | ||||
|  | ||||
| 	// This user-facing text is used for example in the View menu, so that your | ||||
| 	// renderer can be selected. | ||||
| 	label: ()=> Promise<string>; | ||||
|  | ||||
| 	// This is where most of the real-time processing will happen. When a note | ||||
| 	// is rendered for the first time and every time it changes, this handler | ||||
| 	// receives the properties specified in the `dependencies` property. You can | ||||
| 	// then process them, load any additional data you need, and once done you | ||||
| 	// need to return the properties that are needed in the `itemTemplate` HTML. | ||||
| 	// Again, to use the formatted date example, you could have such a renderer: | ||||
| 	// | ||||
| 	// ```typescript | ||||
| 	// dependencies: [ | ||||
| 	//      'note.title', | ||||
| 	//      'note.created_time', | ||||
| 	// ], | ||||
| 	// | ||||
| 	// itemTemplate: // html | ||||
| 	//      ` | ||||
| 	//      <div> | ||||
| 	//          Title: {{note.title}}<br/> | ||||
| 	//          Date: {{formattedDate}} | ||||
| 	//      </div> | ||||
| 	// `, | ||||
| 	// | ||||
| 	// onRenderNote: async (props: any) => { | ||||
| 	//      const formattedDate = dayjs(props.note.created_time).format(); | ||||
| 	//      return { | ||||
| 	//          // Also return the props, so that note.title is available from the | ||||
| 	//          // template | ||||
| 	//          ...props, | ||||
| 	//          formattedDate, | ||||
| 	//      } | ||||
| 	// }, | ||||
| 	// ``` | ||||
| 	onRenderNote: OnRenderNoteHandler; | ||||
|  | ||||
| 	// This handler allows adding some interacivity to the note renderer - | ||||
| 	// whenever an input element within the item is changed (for example, when a | ||||
| 	// checkbox is clicked, or a text input is changed), this `onChange` handler | ||||
| 	// is going to be called. | ||||
| 	// | ||||
| 	// You can inspect `event.elementId` to know which element had some changes, | ||||
| 	// and `event.value` to know the new value. `event.noteId` also tells you | ||||
| 	// what note is affected, so that you can potentially apply changes to it. | ||||
| 	// | ||||
| 	// You specify the element ID, by setting a `data-id` attribute on the | ||||
| 	// input. | ||||
| 	// | ||||
| 	// For example, if you have such a template: | ||||
| 	// | ||||
| 	// ```html | ||||
| 	// <div> | ||||
| 	//      <input type="text" value="{{note.title}}" data-id="noteTitleInput"/> | ||||
| 	// </div> | ||||
| 	// ``` | ||||
| 	// | ||||
| 	// The event handler will receive an event with `elementId` set to | ||||
| 	// `noteTitleInput`. | ||||
| 	onChange?: OnChangeHandler; | ||||
| } | ||||
| @@ -359,6 +359,11 @@ export interface DialogResult { | ||||
| 	formData?: any; | ||||
| } | ||||
|  | ||||
| export interface Size { | ||||
| 	width?: number; | ||||
| 	height?: number; | ||||
| } | ||||
|  | ||||
| export interface Rectangle { | ||||
| 	x?: number; | ||||
| 	y?: number; | ||||
|   | ||||
| @@ -44,6 +44,7 @@ export interface PluginHtmlContents { | ||||
| export interface State { | ||||
| 	plugins: PluginStates; | ||||
| 	pluginHtmlContents: PluginHtmlContents; | ||||
| 	allPluginsStarted: boolean; | ||||
| } | ||||
|  | ||||
| export const stateRootKey = 'pluginService'; | ||||
| @@ -51,6 +52,7 @@ export const stateRootKey = 'pluginService'; | ||||
| export const defaultState: State = { | ||||
| 	plugins: {}, | ||||
| 	pluginHtmlContents: {}, | ||||
| 	allPluginsStarted: false, | ||||
| }; | ||||
|  | ||||
| export const utils = { | ||||
| @@ -162,6 +164,11 @@ const reducer = (draftRoot: Draft<any>, action: any) => { | ||||
| 			(draft.plugins[action.pluginId].views[action.id] as any)[action.name].push(action.value); | ||||
| 			break; | ||||
|  | ||||
| 		case 'PLUGIN_All_STARTED_SET': | ||||
|  | ||||
| 			draft.allPluginsStarted = action.value; | ||||
| 			break; | ||||
|  | ||||
| 		case 'PLUGIN_CONTENT_SCRIPTS_ADD': { | ||||
|  | ||||
| 			const type = action.contentScript.type; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user