Skip to content

Instantly share code, notes, and snippets.

@vasiltabakov
Created October 7, 2017 09:47
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save vasiltabakov/a3bf16f6e69da421cbff60389b9eb975 to your computer and use it in GitHub Desktop.
Save vasiltabakov/a3bf16f6e69da421cbff60389b9eb975 to your computer and use it in GitHub Desktop.
Quill editor editable image caption
These two are partially based on the quill-image-resize-module (the options part).
Of course they need to be registered like so:
Quill.register(TaskerFigure);
Quill.register('modules/figureOptions', FigureOptions);
Code has a lot of room for improvement whereas we only needed a working version in our project's initial development phase.
import Quill from 'quill';
import {TaskerFigure} from './TaskerFigure';
import Parchment from 'parchment';
export class FigureOptions {
quill: Quill;
figure;
overlay;
linkRange;
constructor(quill, options = {}) {
this.quill = quill;
this.quill.root.addEventListener('click', this.handleClick.bind(this), false);
(<HTMLDivElement>this.quill.root.parentNode).style.position = (<HTMLDivElement>this.quill.root.parentNode).style.position || 'relative';
this.quill.on('selection-change', (range, oldRange, source) => {
this.linkRange = oldRange;
});
}
handleClick(evt) {
if (!evt.target) {
return false;
}
if (this.setFigure(evt)) {
} else if (this.figure) {
// clicked on a non image
this.hide();
}
}
setFigure(evt): boolean {
if (evt.target.classList.contains('tasker-figure')) {
this.setBaseFigure(evt.target);
return true;
} else {
let node = evt.target;
while (true) {
if (node.classList && node.classList.contains('tasker-figure')) {
break;
} else {
node = node.parentNode;
}
if (node === undefined || node === null) {
break;
}
}
if (node) {
this.setBaseFigure(node);
return true;
}
}
return false;
}
setBaseFigure(figure) {
if (this.figure === figure) {
return;
}
if (this.figure) {
this.hide();
}
this.show(figure);
}
show(figure) {
this.figure = figure;
this.figure.classList.add('with-options');
this.showOverlay();
}
showOverlay() {
if (this.overlay) {
this.hideOverlay();
}
this.quill.setSelection(null);
// prevent spurious text selection
this.setUserSelect('none');
// listen for the image being deleted or moved
// document.addEventListener('keyup', this.checkImage, true);
// this.quill.root.addEventListener('input', this.checkImage, true);
// Create and add the overlay
this.overlay = document.createElement('div');
const style = this.overlay.style;
style.position = 'absolute';
style.background = '#fff';
style.border = '2px solid #1E90FF';
style.borderRadius = '5px';
style.width = 'auto';
style.display = 'table';
style.padding = '10px';
style.margin = '0 auto';
style.left = '50%';
style.transform = 'translateX(-50%)';
style.borderBottomLeftRadius = '0';
style.borderBottomRightRadius = '0';
style.borderBottom = '2px solid #fff';
this.overlay.classList.add('tasker-figure-options')
const inputField = document.createElement('input');
inputField.value = TaskerFigure.value(this.figure).name;
inputField.placeholder = 'Enter image caption...';
inputField.addEventListener('input', (event) => {
// console.log(child.value);
// this.quill.formatText(this.linkRange, 'link', value, Emitter.sources.USER);
// this.quill.format('taskerFigure', child.value);
if (this.linkRange) {
const delta = this.quill.formatText(this.linkRange.index, this.linkRange.length, 'tasker-caption', inputField.value, 'user');
}
});
this.overlay.appendChild(inputField);
const cancel = document.createElement('i');
cancel.classList.add('material-icons');
cancel.innerText = 'close';
this.overlay.appendChild(cancel);
const remove = document.createElement('i');
remove.classList.add('material-icons');
remove.classList.add('delete');
remove.innerText = 'delete_forever';
this.overlay.appendChild(remove);
remove.addEventListener('click', (event) => {
const blot = Parchment.find(this.figure);
this.hide();
blot.remove();
});
cancel.addEventListener('click', (event) => {
this.hide();
});
this.quill.root.parentNode.appendChild(this.overlay);
this.repositionElements();
}
repositionElements() {
if (!this.overlay || !this.figure) {
return;
}
// position the overlay over the image
const parent: HTMLDivElement = <HTMLDivElement>this.quill.root.parentNode;
const imgRect = this.figure.getBoundingClientRect();
const containerRect = parent.getBoundingClientRect();
Object.assign(this.overlay.style, {
top: `${imgRect.top - containerRect.top + parent.scrollTop - 47.5}px`,
});
}
hide() {
this.figure.classList.remove('with-options');
this.hideOverlay();
this.figure = undefined;
}
hideOverlay() {
if (!this.overlay) {
return;
}
// Remove the overlay
this.quill.root.parentNode.removeChild(this.overlay);
this.overlay = undefined;
// stop listening for image deletion or movement
// document.removeEventListener('keyup', this.checkImage);
// this.quill.root.removeEventListener('input', this.checkImage);
// reset user-select
this.setUserSelect('');
}
setUserSelect(value) {
[
'userSelect',
'mozUserSelect',
'webkitUserSelect',
'msUserSelect',
].forEach((prop) => {
// set on contenteditable element and <html>
this.quill.root.style[prop] = value;
document.documentElement.style[prop] = value;
});
}
}
import Quill from 'quill';
const Embed = Quill.import('blots/embed');
export class TaskerFigure extends Embed {
public static blotName = 'taskerFigure';
public static tagName = 'div';
static value(domNode): any {
const taskerImage = domNode.querySelector('.tasker-image');
const taskerCaption = domNode.querySelector('.tasker-caption');
if (taskerImage && taskerCaption) {
return {
original: {
url: taskerImage.dataset.originalUrl,
path: taskerImage.dataset.originalPath
},
normal: {
url: taskerImage.dataset.normalUrl,
path: taskerImage.dataset.normalPath
},
small: {
url: taskerImage.dataset.smallUrl,
path: taskerImage.dataset.smallPath
},
name: taskerCaption.innerText
};
} else {
return {};
}
}
static create(value) {
const node = super.create(value);
node.setAttribute('contenteditable', false);
node.style.fontSize = '0';
node.classList.add('tasker-figure');
const image = document.createElement('img');
image.setAttribute('src', value.normal.url);
image.classList.add('tasker-image');
image.dataset.normalUrl = value.normal.url;
image.dataset.smallUrl = value.small.url;
image.dataset.originalUrl = value.original.url;
image.dataset.normalPath = value.normal.path;
image.dataset.smallPath = value.small.path;
image.dataset.originalPath = value.original.path;
node.appendChild(image);
const caption = document.createElement('a');
caption.innerText = value.name;
caption.classList.add('tasker-caption');
caption.setAttribute('href', value.original.url);
caption.setAttribute('target', '_blank');
node.appendChild(caption);
return node;
}
format(format: any, value: any) {
if (format === 'tasker-caption') {
this.domNode.querySelector('.tasker-caption').innerText = value;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment