Bhargava 6063bd1724 Help Project:
1. Initial Commit - a boiler plate code and POC to realize the concept of context
sensitive help
2. Frontend code written in ReactJS
3. Backend code written in Java, Spring Boot Framework
4. Frontend Start:
        pre-requisites : node, npm
	npm run dev  ==> to start the frontend vite server
5. Backend Start:
	pre-requisites : java, mvn
        mvn spring-boot:run  ==> to start the backend server
6. Visit http://localhost:5173/ for basic demo of help, press F1 in textboxes
7. Visit http://localhost:5173/editor and enter "admin123" to add/modify texts.

Happy Coding !!!

Thank you,
Bhargava.
2025-07-04 15:54:13 +05:30

265 lines
8.6 KiB
JavaScript

import extend from 'extend';
import Delta from 'quill-delta';
import Emitter from '../core/emitter';
import Keyboard from '../modules/keyboard';
import Theme from '../core/theme';
import ColorPicker from '../ui/color-picker';
import IconPicker from '../ui/icon-picker';
import Picker from '../ui/picker';
import Tooltip from '../ui/tooltip';
const ALIGNS = [ false, 'center', 'right', 'justify' ];
const COLORS = [
"#000000", "#e60000", "#ff9900", "#ffff00", "#008a00", "#0066cc", "#9933ff",
"#ffffff", "#facccc", "#ffebcc", "#ffffcc", "#cce8cc", "#cce0f5", "#ebd6ff",
"#bbbbbb", "#f06666", "#ffc266", "#ffff66", "#66b966", "#66a3e0", "#c285ff",
"#888888", "#a10000", "#b26b00", "#b2b200", "#006100", "#0047b2", "#6b24b2",
"#444444", "#5c0000", "#663d00", "#666600", "#003700", "#002966", "#3d1466"
];
const FONTS = [ false, 'serif', 'monospace' ];
const HEADERS = [ '1', '2', '3', false ];
const SIZES = [ 'small', false, 'large', 'huge' ];
class BaseTheme extends Theme {
constructor(quill, options) {
super(quill, options);
let listener = (e) => {
if (!document.body.contains(quill.root)) {
return document.body.removeEventListener('click', listener);
}
if (this.tooltip != null && !this.tooltip.root.contains(e.target) &&
document.activeElement !== this.tooltip.textbox && !this.quill.hasFocus()) {
this.tooltip.hide();
}
if (this.pickers != null) {
this.pickers.forEach(function(picker) {
if (!picker.container.contains(e.target)) {
picker.close();
}
});
}
};
quill.emitter.listenDOM('click', document.body, listener);
}
addModule(name) {
let module = super.addModule(name);
if (name === 'toolbar') {
this.extendToolbar(module);
}
return module;
}
buildButtons(buttons, icons) {
buttons.forEach((button) => {
let className = button.getAttribute('class') || '';
className.split(/\s+/).forEach((name) => {
if (!name.startsWith('ql-')) return;
name = name.slice('ql-'.length);
if (icons[name] == null) return;
if (name === 'direction') {
button.innerHTML = icons[name][''] + icons[name]['rtl'];
} else if (typeof icons[name] === 'string') {
button.innerHTML = icons[name];
} else {
let value = button.value || '';
if (value != null && icons[name][value]) {
button.innerHTML = icons[name][value];
}
}
});
});
}
buildPickers(selects, icons) {
this.pickers = selects.map((select) => {
if (select.classList.contains('ql-align')) {
if (select.querySelector('option') == null) {
fillSelect(select, ALIGNS);
}
return new IconPicker(select, icons.align);
} else if (select.classList.contains('ql-background') || select.classList.contains('ql-color')) {
let format = select.classList.contains('ql-background') ? 'background' : 'color';
if (select.querySelector('option') == null) {
fillSelect(select, COLORS, format === 'background' ? '#ffffff' : '#000000');
}
return new ColorPicker(select, icons[format]);
} else {
if (select.querySelector('option') == null) {
if (select.classList.contains('ql-font')) {
fillSelect(select, FONTS);
} else if (select.classList.contains('ql-header')) {
fillSelect(select, HEADERS);
} else if (select.classList.contains('ql-size')) {
fillSelect(select, SIZES);
}
}
return new Picker(select);
}
});
let update = () => {
this.pickers.forEach(function(picker) {
picker.update();
});
};
this.quill.on(Emitter.events.EDITOR_CHANGE, update);
}
}
BaseTheme.DEFAULTS = extend(true, {}, Theme.DEFAULTS, {
modules: {
toolbar: {
handlers: {
formula: function() {
this.quill.theme.tooltip.edit('formula');
},
image: function() {
let fileInput = this.container.querySelector('input.ql-image[type=file]');
if (fileInput == null) {
fileInput = document.createElement('input');
fileInput.setAttribute('type', 'file');
fileInput.setAttribute('accept', 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon');
fileInput.classList.add('ql-image');
fileInput.addEventListener('change', () => {
if (fileInput.files != null && fileInput.files[0] != null) {
let reader = new FileReader();
reader.onload = (e) => {
let range = this.quill.getSelection(true);
this.quill.updateContents(new Delta()
.retain(range.index)
.delete(range.length)
.insert({ image: e.target.result })
, Emitter.sources.USER);
this.quill.setSelection(range.index + 1, Emitter.sources.SILENT);
fileInput.value = "";
}
reader.readAsDataURL(fileInput.files[0]);
}
});
this.container.appendChild(fileInput);
}
fileInput.click();
},
video: function() {
this.quill.theme.tooltip.edit('video');
}
}
}
}
});
class BaseTooltip extends Tooltip {
constructor(quill, boundsContainer) {
super(quill, boundsContainer);
this.textbox = this.root.querySelector('input[type="text"]');
this.listen();
}
listen() {
this.textbox.addEventListener('keydown', (event) => {
if (Keyboard.match(event, 'enter')) {
this.save();
event.preventDefault();
} else if (Keyboard.match(event, 'escape')) {
this.cancel();
event.preventDefault();
}
});
}
cancel() {
this.hide();
}
edit(mode = 'link', preview = null) {
this.root.classList.remove('ql-hidden');
this.root.classList.add('ql-editing');
if (preview != null) {
this.textbox.value = preview;
} else if (mode !== this.root.getAttribute('data-mode')) {
this.textbox.value = '';
}
this.position(this.quill.getBounds(this.quill.selection.savedRange));
this.textbox.select();
this.textbox.setAttribute('placeholder', this.textbox.getAttribute(`data-${mode}`) || '');
this.root.setAttribute('data-mode', mode);
}
restoreFocus() {
let scrollTop = this.quill.scrollingContainer.scrollTop;
this.quill.focus();
this.quill.scrollingContainer.scrollTop = scrollTop;
}
save() {
let value = this.textbox.value;
switch(this.root.getAttribute('data-mode')) {
case 'link': {
let scrollTop = this.quill.root.scrollTop;
if (this.linkRange) {
this.quill.formatText(this.linkRange, 'link', value, Emitter.sources.USER);
delete this.linkRange;
} else {
this.restoreFocus();
this.quill.format('link', value, Emitter.sources.USER);
}
this.quill.root.scrollTop = scrollTop;
break;
}
case 'video': {
value = extractVideoUrl(value);
} // eslint-disable-next-line no-fallthrough
case 'formula': {
if (!value) break;
let range = this.quill.getSelection(true);
if (range != null) {
let index = range.index + range.length;
this.quill.insertEmbed(index, this.root.getAttribute('data-mode'), value, Emitter.sources.USER);
if (this.root.getAttribute('data-mode') === 'formula') {
this.quill.insertText(index + 1, ' ', Emitter.sources.USER);
}
this.quill.setSelection(index + 2, Emitter.sources.USER);
}
break;
}
default:
}
this.textbox.value = '';
this.hide();
}
}
function extractVideoUrl(url) {
let match = url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/) ||
url.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/);
if (match) {
return (match[1] || 'https') + '://www.youtube.com/embed/' + match[2] + '?showinfo=0';
}
if (match = url.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/)) { // eslint-disable-line no-cond-assign
return (match[1] || 'https') + '://player.vimeo.com/video/' + match[2] + '/';
}
return url;
}
function fillSelect(select, values, defaultValue = false) {
values.forEach(function(value) {
let option = document.createElement('option');
if (value === defaultValue) {
option.setAttribute('selected', 'selected');
} else {
option.setAttribute('value', value);
}
select.appendChild(option);
});
}
export { BaseTooltip, BaseTheme as default };