forked from FINAKON/HelpProject
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.
495 lines
18 KiB
JavaScript
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 };
|