clone of github.com/decent-newsroom/newsroom
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

166 lines
5.6 KiB

import { Controller } from '@hotwired/stimulus';
import Quill from 'quill';
import('quill/dist/quill.core.css');
import('quill/dist/quill.snow.css');
export default class extends Controller {
connect() {
// --- 1) Custom IMG blot that supports alt ---
const BlockEmbed = Quill.import('blots/block/embed');
class ImageAltBlot extends BlockEmbed {
static blotName = 'imageAlt'; // if you want to replace default, rename to 'image'
static tagName = 'IMG';
static create(value) {
const node = super.create();
if (typeof value === 'string') {
node.setAttribute('src', value);
} else if (value && value.src) {
node.setAttribute('src', value.src);
if (value.alt) node.setAttribute('alt', value.alt);
}
node.setAttribute('draggable', 'false');
return node;
}
static value(node) {
return {
src: node.getAttribute('src') || '',
alt: node.getAttribute('alt') || '',
};
}
}
Quill.register(ImageAltBlot);
// --- 2) Tooltip UI (modeled on Quill's link tooltip) ---
const Tooltip = Quill.import('ui/tooltip');
class ImageTooltip extends Tooltip {
constructor(quill, boundsContainer) {
super(quill, boundsContainer);
this.root.classList.add('ql-image-tooltip');
this.root.innerHTML = [
'<span class="ql-tooltip-arrow"></span>',
'<div class="ql-tooltip-editor">',
'<input class="ql-image-src" type="text" placeholder="Image URL" />',
'<input class="ql-image-alt" type="text" placeholder="Alt text" />',
'<a class="ql-action"></a>',
'<a class="ql-cancel"></a>',
'</div>',
].join('');
this.srcInput = this.root.querySelector('.ql-image-src');
this.altInput = this.root.querySelector('.ql-image-alt');
this.action = this.root.querySelector('.ql-action');
this.cancel = this.root.querySelector('.ql-cancel');
this.action.addEventListener('click', () => this.save());
this.cancel.addEventListener('click', () => this.hide());
const keyHandler = (e) => {
if (e.key === 'Enter') { e.preventDefault(); this.save(); }
if (e.key === 'Escape') { e.preventDefault(); this.hide(); }
};
this.srcInput.addEventListener('keydown', keyHandler);
this.altInput.addEventListener('keydown', keyHandler);
}
edit(prefill = null) {
const range = this.quill.getSelection(true);
if (!range) return;
const bounds = this.quill.getBounds(range);
this.show();
this.position(bounds);
this.root.classList.add('ql-editing');
this.srcInput.value = prefill?.src || '';
this.altInput.value = prefill?.alt || '';
this.srcInput.focus();
this.srcInput.select();
}
hide() {
this.root.classList.remove('ql-editing');
super.hide();
}
save() {
const src = (this.srcInput.value || '').trim();
const alt = (this.altInput.value || '').trim();
// basic safety: allow http(s) or data:image/*
if (!src || !/^https?:|^data:image\//i.test(src)) {
this.srcInput.focus();
return;
}
const range = this.quill.getSelection(true);
if (!range) return;
// If selection is on existing ImageAlt blot, replace it; otherwise insert new
const [blot, blotOffset] = this.quill.getLeaf(range.index);
const isImageBlot = blot && blot.domNode && blot.domNode.tagName === 'IMG';
if (isImageBlot) {
// delete current, insert new one
const idx = range.index - blotOffset;
this.quill.deleteText(idx, 1, 'user');
this.quill.insertEmbed(idx, 'imageAlt', { src, alt }, 'user');
this.quill.setSelection(idx + 1, 0, 'user');
} else {
this.quill.insertEmbed(range.index, 'imageAlt', { src, alt }, 'user');
this.quill.setSelection(range.index + 1, 0, 'user');
}
this.hide();
}
}
// --- 3) Quill init ---
const toolbarOptions = [
['bold', 'italic', 'strike'],
['link', 'blockquote', 'code-block', 'image'],
[{ header: 1 }, { header: 2 }, { header: 3 }],
[{ list: 'ordered' }, { list: 'bullet' }],
];
const options = {
theme: 'snow',
modules: {
toolbar: toolbarOptions,
},
};
// Use the element in this controller's scope
const editorEl = this.element.querySelector('#editor') || document.querySelector('#editor');
const target = this.element.querySelector('#editor_content') || document.querySelector('#editor_content');
const quill = new Quill(editorEl, options);
// One tooltip instance per editor
const imageTooltip = new ImageTooltip(quill, quill.root.parentNode);
// Intercept toolbar 'image' to open our tooltip
quill.getModule('toolbar').addHandler('image', () => {
// If caret is on an IMG, prefill from it
const range = quill.getSelection(true);
let prefill = null;
if (range) {
const [blot] = quill.getLeaf(range.index);
if (blot?.domNode?.tagName === 'IMG') {
prefill = {
src: blot.domNode.getAttribute('src') || '',
alt: blot.domNode.getAttribute('alt') || '',
};
}
}
imageTooltip.edit(prefill);
});
// Keep your hidden field synced as HTML
const sync = () => { if (target) target.value = quill.root.innerHTML; };
quill.on('text-change', sync);
// initialize once
sync();
}
}