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

495 lines
18 KiB
JavaScript

import clone from 'clone';
import equal from 'deep-equal';
import extend from 'extend';
import Delta from 'quill-delta';
import DeltaOp from 'quill-delta/lib/op';
import Parchment from 'parchment';
import Quill from '../core/quill';
import logger from '../core/logger';
import Module from '../core/module';
let debug = logger('quill:keyboard');
const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';
class Keyboard extends Module {
static match(evt, binding) {
binding = normalize(binding);
if (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].some(function(key) {
return (!!binding[key] !== evt[key] && binding[key] !== null);
})) {
return false;
}
return binding.key === (evt.which || evt.keyCode);
}
constructor(quill, options) {
super(quill, options);
this.bindings = {};
Object.keys(this.options.bindings).forEach((name) => {
if (name === 'list autofill' &&
quill.scroll.whitelist != null &&
!quill.scroll.whitelist['list']) {
return;
}
if (this.options.bindings[name]) {
this.addBinding(this.options.bindings[name]);
}
});
this.addBinding({ key: Keyboard.keys.ENTER, shiftKey: null }, handleEnter);
this.addBinding({ key: Keyboard.keys.ENTER, metaKey: null, ctrlKey: null, altKey: null }, function() {});
if (/Firefox/i.test(navigator.userAgent)) {
// Need to handle delete and backspace for Firefox in the general case #1171
this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true }, handleBackspace);
this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true }, handleDelete);
} else {
this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true, prefix: /^.?$/ }, handleBackspace);
this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true, suffix: /^.?$/ }, handleDelete);
}
this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: false }, handleDeleteRange);
this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: false }, handleDeleteRange);
this.addBinding({ key: Keyboard.keys.BACKSPACE, altKey: null, ctrlKey: null, metaKey: null, shiftKey: null },
{ collapsed: true, offset: 0 },
handleBackspace);
this.listen();
}
addBinding(key, context = {}, handler = {}) {
let binding = normalize(key);
if (binding == null || binding.key == null) {
return debug.warn('Attempted to add invalid keyboard binding', binding);
}
if (typeof context === 'function') {
context = { handler: context };
}
if (typeof handler === 'function') {
handler = { handler: handler };
}
binding = extend(binding, context, handler);
this.bindings[binding.key] = this.bindings[binding.key] || [];
this.bindings[binding.key].push(binding);
}
listen() {
this.quill.root.addEventListener('keydown', (evt) => {
if (evt.defaultPrevented) return;
let which = evt.which || evt.keyCode;
let bindings = (this.bindings[which] || []).filter(function(binding) {
return Keyboard.match(evt, binding);
});
if (bindings.length === 0) return;
let range = this.quill.getSelection();
if (range == null || !this.quill.hasFocus()) return;
let [line, offset] = this.quill.getLine(range.index);
let [leafStart, offsetStart] = this.quill.getLeaf(range.index);
let [leafEnd, offsetEnd] = range.length === 0 ? [leafStart, offsetStart] : this.quill.getLeaf(range.index + range.length);
let prefixText = leafStart instanceof Parchment.Text ? leafStart.value().slice(0, offsetStart) : '';
let suffixText = leafEnd instanceof Parchment.Text ? leafEnd.value().slice(offsetEnd) : '';
let curContext = {
collapsed: range.length === 0,
empty: range.length === 0 && line.length() <= 1,
format: this.quill.getFormat(range),
offset: offset,
prefix: prefixText,
suffix: suffixText
};
let prevented = bindings.some((binding) => {
if (binding.collapsed != null && binding.collapsed !== curContext.collapsed) return false;
if (binding.empty != null && binding.empty !== curContext.empty) return false;
if (binding.offset != null && binding.offset !== curContext.offset) return false;
if (Array.isArray(binding.format)) {
// any format is present
if (binding.format.every(function(name) {
return curContext.format[name] == null;
})) {
return false;
}
} else if (typeof binding.format === 'object') {
// all formats must match
if (!Object.keys(binding.format).every(function(name) {
if (binding.format[name] === true) return curContext.format[name] != null;
if (binding.format[name] === false) return curContext.format[name] == null;
return equal(binding.format[name], curContext.format[name]);
})) {
return false;
}
}
if (binding.prefix != null && !binding.prefix.test(curContext.prefix)) return false;
if (binding.suffix != null && !binding.suffix.test(curContext.suffix)) return false;
return binding.handler.call(this, range, curContext) !== true;
});
if (prevented) {
evt.preventDefault();
}
});
}
}
Keyboard.keys = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
ESCAPE: 27,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
DELETE: 46
};
Keyboard.DEFAULTS = {
bindings: {
'bold' : makeFormatHandler('bold'),
'italic' : makeFormatHandler('italic'),
'underline' : makeFormatHandler('underline'),
'indent': {
// highlight tab or tab at beginning of list, indent or blockquote
key: Keyboard.keys.TAB,
format: ['blockquote', 'indent', 'list'],
handler: function(range, context) {
if (context.collapsed && context.offset !== 0) return true;
this.quill.format('indent', '+1', Quill.sources.USER);
}
},
'outdent': {
key: Keyboard.keys.TAB,
shiftKey: true,
format: ['blockquote', 'indent', 'list'],
// highlight tab or tab at beginning of list, indent or blockquote
handler: function(range, context) {
if (context.collapsed && context.offset !== 0) return true;
this.quill.format('indent', '-1', Quill.sources.USER);
}
},
'outdent backspace': {
key: Keyboard.keys.BACKSPACE,
collapsed: true,
shiftKey: null,
metaKey: null,
ctrlKey: null,
altKey: null,
format: ['indent', 'list'],
offset: 0,
handler: function(range, context) {
if (context.format.indent != null) {
this.quill.format('indent', '-1', Quill.sources.USER);
} else if (context.format.list != null) {
this.quill.format('list', false, Quill.sources.USER);
}
}
},
'indent code-block': makeCodeBlockHandler(true),
'outdent code-block': makeCodeBlockHandler(false),
'remove tab': {
key: Keyboard.keys.TAB,
shiftKey: true,
collapsed: true,
prefix: /\t$/,
handler: function(range) {
this.quill.deleteText(range.index - 1, 1, Quill.sources.USER);
}
},
'tab': {
key: Keyboard.keys.TAB,
handler: function(range) {
this.quill.history.cutoff();
let delta = new Delta().retain(range.index)
.delete(range.length)
.insert('\t');
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.history.cutoff();
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
}
},
'list empty enter': {
key: Keyboard.keys.ENTER,
collapsed: true,
format: ['list'],
empty: true,
handler: function(range, context) {
this.quill.format('list', false, Quill.sources.USER);
if (context.format.indent) {
this.quill.format('indent', false, Quill.sources.USER);
}
}
},
'checklist enter': {
key: Keyboard.keys.ENTER,
collapsed: true,
format: { list: 'checked' },
handler: function(range) {
let [line, offset] = this.quill.getLine(range.index);
let formats = extend({}, line.formats(), { list: 'checked' });
let delta = new Delta().retain(range.index)
.insert('\n', formats)
.retain(line.length() - offset - 1)
.retain(1, { list: 'unchecked' });
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
this.quill.scrollIntoView();
}
},
'header enter': {
key: Keyboard.keys.ENTER,
collapsed: true,
format: ['header'],
suffix: /^$/,
handler: function(range, context) {
let [line, offset] = this.quill.getLine(range.index);
let delta = new Delta().retain(range.index)
.insert('\n', context.format)
.retain(line.length() - offset - 1)
.retain(1, { header: null });
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
this.quill.scrollIntoView();
}
},
'list autofill': {
key: ' ',
collapsed: true,
format: { list: false },
prefix: /^\s*?(\d+\.|-|\*|\[ ?\]|\[x\])$/,
handler: function(range, context) {
let length = context.prefix.length;
let [line, offset] = this.quill.getLine(range.index);
if (offset > length) return true;
let value;
switch (context.prefix.trim()) {
case '[]': case '[ ]':
value = 'unchecked';
break;
case '[x]':
value = 'checked';
break;
case '-': case '*':
value = 'bullet';
break;
default:
value = 'ordered';
}
this.quill.insertText(range.index, ' ', Quill.sources.USER);
this.quill.history.cutoff();
let delta = new Delta().retain(range.index - offset)
.delete(length + 1)
.retain(line.length() - 2 - offset)
.retain(1, { list: value });
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.history.cutoff();
this.quill.setSelection(range.index - length, Quill.sources.SILENT);
}
},
'code exit': {
key: Keyboard.keys.ENTER,
collapsed: true,
format: ['code-block'],
prefix: /\n\n$/,
suffix: /^\s+$/,
handler: function(range) {
const [line, offset] = this.quill.getLine(range.index);
const delta = new Delta()
.retain(range.index + line.length() - offset - 2)
.retain(1, { 'code-block': null })
.delete(1);
this.quill.updateContents(delta, Quill.sources.USER);
}
},
'embed left': makeEmbedArrowHandler(Keyboard.keys.LEFT, false),
'embed left shift': makeEmbedArrowHandler(Keyboard.keys.LEFT, true),
'embed right': makeEmbedArrowHandler(Keyboard.keys.RIGHT, false),
'embed right shift': makeEmbedArrowHandler(Keyboard.keys.RIGHT, true)
}
};
function makeEmbedArrowHandler(key, shiftKey) {
const where = key === Keyboard.keys.LEFT ? 'prefix' : 'suffix';
return {
key,
shiftKey,
altKey: null,
[where]: /^$/,
handler: function(range) {
let index = range.index;
if (key === Keyboard.keys.RIGHT) {
index += (range.length + 1);
}
const [leaf, ] = this.quill.getLeaf(index);
if (!(leaf instanceof Parchment.Embed)) return true;
if (key === Keyboard.keys.LEFT) {
if (shiftKey) {
this.quill.setSelection(range.index - 1, range.length + 1, Quill.sources.USER);
} else {
this.quill.setSelection(range.index - 1, Quill.sources.USER);
}
} else {
if (shiftKey) {
this.quill.setSelection(range.index, range.length + 1, Quill.sources.USER);
} else {
this.quill.setSelection(range.index + range.length + 1, Quill.sources.USER);
}
}
return false;
}
};
}
function handleBackspace(range, context) {
if (range.index === 0 || this.quill.getLength() <= 1) return;
let [line, ] = this.quill.getLine(range.index);
let formats = {};
if (context.offset === 0) {
let [prev, ] = this.quill.getLine(range.index - 1);
if (prev != null && prev.length() > 1) {
let curFormats = line.formats();
let prevFormats = this.quill.getFormat(range.index-1, 1);
formats = DeltaOp.attributes.diff(curFormats, prevFormats) || {};
}
}
// Check for astral symbols
let length = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(context.prefix) ? 2 : 1;
this.quill.deleteText(range.index-length, length, Quill.sources.USER);
if (Object.keys(formats).length > 0) {
this.quill.formatLine(range.index-length, length, formats, Quill.sources.USER);
}
this.quill.focus();
}
function handleDelete(range, context) {
// Check for astral symbols
let length = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(context.suffix) ? 2 : 1;
if (range.index >= this.quill.getLength() - length) return;
let formats = {}, nextLength = 0;
let [line, ] = this.quill.getLine(range.index);
if (context.offset >= line.length() - 1) {
let [next, ] = this.quill.getLine(range.index + 1);
if (next) {
let curFormats = line.formats();
let nextFormats = this.quill.getFormat(range.index, 1);
formats = DeltaOp.attributes.diff(curFormats, nextFormats) || {};
nextLength = next.length();
}
}
this.quill.deleteText(range.index, length, Quill.sources.USER);
if (Object.keys(formats).length > 0) {
this.quill.formatLine(range.index + nextLength - 1, length, formats, Quill.sources.USER);
}
}
function handleDeleteRange(range) {
let lines = this.quill.getLines(range);
let formats = {};
if (lines.length > 1) {
let firstFormats = lines[0].formats();
let lastFormats = lines[lines.length - 1].formats();
formats = DeltaOp.attributes.diff(lastFormats, firstFormats) || {};
}
this.quill.deleteText(range, Quill.sources.USER);
if (Object.keys(formats).length > 0) {
this.quill.formatLine(range.index, 1, formats, Quill.sources.USER);
}
this.quill.setSelection(range.index, Quill.sources.SILENT);
this.quill.focus();
}
function handleEnter(range, context) {
if (range.length > 0) {
this.quill.scroll.deleteAt(range.index, range.length); // So we do not trigger text-change
}
let lineFormats = Object.keys(context.format).reduce(function(lineFormats, format) {
if (Parchment.query(format, Parchment.Scope.BLOCK) && !Array.isArray(context.format[format])) {
lineFormats[format] = context.format[format];
}
return lineFormats;
}, {});
this.quill.insertText(range.index, '\n', lineFormats, Quill.sources.USER);
// Earlier scroll.deleteAt might have messed up our selection,
// so insertText's built in selection preservation is not reliable
this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
this.quill.focus();
Object.keys(context.format).forEach((name) => {
if (lineFormats[name] != null) return;
if (Array.isArray(context.format[name])) return;
if (name === 'link') return;
this.quill.format(name, context.format[name], Quill.sources.USER);
});
}
function makeCodeBlockHandler(indent) {
return {
key: Keyboard.keys.TAB,
shiftKey: !indent,
format: {'code-block': true },
handler: function(range) {
let CodeBlock = Parchment.query('code-block');
let index = range.index, length = range.length;
let [block, offset] = this.quill.scroll.descendant(CodeBlock, index);
if (block == null) return;
let scrollIndex = this.quill.getIndex(block);
let start = block.newlineIndex(offset, true) + 1;
let end = block.newlineIndex(scrollIndex + offset + length);
let lines = block.domNode.textContent.slice(start, end).split('\n');
offset = 0;
lines.forEach((line, i) => {
if (indent) {
block.insertAt(start + offset, CodeBlock.TAB);
offset += CodeBlock.TAB.length;
if (i === 0) {
index += CodeBlock.TAB.length;
} else {
length += CodeBlock.TAB.length;
}
} else if (line.startsWith(CodeBlock.TAB)) {
block.deleteAt(start + offset, CodeBlock.TAB.length);
offset -= CodeBlock.TAB.length;
if (i === 0) {
index -= CodeBlock.TAB.length;
} else {
length -= CodeBlock.TAB.length;
}
}
offset += line.length + 1;
});
this.quill.update(Quill.sources.USER);
this.quill.setSelection(index, length, Quill.sources.SILENT);
}
};
}
function makeFormatHandler(format) {
return {
key: format[0].toUpperCase(),
shortKey: true,
handler: function(range, context) {
this.quill.format(format, !context.format[format], Quill.sources.USER);
}
};
}
function normalize(binding) {
if (typeof binding === 'string' || typeof binding === 'number') {
return normalize({ key: binding });
}
if (typeof binding === 'object') {
binding = clone(binding, false);
}
if (typeof binding.key === 'string') {
if (Keyboard.keys[binding.key.toUpperCase()] != null) {
binding.key = Keyboard.keys[binding.key.toUpperCase()];
} else if (binding.key.length === 1) {
binding.key = binding.key.toUpperCase().charCodeAt(0);
} else {
return null;
}
}
if (binding.shortKey) {
binding[SHORTKEY] = binding.shortKey;
delete binding.shortKey;
}
return binding;
}
export { Keyboard as default, SHORTKEY };