mirror of
https://github.com/nymbo/Copy-Highlighter.git
synced 2026-07-22 06:56:52 +00:00
Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50af0005a0 | ||
|
|
22b14c40f5 | ||
|
|
d8a5d19576 | ||
|
|
abb1cd956a | ||
|
|
1fad04603b | ||
|
|
a2fbbeae54 | ||
|
|
15fa41c5e3 | ||
|
|
9b61a78f19 | ||
|
|
6e551e51b5 | ||
|
|
86600490b1 |
11 changed files with 491 additions and 80 deletions
52
.github/workflows/release.yml
vendored
Normal file
52
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --force
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Generate artifact attestations
|
||||
uses: actions/attest@v4
|
||||
with:
|
||||
subject-path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
|
||||
- name: Create GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release create "${GITHUB_REF_NAME}" main.js manifest.json styles.css \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--title "${GITHUB_REF_NAME}" \
|
||||
--notes "Release ${GITHUB_REF_NAME}" \
|
||||
--verify-tag
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@ main.js
|
|||
*.map
|
||||
.obsidian/
|
||||
.DS_Store
|
||||
data.json
|
||||
|
|
|
|||
46
README.md
46
README.md
|
|
@ -1,6 +1,36 @@
|
|||
# Copy Flasher
|
||||
# Copy Highlighter
|
||||
|
||||
Copy Flasher is an Obsidian plugin that briefly highlights copied text so you get visual confirmation that the copy action worked.
|
||||
Copy Highlighter highlights text when you copy it in Obsidian, giving a clear visual confirmation that the copy action worked.
|
||||
|
||||
## Features
|
||||
|
||||
- Highlights copied editor text immediately when you press `Ctrl+C` or `Cmd+C`.
|
||||
- Keeps the highlight visible while the copy shortcut is held.
|
||||
- Supports copied text in reading view and other rendered Obsidian content.
|
||||
- Includes settings for color, opacity, corner radius, rendered-text highlighting, and fallback timing.
|
||||
- Works without external services, network requests, telemetry, or file-system access.
|
||||
|
||||
## Usage
|
||||
|
||||
Select text in Obsidian and copy it. The selected text is highlighted while the copy shortcut is held, then returns to normal when the keys are released.
|
||||
|
||||
For mouse or menu-based copy actions, Copy Highlighter uses a short configurable fallback duration because there is no keyboard release event to observe.
|
||||
|
||||
## Settings
|
||||
|
||||
Open **Settings -> Community plugins -> Copy Highlighter** to customize:
|
||||
|
||||
- Highlight color
|
||||
- Editor opacity
|
||||
- Rendered text opacity
|
||||
- Corner radius
|
||||
- Whether rendered Markdown text is highlighted
|
||||
- Mouse/menu copy duration
|
||||
- Lost-keyup fallback duration
|
||||
|
||||
## Privacy and permissions
|
||||
|
||||
Copy Highlighter runs entirely inside Obsidian. It does not collect data, send network requests, read your clipboard contents, or access files directly.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
@ -22,19 +52,13 @@ Watch for changes during development:
|
|||
npm run dev
|
||||
```
|
||||
|
||||
For local testing, this repository can be linked into a development vault at:
|
||||
|
||||
```text
|
||||
P:\Code Repos\Obs-Dev-Vault\Obs-Dev-Vault\.obsidian\plugins\copy-flasher
|
||||
```
|
||||
|
||||
Obsidian loads community plugins from `.obsidian/plugins/<plugin-id>`. The folder name should match `manifest.json`'s `id`, and Obsidian expects the built `main.js`, `manifest.json`, and optional `styles.css` at that folder root.
|
||||
|
||||
After changing `manifest.json`, restart Obsidian so it re-reads the plugin metadata. After changing TypeScript, rebuild and reload the plugin, or use a hot-reload plugin in your development vault.
|
||||
After changing `manifest.json`, restart Obsidian so it re-reads the plugin metadata. After changing TypeScript, rebuild and reload the plugin.
|
||||
|
||||
## Release Files
|
||||
## Release files
|
||||
|
||||
Community plugin releases typically include:
|
||||
Each GitHub release should include these assets:
|
||||
|
||||
- `main.js`
|
||||
- `manifest.json`
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export default tseslint.config(
|
|||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
activeDocument: "readonly",
|
||||
activeWindow: "readonly",
|
||||
},
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"id": "copy-flasher",
|
||||
"name": "Copy Flasher",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Briefly highlights text copied inside Obsidian.",
|
||||
"id": "copy-highlighter",
|
||||
"name": "Copy Highlighter",
|
||||
"version": "0.2.4",
|
||||
"minAppVersion": "1.0.0",
|
||||
"description": "Highlights copied text.",
|
||||
"author": "Nymbo",
|
||||
"authorUrl": "https://github.com/Nymbo",
|
||||
"isDesktopOnly": false
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "copy-flasher",
|
||||
"version": "0.1.0",
|
||||
"name": "copy-highlighter",
|
||||
"version": "0.2.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "copy-flasher",
|
||||
"version": "0.1.0",
|
||||
"name": "copy-highlighter",
|
||||
"version": "0.2.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.6.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "copy-flasher",
|
||||
"version": "0.1.0",
|
||||
"description": "Briefly highlights text copied inside Obsidian.",
|
||||
"name": "copy-highlighter",
|
||||
"version": "0.2.4",
|
||||
"description": "Highlights copied text.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
399
src/main.ts
399
src/main.ts
|
|
@ -1,20 +1,43 @@
|
|||
import { Plugin } from "obsidian";
|
||||
import { App, Plugin, PluginSettingTab, Setting } from "obsidian";
|
||||
import { RangeSetBuilder, StateEffect, StateField } from "@codemirror/state";
|
||||
import { Decoration, DecorationSet, EditorView, ViewPlugin } from "@codemirror/view";
|
||||
|
||||
const FLASH_DURATION_MS = 650;
|
||||
const LOST_KEYUP_FALLBACK_MS = 5000;
|
||||
const NON_KEYBOARD_COPY_FALLBACK_MS = 650;
|
||||
const MAX_DOM_FLASH_RECTS = 80;
|
||||
|
||||
interface CopyHighlighterSettings {
|
||||
highlightColor: string;
|
||||
editorOpacity: number;
|
||||
renderedOpacity: number;
|
||||
borderRadius: number;
|
||||
enableRenderedMarkdownHighlight: boolean;
|
||||
nonKeyboardCopyDurationMs: number;
|
||||
lostKeyupFallbackMs: number;
|
||||
}
|
||||
|
||||
type FlashRange = {
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
|
||||
type ClearHighlight = () => void;
|
||||
|
||||
const DEFAULT_SETTINGS: CopyHighlighterSettings = {
|
||||
highlightColor: "#ffd54f",
|
||||
editorOpacity: 0.5,
|
||||
renderedOpacity: 0.46,
|
||||
borderRadius: 3,
|
||||
enableRenderedMarkdownHighlight: true,
|
||||
nonKeyboardCopyDurationMs: NON_KEYBOARD_COPY_FALLBACK_MS,
|
||||
lostKeyupFallbackMs: LOST_KEYUP_FALLBACK_MS,
|
||||
};
|
||||
|
||||
const addFlash = StateEffect.define<FlashRange[]>();
|
||||
const clearFlash = StateEffect.define<void>();
|
||||
|
||||
const flashMark = Decoration.mark({
|
||||
class: "copy-flasher-editor-highlight",
|
||||
class: "copy-highlighter-editor-highlight",
|
||||
});
|
||||
|
||||
const flashField = StateField.define<DecorationSet>({
|
||||
|
|
@ -47,15 +70,103 @@ const flashField = StateField.define<DecorationSet>({
|
|||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
|
||||
function copyFlashExtension(durationMs: number) {
|
||||
class HighlightSession {
|
||||
private clearers = new Set<ClearHighlight>();
|
||||
private ctrlOrMetaDown = false;
|
||||
private cDown = false;
|
||||
private fallbackTimer: number | null = null;
|
||||
|
||||
constructor(private getSettings: () => CopyHighlighterSettings) {}
|
||||
|
||||
register(clearHighlight: ClearHighlight) {
|
||||
this.clearers.add(clearHighlight);
|
||||
|
||||
return () => {
|
||||
this.clearers.delete(clearHighlight);
|
||||
};
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "Control" || event.key === "Meta") {
|
||||
this.ctrlOrMetaDown = true;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() === "c") {
|
||||
this.cDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp(event: KeyboardEvent) {
|
||||
if (event.key === "Control" || event.key === "Meta") {
|
||||
this.ctrlOrMetaDown = false;
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() === "c") {
|
||||
this.cDown = false;
|
||||
}
|
||||
|
||||
if (!this.isCopyChordHeld()) {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
beginCopyHighlight() {
|
||||
if (this.fallbackTimer !== null) {
|
||||
window.clearTimeout(this.fallbackTimer);
|
||||
}
|
||||
|
||||
const settings = this.getSettings();
|
||||
const fallbackMs = this.isCopyChordHeld()
|
||||
? settings.lostKeyupFallbackMs
|
||||
: settings.nonKeyboardCopyDurationMs;
|
||||
|
||||
// This is only a cleanup guard. Normal Ctrl/Cmd+C clears on keyup.
|
||||
this.fallbackTimer = window.setTimeout(() => {
|
||||
this.clear();
|
||||
}, fallbackMs);
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.fallbackTimer !== null) {
|
||||
window.clearTimeout(this.fallbackTimer);
|
||||
this.fallbackTimer = null;
|
||||
}
|
||||
|
||||
for (const clearHighlight of this.clearers) {
|
||||
clearHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
resetKeys() {
|
||||
this.ctrlOrMetaDown = false;
|
||||
this.cDown = false;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
private isCopyChordHeld() {
|
||||
return this.ctrlOrMetaDown && this.cDown;
|
||||
}
|
||||
}
|
||||
|
||||
function copyFlashExtension(highlightSession: HighlightSession) {
|
||||
class CopyFlashView {
|
||||
private clearTimer: number | null = null;
|
||||
private unregister: (() => void) | null = null;
|
||||
|
||||
constructor(private view: EditorView) {}
|
||||
|
||||
registerClearer() {
|
||||
if (this.unregister === null) {
|
||||
this.unregister = highlightSession.register(() => {
|
||||
this.view.dispatch({
|
||||
effects: clearFlash.of(),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.clearTimer !== null) {
|
||||
window.clearTimeout(this.clearTimer);
|
||||
if (this.unregister !== null) {
|
||||
this.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,20 +182,11 @@ function copyFlashExtension(durationMs: number) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.clearTimer !== null) {
|
||||
window.clearTimeout(this.clearTimer);
|
||||
}
|
||||
|
||||
this.registerClearer();
|
||||
this.view.dispatch({
|
||||
effects: addFlash.of(ranges),
|
||||
});
|
||||
|
||||
this.clearTimer = window.setTimeout(() => {
|
||||
this.view.dispatch({
|
||||
effects: clearFlash.of(),
|
||||
});
|
||||
this.clearTimer = null;
|
||||
}, durationMs);
|
||||
highlightSession.beginCopyHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,18 +208,58 @@ function copyFlashExtension(durationMs: number) {
|
|||
];
|
||||
}
|
||||
|
||||
export default class CopyFlasherPlugin extends Plugin {
|
||||
export default class CopyHighlighterPlugin extends Plugin {
|
||||
settings: CopyHighlighterSettings = DEFAULT_SETTINGS;
|
||||
private highlightSession = new HighlightSession(() => this.settings);
|
||||
private domHighlights = new Set<HTMLElement>();
|
||||
|
||||
async onload() {
|
||||
this.registerEditorExtension(copyFlashExtension(FLASH_DURATION_MS));
|
||||
await this.loadSettings();
|
||||
this.applyCssVariables();
|
||||
this.addSettingTab(new CopyHighlighterSettingTab(this.app, this));
|
||||
|
||||
this.highlightSession.register(() => {
|
||||
this.clearDomHighlights();
|
||||
});
|
||||
|
||||
this.registerEditorExtension(copyFlashExtension(this.highlightSession));
|
||||
|
||||
this.registerDomEvent(activeDocument, "keydown", (event) => {
|
||||
this.highlightSession.handleKeyDown(event);
|
||||
}, true);
|
||||
|
||||
this.registerDomEvent(activeDocument, "keyup", (event) => {
|
||||
this.highlightSession.handleKeyUp(event);
|
||||
}, true);
|
||||
|
||||
this.registerDomEvent(activeWindow, "blur", () => {
|
||||
this.highlightSession.resetKeys();
|
||||
});
|
||||
|
||||
this.registerDomEvent(activeDocument, "visibilitychange", () => {
|
||||
if (activeDocument.visibilityState === "hidden") {
|
||||
this.highlightSession.resetKeys();
|
||||
}
|
||||
});
|
||||
|
||||
// CodeMirror handles editor selections. This fallback covers copied text in
|
||||
// rendered Markdown or other Obsidian-owned DOM content.
|
||||
this.registerDomEvent(document, "copy", (event) => {
|
||||
this.registerDomEvent(activeDocument, "copy", (event) => {
|
||||
this.flashDomSelection(event);
|
||||
}, true);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.clearDomHighlights();
|
||||
activeDocument.body.style.removeProperty("--copy-highlighter-editor-background");
|
||||
activeDocument.body.style.removeProperty("--copy-highlighter-border-radius");
|
||||
}
|
||||
|
||||
private flashDomSelection(event: ClipboardEvent) {
|
||||
if (!this.settings.enableRenderedMarkdownHighlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
|
||||
if (!(target instanceof Element)) {
|
||||
|
|
@ -128,7 +270,7 @@ export default class CopyFlasherPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
const selection = target.ownerDocument.getSelection();
|
||||
|
||||
if (selection === null || selection.isCollapsed || selection.rangeCount === 0) {
|
||||
return;
|
||||
|
|
@ -147,6 +289,8 @@ export default class CopyFlasherPlugin extends Plugin {
|
|||
for (let index = 0; index < selection.rangeCount; index += 1) {
|
||||
this.flashRange(selection.getRangeAt(index));
|
||||
}
|
||||
|
||||
this.highlightSession.beginCopyHighlight();
|
||||
}
|
||||
|
||||
private flashRange(range: Range) {
|
||||
|
|
@ -155,16 +299,215 @@ export default class CopyFlasherPlugin extends Plugin {
|
|||
.slice(0, MAX_DOM_FLASH_RECTS);
|
||||
|
||||
for (const rect of rects) {
|
||||
const overlay = document.createElement("div");
|
||||
const ownerDocument = range.commonAncestorContainer.ownerDocument ?? activeDocument;
|
||||
const ownerWindow = ownerDocument.defaultView ?? activeWindow;
|
||||
const overlay = ownerDocument.createElement("div");
|
||||
|
||||
overlay.className = "copy-flasher-dom-highlight";
|
||||
overlay.style.left = `${rect.left + window.scrollX}px`;
|
||||
overlay.style.top = `${rect.top + window.scrollY}px`;
|
||||
overlay.className = "copy-highlighter-dom-highlight";
|
||||
overlay.style.left = `${rect.left + ownerWindow.scrollX}px`;
|
||||
overlay.style.top = `${rect.top + ownerWindow.scrollY}px`;
|
||||
overlay.style.width = `${rect.width}px`;
|
||||
overlay.style.height = `${rect.height}px`;
|
||||
overlay.style.backgroundColor = hexToRgba(
|
||||
this.settings.highlightColor,
|
||||
this.settings.renderedOpacity
|
||||
);
|
||||
overlay.style.borderRadius = `${this.settings.borderRadius}px`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
window.setTimeout(() => overlay.remove(), FLASH_DURATION_MS);
|
||||
ownerDocument.body.appendChild(overlay);
|
||||
this.domHighlights.add(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
private clearDomHighlights() {
|
||||
for (const highlight of this.domHighlights) {
|
||||
highlight.remove();
|
||||
}
|
||||
|
||||
this.domHighlights.clear();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<CopyHighlighterSettings>);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
this.applyCssVariables();
|
||||
}
|
||||
|
||||
resetSettings() {
|
||||
this.settings = { ...DEFAULT_SETTINGS };
|
||||
this.applyCssVariables();
|
||||
}
|
||||
|
||||
private applyCssVariables() {
|
||||
activeDocument.body.style.setProperty(
|
||||
"--copy-highlighter-editor-background",
|
||||
hexToRgba(this.settings.highlightColor, this.settings.editorOpacity)
|
||||
);
|
||||
activeDocument.body.style.setProperty(
|
||||
"--copy-highlighter-border-radius",
|
||||
`${this.settings.borderRadius}px`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CopyHighlighterSettingTab extends PluginSettingTab {
|
||||
constructor(app: App, private plugin: CopyHighlighterPlugin) {
|
||||
super(app, plugin);
|
||||
}
|
||||
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Appearance")
|
||||
.setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Highlight color")
|
||||
.setDesc("The color used when copied text is highlighted.")
|
||||
.addColorPicker((colorPicker) => {
|
||||
colorPicker
|
||||
.setValue(this.plugin.settings.highlightColor)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.highlightColor = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Editor opacity")
|
||||
.setDesc("How strong the highlight appears in live editor text.")
|
||||
.addSlider((slider) => {
|
||||
slider
|
||||
.setLimits(0.1, 1, 0.05)
|
||||
.setValue(this.plugin.settings.editorOpacity)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.editorOpacity = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Rendered text opacity")
|
||||
.setDesc("How strong the highlight appears outside the editor, such as reading view.")
|
||||
.addSlider((slider) => {
|
||||
slider
|
||||
.setLimits(0.1, 1, 0.05)
|
||||
.setValue(this.plugin.settings.renderedOpacity)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.renderedOpacity = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Corner radius")
|
||||
.setDesc("Rounds the highlight corners.")
|
||||
.addSlider((slider) => {
|
||||
slider
|
||||
.setLimits(0, 12, 1)
|
||||
.setValue(this.plugin.settings.borderRadius)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.borderRadius = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Highlight rendered Markdown")
|
||||
.setDesc("Also show copy confirmation when copying from reading view and other rendered text.")
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableRenderedMarkdownHighlight)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableRenderedMarkdownHighlight = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Mouse/menu copy duration")
|
||||
.setDesc("How long the highlight stays visible for non-keyboard copy actions.")
|
||||
.addText((text) => {
|
||||
text
|
||||
.setPlaceholder(String(DEFAULT_SETTINGS.nonKeyboardCopyDurationMs))
|
||||
.setValue(String(this.plugin.settings.nonKeyboardCopyDurationMs))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.nonKeyboardCopyDurationMs = parsePositiveInt(
|
||||
value,
|
||||
DEFAULT_SETTINGS.nonKeyboardCopyDurationMs
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.type = "number";
|
||||
text.inputEl.min = "100";
|
||||
text.inputEl.step = "50";
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Lost keyup fallback")
|
||||
.setDesc("Cleanup delay if Obsidian does not receive the key release event.")
|
||||
.addText((text) => {
|
||||
text
|
||||
.setPlaceholder(String(DEFAULT_SETTINGS.lostKeyupFallbackMs))
|
||||
.setValue(String(this.plugin.settings.lostKeyupFallbackMs))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.lostKeyupFallbackMs = parsePositiveInt(
|
||||
value,
|
||||
DEFAULT_SETTINGS.lostKeyupFallbackMs
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.type = "number";
|
||||
text.inputEl.min = "500";
|
||||
text.inputEl.step = "100";
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Reset settings")
|
||||
.setDesc("Restore the default appearance and timing.")
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText("Restore defaults")
|
||||
.onClick(async () => {
|
||||
this.plugin.resetSettings();
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hexToRgba(hex: string, opacity: number) {
|
||||
const normalized = hex.replace("#", "");
|
||||
const isValidHex = /^[0-9a-fA-F]{6}$/.test(normalized);
|
||||
|
||||
if (!isValidHex) {
|
||||
return hexToRgba(DEFAULT_SETTINGS.highlightColor, opacity);
|
||||
}
|
||||
|
||||
const red = parseInt(normalized.slice(0, 2), 16);
|
||||
const green = parseInt(normalized.slice(2, 4), 16);
|
||||
const blue = parseInt(normalized.slice(4, 6), 16);
|
||||
const alpha = Math.max(0, Math.min(opacity, 1));
|
||||
|
||||
return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
|
||||
}
|
||||
|
||||
function parsePositiveInt(value: string, fallback: number) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
|
||||
if (Number.isFinite(parsed) && parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
|
|
|||
32
styles.css
32
styles.css
|
|
@ -1,32 +1,12 @@
|
|||
.copy-flasher-editor-highlight {
|
||||
background-color: rgba(255, 213, 79, 0.45);
|
||||
border-radius: 3px;
|
||||
animation: copy-flasher-editor-fade 650ms ease-out forwards;
|
||||
.copy-highlighter-editor-highlight {
|
||||
background-color: var(--copy-highlighter-editor-background, rgba(255, 213, 79, 0.5));
|
||||
border-radius: var(--copy-highlighter-border-radius, 3px);
|
||||
}
|
||||
|
||||
.copy-flasher-dom-highlight {
|
||||
.copy-highlighter-dom-highlight {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
background-color: rgba(255, 213, 79, 0.42);
|
||||
border-radius: 3px;
|
||||
animation: copy-flasher-dom-fade 650ms ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes copy-flasher-editor-fade {
|
||||
0% {
|
||||
background-color: rgba(255, 213, 79, 0.62);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(255, 213, 79, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes copy-flasher-dom-fade {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
background-color: rgba(255, 213, 79, 0.46);
|
||||
border-radius: var(--copy-highlighter-border-radius, 3px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"strict": true,
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
|
|
@ -9,10 +9,11 @@
|
|||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"isolatedModules": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strictNullChecks": true,
|
||||
"strictBindCallApply": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,11 @@
|
|||
{
|
||||
"0.1.0": "0.15.0"
|
||||
"0.1.0": "0.15.0",
|
||||
"0.1.1": "0.15.0",
|
||||
"0.1.2": "0.16.0",
|
||||
"0.1.3": "0.16.0",
|
||||
"0.2.0": "0.16.0",
|
||||
"0.2.1": "1.0.0",
|
||||
"0.2.2": "1.0.0",
|
||||
"0.2.3": "1.0.0",
|
||||
"0.2.4": "1.0.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue