ngx-quill icon indicating copy to clipboard operation
ngx-quill copied to clipboard

How to trigger ngModelChange event from Custom Video Formats

Open Gourishmesta opened this issue 2 years ago • 10 comments

Hi there

I have added a custom video formatter which has the logic to resize the video element which is working as expected, but only issue here is that change in the size of video element is not triggering the editor content change event but after changing the size if i type in some text in the editor than the updated video element size is reflected in editor Change event.

So is there way to trigger the editor content change event withing the Video BlobFormatter ?

Below is the code on how I am registering the Custom video Formatter Quill.register({ 'modules/blotFormatter': BlotFormatter, 'formats/video': QuillCustomVideo, 'formats/image': QuillCustomImage, });

An below is how the resize feature looks on UI image

Thank You

Gourishmesta avatar Feb 06 '24 09:02 Gourishmesta

it depends how your resize is implemented.

ngx-quill per default only track real "user" changes. if you check the quilljs documentation you see each "update" api accepts a last "source" parameter, which can be "user", "api", "silent". So per default ngx-quill only recognized "user" changes as real changes. if not each formControl/ngModel change from outside would retrigger the change-outputs.

you have two options -> in your custom resize module trigger the changes as "user" or set the trackChanges input of the ngx-quill editor component to all.

KillerCodeMonkey avatar Feb 06 '24 09:02 KillerCodeMonkey

it depends how your resize is implemented.

ngx-quill per default only track real "user" changes. if you check the quilljs documentation you see each "update" api accepts a last "source" parameter, which can be "user", "api", "silent". So per default ngx-quill only recognized "user" changes as real changes. if not each formControl/ngModel change from outside would retrigger the change-outputs.

you have two options -> in your custom resize module trigger the changes as "user" or set the trackChanges input of the ngx-quill editor component to all.

image

Can i trigger changes from above CustomVideoBlob?

Gourishmesta avatar Feb 06 '24 10:02 Gourishmesta

do not know... since i can not see all your code..

KillerCodeMonkey avatar Feb 06 '24 10:02 KillerCodeMonkey

Sorry for that below is how i am adding the video format blot and a resize logic on it



import Quill from 'quill';
const Parchment = Quill.import('parchment');
const Video = Quill.import('formats/video');

interface VideoAttributes {
	[key: string]: string | undefined;
	width?: string;
	height?: string;
	style?: string;
}

const ATTRIBUTES = ['width', 'height', 'style'];
interface NubStyles {
	[key: string]: {
		top?: string;
		right?: string;
		bottom?: string;
		left?: string;
	};
}
const nubStyles: NubStyles = {
	tLeft: {
		top: '-5px',
		left: '-5px',
	},
	tRight: {
		top: '-5px',
		right: '-5px',
	},
	bLeft: {
		bottom: '-5px',
		left: '-5px',
	},
	bRight: {
		bottom: '-5px',
		right: '-5px',
	},
};

const getClosest = (el: any, sel: any) => {
	while ((el = el.parentElement) && !(el.matches || el.matchesSelector).call(el, sel));
	return el;
};

const createSpacer = () => {
	let spacer = document.createElement('div');
	spacer.appendChild(document.createElement('br'));
	return spacer;
};
class VideoBuilder {
	selectedElement: any;
	parentElement: any;
	node: any;
	videoElement: any;
	boxes: HTMLSpanElement[] = [];
	dragHandeler: any;
	mouseUp: any;
	mouseDown: any;
	dragBox: any;
	dragStartX: any;
	dragStartY: any;
	preDragWidth: any;
	preDragHeight: any;
	handelDeselect: any;
	buildVideoElement(src: any, node: any, videoHeight: number, videoWidth: number) {
		let videoElement = document.createElement('video');
		videoElement.setAttribute('frameborder', '0');
		videoElement.setAttribute('allowfullscreen', 'true');
		videoElement.className = 'td-quill-video-editing';

		videoElement.setAttribute('width', videoWidth.toString());
		videoElement.setAttribute('height', videoHeight.toString());
		videoElement.setAttribute('src', src);
		return videoElement;
	}

