Skip to content

Instantly share code, notes, and snippets.

@oquno
Last active October 29, 2024 13:06
Show Gist options
  • Save oquno/2419d0ad137b8db208a8da232cf5f567 to your computer and use it in GitHub Desktop.
Save oquno/2419d0ad137b8db208a8da232cf5f567 to your computer and use it in GitHub Desktop.
Userscript to insert recent images from Gyazo into textarea.
// ==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();
})();
@oquno
Copy link
Author

oquno commented Oct 29, 2024

Storing token in localStorage may not best way for you. Edit this script as you like when you use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment