Last active
October 29, 2024 13:06
-
-
Save oquno/2419d0ad137b8db208a8da232cf5f567 to your computer and use it in GitHub Desktop.
Userscript to insert recent images from Gyazo into textarea.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Gyazo Image Insert | |
// @namespace https://oq.la/ | |
// @version 1.5 | |
// @description Insert recent images from Gyazo into textarea and modern contentEditable elements. | |
// @match *://*/* | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
let currentPage = 1; | |
let loading = false; | |
let imageWindow = null; // Track the image window instance | |
let embedFormat = localStorage.getItem('gyazoEmbedFormat') || 'Markdown'; | |
let activeTextArea = null; | |
let gyazoButton = null; | |
// Create input fields for Gyazo Access Token | |
function createSettingsUI() { | |
if (document.getElementById('gyazo-settings-ui')) return; // Avoid creating duplicate UI | |
const settingsDiv = document.createElement('div'); | |
settingsDiv.id = 'gyazo-settings-ui'; | |
Object.assign(settingsDiv.style, { | |
position: 'fixed', | |
top: '20px', | |
left: '20px', | |
width: '300px', | |
padding: '10px', | |
backgroundColor: '#f9f9f9', | |
border: '1px solid #ccc', | |
zIndex: '10000' | |
}); | |
const title = document.createElement('h3'); | |
title.textContent = 'Gyazo API Settings'; | |
settingsDiv.appendChild(title); | |
const accessTokenLabel = document.createElement('label'); | |
accessTokenLabel.textContent = 'Gyazo Access Token:'; | |
settingsDiv.appendChild(accessTokenLabel); | |
const accessTokenInput = document.createElement('input'); | |
Object.assign(accessTokenInput, { | |
type: 'text', | |
value: localStorage.getItem('gyazoAccessToken') || '' | |
}); | |
Object.assign(accessTokenInput.style, { | |
width: '100%', | |
marginBottom: '10px' | |
}); | |
accessTokenInput.addEventListener('input', () => { | |
localStorage.setItem('gyazoAccessToken', accessTokenInput.value); | |
}); | |
const link = document.createElement("a"); | |
Object.assign(link, { | |
href: "https://gyazo.com/oauth/applications", | |
target: "_blank", | |
}) | |
link.innerText = "your apps"; | |
const linkDiv = document.createElement("div"); | |
linkDiv.appendChild(link); | |
settingsDiv.appendChild(accessTokenInput); | |
settingsDiv.appendChild(linkDiv); | |
const closeButton = document.createElement('button'); | |
closeButton.textContent = 'Close'; | |
Object.assign(closeButton.style, { | |
marginTop: '10px', | |
padding: '5px', | |
backgroundColor: '#0073e6', | |
color: '#fff', | |
border: 'none', | |
cursor: 'pointer' | |
}); | |
closeButton.addEventListener('click', () => document.body.removeChild(settingsDiv)); | |
settingsDiv.appendChild(closeButton); | |
document.body.appendChild(settingsDiv); | |
} | |
// Create the Gyazo button that will be shown when a textarea or contentEditable is focused | |
function createGyazoButton() { | |
const button = document.createElement('button'); | |
button.textContent = '🔍 Gyazo Image'; | |
Object.assign(button.style, { | |
position: 'fixed', | |
top: localStorage.getItem('gyazoButtonTop') || '5px', | |
left: localStorage.getItem('gyazoButtonLeft') || '5px', | |
padding: '5px 10px', | |
fontSize: '12px', | |
zIndex: '10001', | |
backgroundColor: '#0073e6', | |
color: '#fff', | |
border: 'none', | |
borderRadius: '3px', | |
cursor: 'pointer', | |
display: 'none' // Initially hidden | |
}); | |
document.body.appendChild(button); | |
return button; | |
} | |
// Handle when a textarea or contentEditable gains focus | |
function handleElementFocus(event) { | |
if (event.target.tagName === 'TEXTAREA' || event.target.isContentEditable) { | |
activeTextArea = event.target; | |
showGyazoButton(); | |
} | |
} | |
// Handle when a textarea or contentEditable loses focus | |
function handleElementBlur(event) { | |
if (event.target === activeTextArea) { | |
setTimeout(() => { | |
if (!imageWindow) { | |
activeTextArea = null; | |
hideGyazoButton(); | |
} | |
}, 200); // Delay to prevent immediate hiding when interacting with the Gyazo button | |
} | |
} | |
// Show the Gyazo button | |
function showGyazoButton() { | |
if (gyazoButton) { | |
gyazoButton.style.display = 'block'; | |
} | |
} | |
function closeImageWindow() { | |
document.body.removeChild(imageWindow); | |
imageWindow = null; | |
currentPage = 1; // Reset currentPage when window is closed | |
} | |
// Hide the Gyazo button | |
function hideGyazoButton() { | |
if (gyazoButton) { | |
gyazoButton.style.display = 'none'; | |
} | |
} | |
// Setup the Gyazo button and add event listeners for dragging | |
function setupGyazoButton() { | |
gyazoButton = createGyazoButton(); | |
let isDragging = false; | |
let offsetX, offsetY; | |
// Enable dragging of the Gyazo button | |
gyazoButton.addEventListener('mousedown', (event) => { | |
isDragging = true; | |
offsetX = event.clientX - gyazoButton.getBoundingClientRect().left; | |
offsetY = event.clientY - gyazoButton.getBoundingClientRect().top; | |
event.preventDefault(); | |
}); | |
// Handle the mouse movement during dragging | |
document.addEventListener('mousemove', (event) => { | |
if (!isDragging) return; | |
if (imageWindow) { | |
closeImageWindow(); | |
} | |
let newLeft = event.clientX - offsetX; | |
let newTop = event.clientY - offsetY; | |
// Ensure the button stays within the viewport bounds | |
newLeft = Math.max(0, Math.min(window.innerWidth - gyazoButton.offsetWidth, newLeft)); | |
newTop = Math.max(0, Math.min(window.innerHeight - gyazoButton.offsetHeight, newTop)); | |
gyazoButton.style.left = `${newLeft}px`; | |
gyazoButton.style.top = `${newTop}px`; | |
}); | |
// Stop dragging and save the new position | |
document.addEventListener('mouseup', () => { | |
if (isDragging) { | |
localStorage.setItem('gyazoButtonLeft', gyazoButton.style.left); | |
localStorage.setItem('gyazoButtonTop', gyazoButton.style.top); | |
} | |
isDragging = false; | |
}); | |
// Open the image search window when the button is clicked | |
gyazoButton.addEventListener('click', (event) => { | |
if (imageWindow) { | |
closeImageWindow(); | |
} | |
else if (!isDragging && activeTextArea) { | |
openImageWindow(activeTextArea); | |
} | |
}); | |
} | |
// Open a window to search and select images from Gyazo | |
async function openImageWindow(targetElement) { | |
// Prevent multiple image windows from opening | |
if (imageWindow) return; | |
const accessToken = localStorage.getItem('gyazoAccessToken'); | |
if (!accessToken) { | |
createSettingsUI(); | |
return; | |
} | |
// Create the image window UI | |
imageWindow = document.createElement('div'); | |
Object.assign(imageWindow.style, { | |
position: 'fixed', | |
top: `${parseInt(localStorage.getItem('gyazoButtonTop')) + 50 || 70}px`, | |
left: `${parseInt(localStorage.getItem('gyazoButtonLeft')) || 5}px`, | |
width: '350px', | |
height: '450px', | |
overflow: 'hidden', | |
border: '1px solid #ccc', | |
backgroundColor: '#f9f9f9', | |
padding: '10px', | |
zIndex: '10000' | |
}); | |
// Add title and close button to the image window | |
const titleElement = document.createElement('h3'); | |
titleElement.textContent = 'Gyazo Recent Images'; | |
imageWindow.appendChild(titleElement); | |
const closeButton = document.createElement('button'); | |
closeButton.textContent = '✖'; | |
Object.assign(closeButton.style, { | |
float: 'right', | |
cursor: 'pointer', | |
backgroundColor: 'transparent', | |
border: 'none', | |
fontSize: '16px' | |
}); | |
closeButton.addEventListener('click', () => { | |
closeImageWindow(); | |
}); | |
titleElement.appendChild(closeButton); | |
// Add format selector dropdown | |
const formatSelect = document.createElement('select'); | |
const formats = ['URL', 'Markdown', 'HTML Embed']; | |
formats.forEach(format => { | |
const option = document.createElement('option'); | |
option.value = format; | |
option.textContent = format; | |
if (format === embedFormat) option.selected = true; | |
formatSelect.appendChild(option); | |
}); | |
Object.assign(formatSelect.style, { | |
marginBottom: '10px' | |
}); | |
formatSelect.addEventListener('change', () => { | |
embedFormat = formatSelect.value; | |
localStorage.setItem('gyazoEmbedFormat', embedFormat); | |
}); | |
imageWindow.appendChild(formatSelect); | |
// Container for displaying image results | |
const resultContainer = document.createElement('div'); | |
Object.assign(resultContainer.style, { | |
overflowY: 'auto', | |
height: '380px', | |
display: 'grid', | |
gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))', | |
gap: '0px', | |
}); | |
imageWindow.appendChild(resultContainer); | |
document.body.appendChild(imageWindow); | |
// Add scroll event listener for infinite scrolling | |
resultContainer.addEventListener('scroll', () => { | |
if (!loading && resultContainer.scrollTop + resultContainer.clientHeight >= resultContainer.scrollHeight - 10) { | |
loadGyazoImages(resultContainer, targetElement); // Load more images if reached near the bottom | |
} | |
}); | |
// Initial load of user's recent images | |
loadGyazoImages(resultContainer, targetElement); | |
} | |
// Load images from Gyazo API | |
function loadGyazoImages(resultContainer, targetElement) { | |
const accessToken = localStorage.getItem('gyazoAccessToken'); | |
if (!accessToken) return alert('Access token is not set.'); | |
loading = true; | |
const url = `https://api.gyazo.com/api/images?access_token=${accessToken}&page=${currentPage}&per_page=20`; | |
// Fetch images from Gyazo API | |
fetch(url) | |
.then(response => response.json()) | |
.then(data => { | |
showImageResults(data, resultContainer, targetElement); | |
loading = false; | |
currentPage++; // Load next page on subsequent calls | |
}) | |
.catch(error => { | |
console.error('Gyazo API Error:', error); | |
loading = false; | |
}); | |
} | |
// Display the fetched images in the result container | |
function showImageResults(images, resultContainer, targetElement) { | |
images.forEach(image => { | |
const div = document.createElement('div'); | |
Object.assign(div.style, { | |
width: '80px', | |
height: '80px', | |
display: 'flex', | |
justifyContent: 'center', | |
alignItems: 'center', | |
}); | |
const img = document.createElement('img'); | |
img.src = image.thumb_url; | |
Object.assign(img.style, { | |
cursor: image.image_id ? 'pointer' : 'not-allowed', | |
margin: '5px', | |
maxWidth: '80px', | |
maxHeight: '80px', | |
opacity: image.image_id ? '1' : '0.5' // Grayed out if image is not selectable | |
}); | |
img.title = image.metadata && image.metadata.title ? image.metadata.title : 'Click to insert the image'; // Add tooltip with image title if available | |
// Add click event listener to insert the image if image_id is available | |
if (image.image_id) { | |
img.addEventListener('click', () => { | |
insertImageWithFormat(image, targetElement); | |
}); | |
} | |
div.appendChild(img); | |
resultContainer.appendChild(div); | |
}); | |
} | |
// Insert the selected image into the textarea or contentEditable in the chosen format | |
function insertImageWithFormat(image, targetElement) { | |
const imageUrl = `https://gyazo.com/${image.image_id}`; | |
const imageTitle = image.metadata && image.metadata.title ? image.metadata.title : 'Gyazo Image'; | |
let imageCode = ''; | |
switch (embedFormat) { | |
case 'URL': | |
imageCode = imageUrl; | |
break; | |
case 'Markdown': | |
imageCode = `[![${imageTitle}](${image.url})](${imageUrl})`; | |
break; | |
case 'HTML Embed': | |
imageCode = `<a href="${imageUrl}" title="${imageTitle}" target="_blank"><img src="${image.url}" title="${imageTitle}" alt="${imageTitle}"></a>`; | |
break; | |
default: | |
return; | |
} | |
if (targetElement.tagName === 'TEXTAREA') { | |
// Insert at the cursor position in the textarea | |
const startPos = targetElement.selectionStart; | |
const endPos = targetElement.selectionEnd; | |
targetElement.value = targetElement.value.substring(0, startPos) + imageCode + targetElement.value.substring(endPos); | |
targetElement.setSelectionRange(startPos + imageCode.length, startPos + imageCode.length); | |
targetElement.focus(); | |
} else if (targetElement.isContentEditable) { | |
// Insert in the contentEditable element | |
document.execCommand('insertHTML', false, imageCode); | |
} | |
} | |
// Monitor textareas and contentEditable elements dynamically | |
function monitorElements() { | |
const observedElements = new Set(); | |
// Observe DOM changes to find new elements | |
const observer = new MutationObserver((mutations) => { | |
mutations.forEach((mutation) => { | |
mutation.addedNodes.forEach((node) => { | |
if (node.tagName === 'TEXTAREA' || (node.isContentEditable && node.tagName !== 'IFRAME')) { | |
if (!observedElements.has(node)) { | |
observedElements.add(node); | |
node.addEventListener('focus', handleElementFocus); | |
node.addEventListener('blur', handleElementBlur); | |
} | |
} else if (node.querySelectorAll) { | |
node.querySelectorAll('textarea, [contenteditable=true]').forEach((element) => { | |
if (!observedElements.has(element)) { | |
observedElements.add(element); | |
element.addEventListener('focus', handleElementFocus); | |
element.addEventListener('blur', handleElementBlur); | |
} | |
}); | |
} | |
}); | |
mutation.removedNodes.forEach((node) => { | |
if ((node.tagName === 'TEXTAREA' || node.isContentEditable) && observedElements.has(node)) { | |
observedElements.delete(node); | |
node.removeEventListener('focus', handleElementFocus); | |
node.removeEventListener('blur', handleElementBlur); | |
} else if (node.querySelectorAll) { | |
node.querySelectorAll('textarea, [contenteditable=true]').forEach((element) => { | |
if (observedElements.has(element)) { | |
observedElements.delete(element); | |
element.removeEventListener('focus', handleElementFocus); | |
element.removeEventListener('blur', handleElementBlur); | |
} | |
}); | |
} | |
}); | |
}); | |
}); | |
observer.observe(document.body, { childList: true, subtree: true }); | |
// Initially add existing elements | |
document.querySelectorAll('textarea, [contenteditable=true]').forEach((element) => { | |
if (!observedElements.has(element)) { | |
observedElements.add(element); | |
element.addEventListener('focus', handleElementFocus); | |
element.addEventListener('blur', handleElementBlur); | |
} | |
}); | |
} | |
// Initialize script | |
function init() { | |
setupGyazoButton(); | |
monitorElements(); | |
} | |
init(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Storing token in localStorage may not best way for you. Edit this script as you like when you use.