	buildNode(node: any, wrapper: any, videoHeight: number, videoWidth: number) {
		node.appendChild(wrapper);
		setTimeout(() => {
			let videoElement = node.getElementsByTagName('video')[0];
			videoElement.setAttribute('width', videoWidth.toString() || 300);
			videoElement.setAttribute('height', videoHeight.toString() || 150);
			node.setAttribute('contenteditable', 'false');
			node.parentElement && node.parentElement.insertBefore(createSpacer(), node);
			node.parentElement && node.parentElement.appendChild(createSpacer());
		}, 0);
		return node;
	}

	buildOverlay() {
		let overlay = document.createElement('div');
		overlay.setAttribute('class', 'td-quill-video-overlay');
		overlay.setAttribute('contenteditable', 'false');
		overlay.addEventListener('click', (event) => {
			let rootElement = getClosest(overlay, '.ql-editor');
			if (rootElement && rootElement.__blot) {
				let node = Parchment.find(overlay?.parentElement?.parentElement);
				if (node instanceof Video) {
					node.domNode.builder.select(overlay, node);
				}
			}
		});

		return overlay;
	}

	select(overlay: anynode: any) {
		this.selectedElement = overlay;
		if (this.selectedElement.className.indexOf('active') === -1) {
			this.parentElement = this.selectedElement.parentElement;
			this.node = node;
			this.videoElement = this.parentElement.getElementsByTagName('video')[0];
			this.selectedElement.setAttribute('class', 'td-quill-video-overlay active');
			this.buildResize();
			this.handelDeselect = this.deselect.bind(this);
			this.node.parent.domNode.addEventListener('click', this.handelDeselect, false);
		}
	}

	deselect(event: any) {
		if (event.target !== this.selectedElement) {
			this.selectedElement.setAttribute('class', 'td-quill-video-overlay');
			this.clearNubEvents(true);
			while (this.selectedElement.firstChild) {
				this.selectedElement.removeChild(this.selectedElement.firstChild);
			}
			this.selectedElement = null;
			this.node.parent.domNode.removeEventListener('click', this.handelDeselect, false);
		}
	}

	buildResize() {
		this.boxes = [];
		this.dragHandeler = this.handleDrag.bind(this);
		this.mouseUp = this.handelMouseUp.bind(this);
		this.mouseDown = this.handleMousedown.bind(this);
		for (let key in nubStyles) {
			let nub = this.buildNub(key);
			this.boxes.push(nub);
			this.selectedElement.appendChild(nub);
		}
		return this.selectedElement;
	}

	buildNub(pos: any) {
		let nub = document.createElement('span');
		nub.className = 'td-quill-resize-nub';
		Object.assign(nub.style, nubStyles[pos]);
		nub.addEventListener('mousedown', this.mouseDown, false);
		return nub;
	}

	handleMousedown(event: any) {
		this.dragBox = event.target;
		this.dragStartX = event.clientX;
		this.dragStartY = event.clientY;
		this.preDragWidth = parseInt(this.videoElement.width, 10) || 300;
		this.preDragHeight = parseInt(this.videoElement.height, 10) || 150;
		document.addEventListener('mousemove', this.dragHandeler, false);
		document.addEventListener('mouseup', this.mouseUp, false);
	}

	handleDrag(event: any) {
		if (!this.videoElement) {
			return;
		}
		const deltaX = event.clientX - this.dragStartX;
		const deltaY = event.clientY - this.dragStartY;
		if (this.dragBox === this.boxes[0] || this.dragBox === this.boxes[2]) {
			this.videoElement.width = Math.round(this.preDragWidth - deltaX);
			this.videoElement.height = Math.round(this.preDragHeight + deltaY);
		} else {
			this.videoElement.width = Math.round(this.preDragWidth + deltaX);
			this.videoElement.height = Math.round(this.preDragHeight + deltaY);
		}
		this.node.domNode.style.width = this.videoElement.width;
		this.node.domNode.style.height = this.videoElement.height;
	}

	handelMouseUp(event: any) {
		this.clearNubEvents(false);
	}

	clearNubEvents(include_nub: boolean) {
		for (let nub in this.boxes) {
			document.removeEventListener('mousemove', this.dragHandeler, false);
			document.removeEventListener('mouseup', this.mouseUp, false);
			if (include_nub) {
				this.boxes[nub].removeEventListener('mousedown', this.handleMousedown, false);
			}
		}
		this.node.parent.update('user');
	}
}
export default class QuillCustomVideo extends Video {
	static videoHeight: number = 150;
	static videoWidth: number = 300;
	static formats(domNode: HTMLElement): VideoAttributes {
		return ATTRIBUTES.reduce((formats, attribute) => {
			if (domNode.hasAttribute(attribute)) {
				formats[attribute] = domNode.getAttribute(attribute) || '';
			}
			return formats;
		}, {} as VideoAttributes);
	}

	format(name: string, value: any) {
		if (ATTRIBUTES.indexOf(name) > -1) {
			if (value) {
				this['domNode'].setAttribute(name, value);
			} else {
				this['domNode'].removeAttribute(name);
			}
		} else {
			super.format(name, value);
		}
	}

	static create(url: any) {
		let node = super.create();
		node.builder = new VideoBuilder();
		let wrapper = document.createElement('div');
		wrapper.className = 'td-quill-video-wrapper';
		let videoElement = node.builder.buildVideoElement(url, node, this.videoHeight, this.videoWidth);
		let overlay = node.builder.buildOverlay();
		wrapper.appendChild(videoElement);
		wrapper.appendChild(overlay);
		return node.builder.buildNode(node, wrapper, this.videoHeight, this.videoWidth);
		
	}

	static value(node: HTMLElement) {
		debugger;
		if (node.innerHTML) {
			let firstChild = node.firstChild;
			if (firstChild) {
				let secondChild = firstChild.firstChild;
				if (secondChild && secondChild instanceof HTMLVideoElement) {
					this.videoHeight = secondChild.clientHeight;
					this.videoWidth = secondChild.clientWidth;
				} else {
					console.log('No video element found or the element is not a video.');
				}
			}
		}
		return node.getElementsByTagName('video')[0].getAttribute('src');
	}
}

QuillCustomVideo.blotName = 'video';
QuillCustomVideo.tagName = 'div';
Quill.register(QuillCustomVideo, true);

Gourishmesta avatar Feb 06 '24 10:02 Gourishmesta

you can try to use https://quilljs.com/docs/api/#update. Call it with 'user' whenever the "user" is changing something you want to be triggered as a real change

KillerCodeMonkey avatar Feb 06 '24 11:02 KillerCodeMonkey

you can try to use https://quilljs.com/docs/api/#update. Call it with 'user' whenever the "user" is changing something you want to be triggered as a real change

Ya sure ill try this and check

Thanks for your quick response

Gourishmesta avatar Feb 06 '24 12:02 Gourishmesta

@Gourishmesta any news on that one?

KillerCodeMonkey avatar Feb 09 '24 10:02 KillerCodeMonkey

@Gourishmesta any news on that one?

Nope i tried calling update with user as parameter still didn't work

Gourishmesta avatar Feb 13 '24 03:02 Gourishmesta

another thing would be to check how the editor triggers changes for other formats/modules like images, links and so on.

KillerCodeMonkey avatar Feb 21 '24 14:02 KillerCodeMonkey

@KillerCodeMonkey will give it a try Thank You

Gourishmesta avatar Feb 22 '24 03:02 Gourishmesta

any updates @Gourishmesta

KillerCodeMonkey avatar Mar 11 '24 13:03 KillerCodeMonkey

closing

KillerCodeMonkey avatar Mar 26 '24 14:03 KillerCodeMonkey