Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

toggle toobar-icon classlist #26

Open
maxihaindl opened this issue Feb 28, 2025 · 0 comments
Open

toggle toobar-icon classlist #26

maxihaindl opened this issue Feb 28, 2025 · 0 comments

Comments

@maxihaindl
Copy link

maxihaindl commented Feb 28, 2025

Hi,

first of all thanks for your great work. I really apprechiate resizing images in an editor like you made it possible!

I am using quill-blot-formatter2 with images, links and even links around images - all with custom blots and custom modals on the frontend.

nevertheless, selecting an image or a linked image, does not toggle the toolbar buttons active. Even if I try to toggle these by hand. whereas selecting a plain link does toggle the link button active.

customEditor.root.querySelectorAll('div > a').forEach(anchor => {
	anchor.addEventListener('click', () => {
		const toolbarLinkButton =
			customEditorToolbar.container.querySelector('.ql-link');

		console.log(toolbarLinkButton);
		toolbarLinkButton.classList.toggle('ql-active');
		console.log(toolbarLinkButton);
	});
});

this is what my page looks like using qbf2:
Image

this is without:

Image

its just a sneaky suspicion, but it seems the toggled/added class get's removed right away by qbf2. if I turn off quill-blot-formatter2 like commenting out the config, the buttons will get active as you'd wish.

This is my config:

const customEditorOptions = {
	debug: 'log',
	modules: {
		keyboard: {
			bindings: {
				tab: {
					key: 9,
					handler: () => {},
				},
			},
		},
		toolbar: {
			container: `#${editor.dataset.editorId}-toolbar`,
			handlers: {},
		},
		blotFormatter2: {
			align: {allowAligning: false},
			image: {
				allowResizing: false,
				allowAltTitleEdit: false,
			},
			overlay: {
				style: {
					backgroundColor: 'rgba(173, 120, 28, .4)',
					border:
						'1px dashed var(--application-color-neutral-border--active)',
				},
			},
		},
	},
};

These are my custom blots:

import Quill from 'quill';

const BlockEmbed = Quill.import('blots/block/embed');

/**
 * CustomImageBlot of an Image in Quill Editor
 *
 * `this.linkProps` acts as a gatekeeper to prevent unintentional properties
 * if you want to add new properties to your blot, add them to the array and to static value()
 * and pass them to your instance methods
 */
export default class CustomImageBlot extends BlockEmbed {
	static blotName = 'customImage';
	static tagName = 'div';

	constructor(scroll, domNode, value) {
		super(scroll.scroll, domNode);

		this.link = domNode.querySelector('a');
		this.image = domNode.querySelector('img');
		this.linkProps = ['href', 'target', 'aria-label'];
		this.imageFloatingClassTemplate = 'ql-image-float-';
		this.imageFloatingActiveClass = '';
	}

	static create(value) {
		const node = super.create();
		const image = document.createElement('img');

		image.setAttribute('src', value.src);

		if (value.alt) image.setAttribute('alt', value.alt);
		if (value.copyright) image.setAttribute('data-copyright', value.copyright);
		if (value.width) image.setAttribute('width', value.width);
		if (value.height) image.setAttribute('height', value.height);

		if (value.link) {
			const link = document.createElement('a');
			link.setAttribute('href', value.link.href);
			link.setAttribute('target', value.link.target);
			link.setAttribute('aria-label', value.link.ariaLabel);

			link.appendChild(image);
			node.appendChild(link);

			return node;
		}

		node.appendChild(image);

		return node;
	}

	static value(node) {
		const image = node.querySelector('img');
		const link = node.querySelector('a');

		try {
			const v = {
				src: image.getAttribute('src'),
				alt: image.getAttribute('alt'),
				copyright: image.getAttribute('data-copyright'),
				width: image.getAttribute('width'),
				height: image.getAttribute('height'),
			};

			if (link)
				v['link'] = {
					href: link.getAttribute('href'),
					target: link.getAttribute('target'),
					ariaLabel: link.getAttribute('aria-label'),
				};

			return v;
		} catch (e) {
			return true;
		}
	}

	/**
	 * wraps the image with an anchor tag
	 * @param {{ href: string, target: '_self' | '_blank' }} linkAttributes
	 */
	createImageAnchorWrapper(linkAttributes) {
		if (this.link) throw new Error('Cannot wrap an existing Anchor!');

		if (!linkAttributes.href || !linkAttributes.target || !linkAttributes)
			throw new Error('Missing link properties!');

		const link = document.createElement('a');

		Object.keys(linkAttributes).forEach(attribute => {
			// make sure to attach only defined attributes
			if (this.linkProps.findIndex(item => item === attribute) !== -1) {
				link.setAttribute(attribute, linkAttributes[attribute]);
			}
		});

		link.appendChild(this.image);
		this.domNode.appendChild(link);

		return this;
	}

	/**
	 * edit existing anchor-wrapper of the image
	 * @param {{ href: string, target: '_self' | '_blank', aria-label: string }} linkAttributes
	 */
	editImageAnchorWrapper(linkAttributes) {
		if (!this.link) throw new Error('Missing Anchor-Wrapper');

		Object.keys(linkAttributes).forEach(attribute => {
			// make sure to attach only defined attributes
			if (this.linkProps.findIndex(item => item === attribute) !== -1) {
				this.link.removeAttribute(attribute);
				this.link.setAttribute(attribute, linkAttributes[attribute]);
			}
		});

		return this;
	}

	/**
	 * removes existing anchor-wrapper from the image
	 */
	removeImageAnchorWrapper() {
		if (!this.link) throw new Error('Missing Anchor-Wrapper');

		this.domNode.removeChild(this.link);
		this.domNode.appendChild(this.image);

		return this;
	}

	/**
	 * floats an image
	 * @param {"right" | "left" | "full" | null} value
	 */
	floatImage(value) {
		if (value) {
			const className = `${this.imageFloatingClassTemplate}${value}`;

			if (this.imageFloatingActiveClass) {
				this.image.classList.remove(this.imageFloatingActiveClass);
				this.image.classList.add(className);
			} else {
				this.image.classList.add(className);
			}

			this.imageFloatingActiveClass = className;
		} else this.image.classList.remove(this.imageFloatingActiveClass);

		return this;
	}
}

How can I take influence on that behavior?

Update:
I managed to handle toolbar-button active-state by myself with two click listeners

customEditor.root.querySelectorAll('img').forEach(image => {
	image.addEventListener('click', () => {
		initSelection(customEditor);
		toggleToolbarIcon(customEditor, '.ql-image');
	});
});

customEditor.root.querySelectorAll('div > a').forEach(anchor => {
	anchor.addEventListener('click', () => {
		initSelection(customEditor);
		toggleToolbarIcon(customEditor, '.ql-link');
	});
});

const toggleToolbarIcon = (editor, buttonSelector) => {
	setTimeout(() => {
		const toolbar = editor.getModule('toolbar');
		const button = toolbar.container.querySelector(buttonSelector);

		button.classList.toggle('ql-active');
	}, 50);
};

The key here is to set a timeout to let events populate properly within quill and then toggle the class
This does the job, but still doesn't really feel naturally.

Another sideffect is now, when resizing the image, active-class still gets vanished, so the buttons turn 'off'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant