mirror of
https://github.com/esm7/obsidian-map-view.git
synced 2026-07-22 05:40:27 +00:00
Geojson inline tags and many other improvements
This commit is contained in:
parent
7ff7b3401e
commit
c1e9c97e9f
17 changed files with 290 additions and 101 deletions
14
README.md
14
README.md
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
[](https://www.buymeacoffee.com/esm7)
|
||||
|
||||
TODO:
|
||||
GeoJSON:
|
||||
|
||||
- Inline
|
||||
- File
|
||||
- GPX and other files support
|
||||
- Inline tags
|
||||
- How to change display rules - via name, linkedfrom
|
||||
|
||||
Rewrite display rules section
|
||||
Add overlays (and warn about performance if adding too many)
|
||||
|
||||
## Intro
|
||||
|
||||
This plugin introduces an **interactive map view** for [Obsidian.md](https://obsidian.md/).
|
||||
|
|
@ -690,7 +702,7 @@ And while both plugins are about maps and use Leaflet.js as their visual engine,
|
|||
- Inline location bug on iOS (https://github.com/esm7/obsidian-map-view/issues/301)
|
||||
- Places API (New)
|
||||
- TODO: Fix places API templates
|
||||
- Major performance optimizations to filtering
|
||||
- Major performance optimizations, especially to filtering, preview popups, embedded maps, Map View starts instantly, price is that it loads on startup and keeps the memory (but if you use lots of embedded maps, it's well worth it)
|
||||
- Filtering now reapplies properly when modifying notes
|
||||
- Paths
|
||||
- Badges
|
||||
|
|
|
|||
|
|
@ -33,15 +33,14 @@
|
|||
"postcss-less": "^6.0.0",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"rollup": "^4.28.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-svelte": "^7.2.2",
|
||||
"svelte": "^5.6.2",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"svelte": "^5.34.7",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.1",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import postcss_url from 'postcss-url';
|
|||
import copy from 'rollup-plugin-copy';
|
||||
import image from '@rollup/plugin-image';
|
||||
import svelte from 'rollup-plugin-svelte';
|
||||
import { sveltePreprocess } from 'svelte-preprocess';
|
||||
// Not using sveltePreprocess anymore, seeming not needed: https://github.com/sveltejs/svelte-preprocess?tab=readme-ov-file#when-to-use-it
|
||||
// import { sveltePreprocess } from 'svelte-preprocess';
|
||||
|
||||
const isProd = process.env.BUILD === 'production';
|
||||
|
||||
|
|
@ -35,7 +36,6 @@ export default {
|
|||
plugins: [
|
||||
image(),
|
||||
svelte({
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: {
|
||||
css: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import * as leaflet from 'leaflet';
|
|||
import 'leaflet-extra-markers';
|
||||
import 'leaflet-extra-markers/dist/css/leaflet.extra-markers.min.css';
|
||||
import * as consts from 'src/consts';
|
||||
import * as regex from 'src/regex';
|
||||
import type MapViewPlugin from './main';
|
||||
|
||||
type MarkerId = string;
|
||||
|
||||
|
|
@ -58,6 +60,8 @@ export abstract class BaseGeoLayer {
|
|||
|
||||
/** Get the bounds of the data */
|
||||
abstract getBounds(): leaflet.LatLng[];
|
||||
|
||||
public runDisplayRules(plugin: MapViewPlugin) {}
|
||||
}
|
||||
|
||||
// TODO change class name to LayersMap
|
||||
|
|
@ -95,3 +99,13 @@ export function cacheTagsFromLayers(
|
|||
marker.tags.forEach((tag) => tagsSet.add(tag));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a list of inline tags (`tag:abc tag:bcd`) and add it to layer.tags in the form of ['#abc', '#bcd'].
|
||||
*/
|
||||
export function addTagsToLayer(layer: BaseGeoLayer, tagsString: string) {
|
||||
const tagRegex = regex.INLINE_TAG_IN_NOTE;
|
||||
const tags = tagsString.matchAll(tagRegex);
|
||||
for (const tag of tags)
|
||||
if (tag.groups.tag) layer.tags.push('#' + tag.groups.tag);
|
||||
}
|
||||
|
|
|
|||
36
src/components/ChipsList.svelte
Normal file
36
src/components/ChipsList.svelte
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { getIcon } from 'obsidian';
|
||||
|
||||
let { chips = $bindable() } = $props<{
|
||||
chips: string[];
|
||||
}>();
|
||||
|
||||
function remove(chipName: string) {
|
||||
const index = chips.indexOf(chipName);
|
||||
chips.splice(index, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Class used for Obsidian chips container -->
|
||||
<div class="setting-command-hotkeys">
|
||||
{#each chips as chipName}
|
||||
<span class="setting-hotkey mv-edit-tag-name">
|
||||
{chipName}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<span
|
||||
class="setting-hotkey-icon"
|
||||
onclick={() => {
|
||||
remove(chipName);
|
||||
}}
|
||||
>
|
||||
{@html getIcon('x').outerHTML}
|
||||
</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mv-edit-tag-name {
|
||||
font-family: inherit;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -24,8 +24,8 @@
|
|||
|
||||
async function save() {
|
||||
plugin.settings.displayRules = rulesCopy;
|
||||
plugin.displayRulesCache.build(plugin.settings.displayRules);
|
||||
await plugin.saveSettings();
|
||||
plugin.refreshDisplayRules();
|
||||
plugin.refreshAllMapViews();
|
||||
close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
<script lang="ts">
|
||||
let { label, existingText, close, onOk } = $props<{
|
||||
label: string;
|
||||
existingText: string;
|
||||
close: () => void;
|
||||
onOk: (text: string) => void;
|
||||
}>();
|
||||
import { SimpleInputSuggest } from '../simpleInputSuggest';
|
||||
import { onMount } from 'svelte';
|
||||
import { App } from 'obsidian';
|
||||
|
||||
let { label, description, existingText, close, onOk, suggestions, app } =
|
||||
$props<{
|
||||
label: string;
|
||||
description: string | undefined;
|
||||
existingText: string;
|
||||
close: () => void;
|
||||
onOk: (text: string) => void;
|
||||
suggestions: string[] | undefined;
|
||||
// Required only when using suggestor
|
||||
app: App | undefined;
|
||||
}>();
|
||||
|
||||
let text = $state(existingText);
|
||||
let inputComponent: HTMLInputElement;
|
||||
|
||||
function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
|
|
@ -20,13 +30,37 @@
|
|||
close();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
new SimpleInputSuggest(
|
||||
app,
|
||||
inputComponent,
|
||||
suggestions,
|
||||
(selection: string) => {
|
||||
text = selection;
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mv-text-box-dialog">
|
||||
<form onsubmit={handleSubmit}>
|
||||
<p>{label}</p>
|
||||
<p class="label">
|
||||
{label}
|
||||
{#if description}
|
||||
<br />
|
||||
<span class="description">{description}</span>
|
||||
{/if}
|
||||
</p>
|
||||
<div class="input-container">
|
||||
<input type="text" bind:value={text} onkeydown={handleKeyDown} />
|
||||
<input
|
||||
type="text"
|
||||
bind:this={inputComponent}
|
||||
bind:value={text}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="modal-button-container">
|
||||
|
|
@ -39,10 +73,15 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.mv-text-box-dialog p {
|
||||
.mv-text-box-dialog p.label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mv-text-box-dialog span.description {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.input-container input {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { untrack, onMount } from 'svelte';
|
||||
import { Notice, App, getIcon, TFile, type HeadingCache } from 'obsidian';
|
||||
import { type PluginSettings } from '../settings';
|
||||
import { type ViewSettings, MapContainer } from '../mapContainer';
|
||||
import MapViewPlugin from '../main';
|
||||
import ViewCollapsibleSection from './ViewCollapsibleSection.svelte';
|
||||
import QueryTextField from './QueryTextField.svelte';
|
||||
import { SvelteModal } from 'src/svelte';
|
||||
import TextBoxDialog from './TextBoxDialog.svelte';
|
||||
import { SimpleInputSuggest } from '../simpleInputSuggest';
|
||||
import {
|
||||
type MapState,
|
||||
areStatesEqual,
|
||||
|
|
@ -13,10 +16,10 @@
|
|||
copyState,
|
||||
} from 'src/mapState';
|
||||
import { NewPresetDialog } from 'src/newPresetDialog';
|
||||
import { QuerySuggest } from 'src/query';
|
||||
import * as utils from 'src/utils';
|
||||
import { NoteSelectDialog } from 'src/noteSelectDialog';
|
||||
import { type EditModeTools } from 'src/viewControls';
|
||||
import ChipsList from './ChipsList.svelte';
|
||||
|
||||
let {
|
||||
plugin,
|
||||
|
|
@ -47,6 +50,9 @@
|
|||
let noteToEdit: TFile = $state(null);
|
||||
let noteHeading: string | null = $state(null);
|
||||
let allNoteHeadings: string[] = $state([]);
|
||||
let editTags: string[] = $state([]);
|
||||
let allTags: string[] = $state(utils.getAllTagNames(app, plugin));
|
||||
let addTagInputElement: HTMLInputElement = $state();
|
||||
|
||||
$effect(() => {
|
||||
const considerAutoFit = statesDifferOnlyInQuery(
|
||||
|
|
@ -129,6 +135,21 @@
|
|||
// svelte-ignore state_referenced_locally
|
||||
lastSavedState = mapState;
|
||||
|
||||
onMount(() => {
|
||||
const suggestor = new SimpleInputSuggest(
|
||||
app,
|
||||
addTagInputElement,
|
||||
allTags,
|
||||
(selection: string) => {
|
||||
if (editTags.findIndex((tag) => tag === selection) === -1)
|
||||
editTags.push(selection);
|
||||
suggestor.close();
|
||||
addTagInputElement.value = '';
|
||||
addTagInputElement.blur();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Finds in the presets array a preset that matches the current map state and returns its index, or -1 if none was found
|
||||
function findPresetIndexForCurrentState() {
|
||||
for (const [index, preset] of presets.entries())
|
||||
|
|
@ -502,6 +523,19 @@
|
|||
<option value={heading}>{heading}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<div class="mv-edit-tags">
|
||||
{@html getIcon('tag').outerHTML}
|
||||
<div class="mv-tag-chips">
|
||||
<ChipsList bind:chips={editTags}></ChipsList>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
bind:this={addTagInputElement}
|
||||
class="text-input-inline"
|
||||
placeholder="#tag"
|
||||
style="width: 4em;"
|
||||
/>
|
||||
</div>
|
||||
</ViewCollapsibleSection>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ import {
|
|||
type FrontmatterLinkCache,
|
||||
type ReferenceCache,
|
||||
} from 'obsidian';
|
||||
import * as path from 'path';
|
||||
|
||||
import { BaseGeoLayer, verifyLocation } from 'src/baseGeoLayer';
|
||||
import { BaseGeoLayer, verifyLocation, addTagsToLayer } from 'src/baseGeoLayer';
|
||||
import { type IconOptions } from 'src/markerIcons';
|
||||
import {
|
||||
djb2Hash,
|
||||
|
|
@ -50,6 +51,10 @@ export class FileMarker extends BaseGeoLayer {
|
|||
this.generateId();
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.extraName ?? this.file.basename;
|
||||
}
|
||||
|
||||
get isFrontmatterMarker(): boolean {
|
||||
return !this.fileLine;
|
||||
}
|
||||
|
|
@ -129,6 +134,14 @@ export class FileMarker extends BaseGeoLayer {
|
|||
getBounds(): leaflet.LatLng[] {
|
||||
return [this.location];
|
||||
}
|
||||
|
||||
runDisplayRules(plugin: MapViewPlugin) {
|
||||
this.icon = getIconFromRules(
|
||||
this,
|
||||
plugin.displayRulesCache,
|
||||
plugin.iconFactory,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type FileWithMarkers = {
|
||||
|
|
@ -204,13 +217,7 @@ export async function getMarkersFromFileContent(
|
|||
const marker = new FileMarker(file, location);
|
||||
if (match.groups.name && match.groups.name.length > 0)
|
||||
marker.extraName = match.groups.name;
|
||||
if (match.groups.tags) {
|
||||
// Parse the list of tags
|
||||
const tagRegex = regex.INLINE_TAG_IN_NOTE;
|
||||
const tags = match.groups.tags.matchAll(tagRegex);
|
||||
for (const tag of tags)
|
||||
if (tag.groups.tag) marker.tags.push('#' + tag.groups.tag);
|
||||
}
|
||||
if (match.groups.tags) addTagsToLayer(marker, match.groups.tags);
|
||||
marker.tags = marker.tags.concat(fileTags);
|
||||
marker.fileLocation = match.index;
|
||||
marker.geolocationMatch = match;
|
||||
|
|
@ -511,15 +518,8 @@ export async function buildAndAppendFileMarkers(
|
|||
}
|
||||
}
|
||||
|
||||
// Apply display rules
|
||||
for (const layer of layers) {
|
||||
if (layer instanceof FileMarker) {
|
||||
layer.icon = getIconFromRules(
|
||||
layer,
|
||||
plugin.displayRulesCache,
|
||||
plugin.iconFactory,
|
||||
);
|
||||
}
|
||||
layer.runDisplayRules(plugin);
|
||||
}
|
||||
|
||||
return layers;
|
||||
|
|
@ -545,7 +545,7 @@ export async function moveFileMarker(
|
|||
newLng = consts.LNG_LIMITS[1];
|
||||
}
|
||||
// We will now change the content of the note containing the marker. This will trigger Map View to rebuild
|
||||
// the marker, causing the actual marker object to be replaced
|
||||
// the marker, causing the actual marker object to be replaced.
|
||||
if (marker.isFrontmatterMarker) {
|
||||
await utils.verifyOrAddFrontMatter(
|
||||
app,
|
||||
|
|
@ -560,12 +560,46 @@ export async function moveFileMarker(
|
|||
marker.file,
|
||||
marker.fileLocation,
|
||||
marker.geolocationMatch,
|
||||
newLat,
|
||||
newLng,
|
||||
marker.location,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function renameMarker(
|
||||
marker: FileMarker,
|
||||
settings: PluginSettings,
|
||||
app: App,
|
||||
plugin: MapViewPlugin,
|
||||
) {
|
||||
const dialog = new SvelteModal(TextBoxDialog, app, plugin, settings, {
|
||||
label: 'Select a name for the new marker:',
|
||||
description: marker.isFrontmatterMarker
|
||||
? 'This will rename the file the marker is stored at.'
|
||||
: undefined,
|
||||
existingText: marker.name,
|
||||
onOk: (text: string) => {
|
||||
if (marker.isFrontmatterMarker) {
|
||||
const newPath = path.join(
|
||||
marker.file.parent.name,
|
||||
text + '.' + marker.file.extension,
|
||||
);
|
||||
app.vault.rename(marker.file, newPath);
|
||||
} else if (marker.geolocationMatch?.groups) {
|
||||
utils.updateInlineGeolocation(
|
||||
app,
|
||||
marker.file,
|
||||
marker.fileLocation,
|
||||
marker.geolocationMatch,
|
||||
marker.location,
|
||||
text,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
dialog.open();
|
||||
}
|
||||
|
||||
export async function createMarkerInFile(
|
||||
marker: leaflet.Marker,
|
||||
file: TFile,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { type GeoJSON } from 'geojson';
|
|||
import * as toGeoJson from '@tmcw/togeojson';
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
|
||||
import { BaseGeoLayer, verifyLocation } from 'src/baseGeoLayer';
|
||||
import { BaseGeoLayer, addTagsToLayer } from 'src/baseGeoLayer';
|
||||
import { type IconOptions } from 'src/markerIcons';
|
||||
import {
|
||||
djb2Hash,
|
||||
|
|
@ -79,6 +79,11 @@ export class GeoJsonLayer extends BaseGeoLayer {
|
|||
this.fileLine === other.fileLine
|
||||
);
|
||||
}
|
||||
|
||||
runDisplayRules(plugin: MapViewPlugin) {
|
||||
const [_, pathOptions] = plugin.displayRulesCache.runOn(this);
|
||||
this.pathOptions = pathOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export type FileWithGeoJsons = {
|
||||
|
|
@ -143,6 +148,8 @@ export async function buildGeoJsonLayers(
|
|||
.split('\n').length - 1;
|
||||
// TODO add file block and heading
|
||||
layer.generateId();
|
||||
if (match.groups.tags)
|
||||
addTagsToLayer(layer, match.groups.tags);
|
||||
layers.push(layer);
|
||||
}
|
||||
}
|
||||
|
|
@ -182,8 +189,7 @@ export async function buildGeoJsonLayers(
|
|||
if (settings.debug) console.timeLog('buildGeoJsonLayers');
|
||||
// Calculate display rules
|
||||
for (const layer of layers) {
|
||||
const [_, pathOptions] = plugin.displayRulesCache.runOn(layer);
|
||||
(layer as GeoJsonLayer).pathOptions = pathOptions;
|
||||
layer.runDisplayRules(plugin);
|
||||
}
|
||||
if (settings.debug) console.timeEnd('buildGeoJsonLayers');
|
||||
return layers;
|
||||
|
|
|
|||
|
|
@ -1236,6 +1236,13 @@ export default class MapViewPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
public refreshDisplayRules() {
|
||||
this.displayRulesCache.build(this.settings.displayRules);
|
||||
for (const layer of this.layerCache.layers) {
|
||||
layer.runDisplayRules(this);
|
||||
}
|
||||
}
|
||||
|
||||
public refreshAllMapViews() {
|
||||
for (const mapContainer of this.allMapContainers) {
|
||||
mapContainer.refreshMap();
|
||||
|
|
|
|||
|
|
@ -969,7 +969,7 @@ export class MapContainer {
|
|||
});
|
||||
|
||||
if (this.state.markerLabels && this.state.markerLabels != 'off')
|
||||
newMarker.bindTooltip(marker.extraName ?? marker.file.basename, {
|
||||
newMarker.bindTooltip(marker.name, {
|
||||
permanent: true,
|
||||
direction: this.state.markerLabels,
|
||||
className: 'mv-marker-label',
|
||||
|
|
@ -1045,41 +1045,6 @@ export class MapContainer {
|
|||
}
|
||||
});
|
||||
});
|
||||
newMarker.on('moveend', async (_event: leaflet.LeafletEvent) => {
|
||||
marker.location = newMarker.getLatLng().clone();
|
||||
let newLat = marker.location.lat;
|
||||
// If the user drags the marker too far, the longitude will exceed the threshold, an
|
||||
// exception will be thrown, and the marker will disappear.
|
||||
// If the threshold is exceeded, set the longitude back to the max (back in bounds).
|
||||
// leaflet seems to protect against drags beyond the latitude threshold.
|
||||
let newLng = marker.location.lng;
|
||||
if (newLng < consts.LNG_LIMITS[0]) {
|
||||
newLng = consts.LNG_LIMITS[0];
|
||||
}
|
||||
if (newLng > consts.LNG_LIMITS[1]) {
|
||||
newLng = consts.LNG_LIMITS[1];
|
||||
}
|
||||
// We will now change the content of the note containing the marker. This will trigger Map View to rebuild
|
||||
// the marker, causing the actual marker object to be replaced
|
||||
if (marker.isFrontmatterMarker) {
|
||||
await utils.verifyOrAddFrontMatter(
|
||||
this.app,
|
||||
marker.file,
|
||||
this.settings.frontMatterKey,
|
||||
`${newLat},${newLng}`,
|
||||
false,
|
||||
);
|
||||
} else if (marker.geolocationMatch?.groups) {
|
||||
await utils.updateInlineGeolocation(
|
||||
this.app,
|
||||
marker.file,
|
||||
marker.fileLocation,
|
||||
marker.geolocationMatch,
|
||||
newLat,
|
||||
newLng,
|
||||
);
|
||||
}
|
||||
});
|
||||
newMarker.on(
|
||||
'pm:edit',
|
||||
(e: {
|
||||
|
|
@ -1109,13 +1074,22 @@ export class MapContainer {
|
|||
let mapPopup = new Menu();
|
||||
if (marker instanceof FileMarker) {
|
||||
menus.populateOpenNote(this, marker, mapPopup, this.settings);
|
||||
if (this.state.editMode)
|
||||
menus.populateRename(
|
||||
this,
|
||||
marker,
|
||||
mapPopup,
|
||||
this.settings,
|
||||
this.app,
|
||||
this.plugin,
|
||||
);
|
||||
menus.populateRouting(
|
||||
this,
|
||||
marker.location,
|
||||
mapPopup,
|
||||
this.settings,
|
||||
);
|
||||
const name = marker.extraName ?? marker.file.basename;
|
||||
const name = marker.name;
|
||||
menus.populateOpenInItems(
|
||||
mapPopup,
|
||||
marker.location,
|
||||
|
|
|
|||
20
src/menus.ts
20
src/menus.ts
|
|
@ -12,7 +12,7 @@ import { type MapState } from 'src/mapState';
|
|||
import MapViewPlugin from 'src/main';
|
||||
import { MapContainer } from 'src/mapContainer';
|
||||
import { type PluginSettings } from 'src/settings';
|
||||
import { FileMarker } from 'src/fileMarker';
|
||||
import { FileMarker, renameMarker } from 'src/fileMarker';
|
||||
import { getIconFromOptions } from 'src/markerIcons';
|
||||
import { SvelteModal } from 'src/svelte';
|
||||
import ImportDialog from './components/ImportDialog.svelte';
|
||||
|
|
@ -382,6 +382,24 @@ export function populateOpenNote(
|
|||
});
|
||||
}
|
||||
|
||||
export function populateRename(
|
||||
mapContainer: MapContainer,
|
||||
fileMarker: FileMarker,
|
||||
menu: Menu,
|
||||
settings: PluginSettings,
|
||||
app: App,
|
||||
plugin: MapViewPlugin,
|
||||
) {
|
||||
menu.addItem((item: MenuItem) => {
|
||||
item.setTitle('Rename...');
|
||||
item.setIcon('folder-pen');
|
||||
item.setSection('open-note');
|
||||
item.onClick(async (_evt: MouseEvent) => {
|
||||
renameMarker(fileMarker, settings, app, plugin);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// The MenuItem object in the Obsidian API doesn't let us listen to a middle-click, so we patch around it
|
||||
function addPatchyMiddleClickHandler(
|
||||
item: MenuItem,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
import { Editor, App, SuggestModal, TFile, type Instruction } from 'obsidian';
|
||||
import * as leaflet from 'leaflet';
|
||||
|
||||
import MapViewPlugin from 'src/main';
|
||||
import { type PluginSettings } from 'src/settings';
|
||||
import {
|
||||
getIconFromOptions,
|
||||
createIconElement,
|
||||
type IconOptions,
|
||||
} from 'src/markerIcons';
|
||||
import * as utils from 'src/utils';
|
||||
import * as consts from 'src/consts';
|
||||
|
||||
export class SuggestInfo {
|
||||
file: TFile;
|
||||
|
|
@ -18,13 +10,6 @@ export class SuggestInfo {
|
|||
export class NoteSelectDialog extends SuggestModal<SuggestInfo> {
|
||||
private plugin: MapViewPlugin;
|
||||
private settings: PluginSettings;
|
||||
private lastSearchTime = 0;
|
||||
private lastSearch = '';
|
||||
private lastSearchResults: SuggestInfo[] = [];
|
||||
private includeResults: SuggestInfo[] = [];
|
||||
private hasIcons: boolean = false;
|
||||
|
||||
private dialogAction: NoteSelectDialog;
|
||||
|
||||
// If dialogAction is 'custom', this will launch upon selection
|
||||
public customOnSelect: (
|
||||
|
|
|
|||
|
|
@ -37,4 +37,5 @@ export const FRONT_MATTER_LOCATION_V2 =
|
|||
// location: [32.84577588420059,35.36074429750443]
|
||||
export const FRONT_MATTER_LOCATION =
|
||||
/(?<header>^---.*)(?<loc>location:[ \t]*\[(?<lat>[+-]?([0-9]*[.])?[0-9]+),(?<lng>[+-]?([0-9]*[.])?[0-9]+)\]).*^---/ms;
|
||||
export const INLINE_GEOJSON = /```geojson\n(?<content>[^`]*)```/gu;
|
||||
export const INLINE_GEOJSON =
|
||||
/```geojson\n(?<content>[^`]*)```\s*\n?(?<tags>(tag:[\p{L}\p{N}_\/\-]+(?:[\s,.]+|$))*)/gu;
|
||||
|
|
|
|||
31
src/simpleInputSuggest.ts
Normal file
31
src/simpleInputSuggest.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { App, AbstractInputSuggest } from 'obsidian';
|
||||
|
||||
export class SimpleInputSuggest extends AbstractInputSuggest<string> {
|
||||
private options: string[];
|
||||
private action: (selection: string) => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
textInputEl: HTMLInputElement | HTMLDivElement,
|
||||
options: string[],
|
||||
action: (selection: string) => void,
|
||||
) {
|
||||
super(app, textInputEl);
|
||||
this.options = options;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
protected getSuggestions(query: string) {
|
||||
return this.options.filter((option) =>
|
||||
option.toLowerCase().contains(query.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
|
||||
this.action(value);
|
||||
}
|
||||
|
||||
renderSuggestion(value: string, el: HTMLElement): void {
|
||||
el.setText(value);
|
||||
}
|
||||
}
|
||||
11
src/utils.ts
11
src/utils.ts
|
|
@ -270,23 +270,22 @@ export async function verifyOrAddFrontMatterForInline(
|
|||
* Update the inline geolocation
|
||||
* @param app The Obsidian Editor instance
|
||||
* @param file The TFile containing the location to update
|
||||
* @param newLocation The new location to set
|
||||
* @param geolocationMatch Regex match info related to the inline geolocation
|
||||
* @param newLat The new latitude to set
|
||||
* @param newLng The new longitude to set
|
||||
* @param newLocation - a new location to use
|
||||
* @param newName - a new name to use or null if unchanged
|
||||
*/
|
||||
export async function updateInlineGeolocation(
|
||||
app: App,
|
||||
file: TFile,
|
||||
fileLocation: number,
|
||||
geolocationMatch: RegExpMatchArray,
|
||||
newLat: number,
|
||||
newLng: number,
|
||||
newLocation: leaflet.LatLng,
|
||||
newName: string | null,
|
||||
): Promise<void> {
|
||||
const content = await app.vault.read(file);
|
||||
let groups = geolocationMatch?.groups;
|
||||
if (groups) {
|
||||
const newGeoLocationText = `[${groups.name}](geo:${newLat},${newLng})`;
|
||||
const newGeoLocationText = `[${newName ?? groups.name}](geo:${newLocation.lat},${newLocation.lng})`;
|
||||
// We want to replace just the part containing the coordinates, not optional tags that follow and are
|
||||
// included in geolocationMatch. So we do a re-match without any tags, to know the length of the part
|
||||
// we want to replace.
|
||||
|
|
|
|||
Loading…
Reference in a new issue