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.
325 lines
11 KiB
JavaScript
325 lines
11 KiB
JavaScript
import { TAG_ID as $, NS, NUMBERED_HEADERS } from '../common/html.js';
|
|
//Element utils
|
|
const IMPLICIT_END_TAG_REQUIRED = new Set([$.DD, $.DT, $.LI, $.OPTGROUP, $.OPTION, $.P, $.RB, $.RP, $.RT, $.RTC]);
|
|
const IMPLICIT_END_TAG_REQUIRED_THOROUGHLY = new Set([
|
|
...IMPLICIT_END_TAG_REQUIRED,
|
|
$.CAPTION,
|
|
$.COLGROUP,
|
|
$.TBODY,
|
|
$.TD,
|
|
$.TFOOT,
|
|
$.TH,
|
|
$.THEAD,
|
|
$.TR,
|
|
]);
|
|
const SCOPING_ELEMENTS_HTML = new Set([
|
|
$.APPLET,
|
|
$.CAPTION,
|
|
$.HTML,
|
|
$.MARQUEE,
|
|
$.OBJECT,
|
|
$.TABLE,
|
|
$.TD,
|
|
$.TEMPLATE,
|
|
$.TH,
|
|
]);
|
|
const SCOPING_ELEMENTS_HTML_LIST = new Set([...SCOPING_ELEMENTS_HTML, $.OL, $.UL]);
|
|
const SCOPING_ELEMENTS_HTML_BUTTON = new Set([...SCOPING_ELEMENTS_HTML, $.BUTTON]);
|
|
const SCOPING_ELEMENTS_MATHML = new Set([$.ANNOTATION_XML, $.MI, $.MN, $.MO, $.MS, $.MTEXT]);
|
|
const SCOPING_ELEMENTS_SVG = new Set([$.DESC, $.FOREIGN_OBJECT, $.TITLE]);
|
|
const TABLE_ROW_CONTEXT = new Set([$.TR, $.TEMPLATE, $.HTML]);
|
|
const TABLE_BODY_CONTEXT = new Set([$.TBODY, $.TFOOT, $.THEAD, $.TEMPLATE, $.HTML]);
|
|
const TABLE_CONTEXT = new Set([$.TABLE, $.TEMPLATE, $.HTML]);
|
|
const TABLE_CELLS = new Set([$.TD, $.TH]);
|
|
//Stack of open elements
|
|
export class OpenElementStack {
|
|
get currentTmplContentOrNode() {
|
|
return this._isInTemplate() ? this.treeAdapter.getTemplateContent(this.current) : this.current;
|
|
}
|
|
constructor(document, treeAdapter, handler) {
|
|
this.treeAdapter = treeAdapter;
|
|
this.handler = handler;
|
|
this.items = [];
|
|
this.tagIDs = [];
|
|
this.stackTop = -1;
|
|
this.tmplCount = 0;
|
|
this.currentTagId = $.UNKNOWN;
|
|
this.current = document;
|
|
}
|
|
//Index of element
|
|
_indexOf(element) {
|
|
return this.items.lastIndexOf(element, this.stackTop);
|
|
}
|
|
//Update current element
|
|
_isInTemplate() {
|
|
return this.currentTagId === $.TEMPLATE && this.treeAdapter.getNamespaceURI(this.current) === NS.HTML;
|
|
}
|
|
_updateCurrentElement() {
|
|
this.current = this.items[this.stackTop];
|
|
this.currentTagId = this.tagIDs[this.stackTop];
|
|
}
|
|
//Mutations
|
|
push(element, tagID) {
|
|
this.stackTop++;
|
|
this.items[this.stackTop] = element;
|
|
this.current = element;
|
|
this.tagIDs[this.stackTop] = tagID;
|
|
this.currentTagId = tagID;
|
|
if (this._isInTemplate()) {
|
|
this.tmplCount++;
|
|
}
|
|
this.handler.onItemPush(element, tagID, true);
|
|
}
|
|
pop() {
|
|
const popped = this.current;
|
|
if (this.tmplCount > 0 && this._isInTemplate()) {
|
|
this.tmplCount--;
|
|
}
|
|
this.stackTop--;
|
|
this._updateCurrentElement();
|
|
this.handler.onItemPop(popped, true);
|
|
}
|
|
replace(oldElement, newElement) {
|
|
const idx = this._indexOf(oldElement);
|
|
this.items[idx] = newElement;
|
|
if (idx === this.stackTop) {
|
|
this.current = newElement;
|
|
}
|
|
}
|
|
insertAfter(referenceElement, newElement, newElementID) {
|
|
const insertionIdx = this._indexOf(referenceElement) + 1;
|
|
this.items.splice(insertionIdx, 0, newElement);
|
|
this.tagIDs.splice(insertionIdx, 0, newElementID);
|
|
this.stackTop++;
|
|
if (insertionIdx === this.stackTop) {
|
|
this._updateCurrentElement();
|
|
}
|
|
if (this.current && this.currentTagId !== undefined) {
|
|
this.handler.onItemPush(this.current, this.currentTagId, insertionIdx === this.stackTop);
|
|
}
|
|
}
|
|
popUntilTagNamePopped(tagName) {
|
|
let targetIdx = this.stackTop + 1;
|
|
do {
|
|
targetIdx = this.tagIDs.lastIndexOf(tagName, targetIdx - 1);
|
|
} while (targetIdx > 0 && this.treeAdapter.getNamespaceURI(this.items[targetIdx]) !== NS.HTML);
|
|
this.shortenToLength(Math.max(targetIdx, 0));
|
|
}
|
|
shortenToLength(idx) {
|
|
while (this.stackTop >= idx) {
|
|
const popped = this.current;
|
|
if (this.tmplCount > 0 && this._isInTemplate()) {
|
|
this.tmplCount -= 1;
|
|
}
|
|
this.stackTop--;
|
|
this._updateCurrentElement();
|
|
this.handler.onItemPop(popped, this.stackTop < idx);
|
|
}
|
|
}
|
|
popUntilElementPopped(element) {
|
|
const idx = this._indexOf(element);
|
|
this.shortenToLength(Math.max(idx, 0));
|
|
}
|
|
popUntilPopped(tagNames, targetNS) {
|
|
const idx = this._indexOfTagNames(tagNames, targetNS);
|
|
this.shortenToLength(Math.max(idx, 0));
|
|
}
|
|
popUntilNumberedHeaderPopped() {
|
|
this.popUntilPopped(NUMBERED_HEADERS, NS.HTML);
|
|
}
|
|
popUntilTableCellPopped() {
|
|
this.popUntilPopped(TABLE_CELLS, NS.HTML);
|
|
}
|
|
popAllUpToHtmlElement() {
|
|
//NOTE: here we assume that the root <html> element is always first in the open element stack, so
|
|
//we perform this fast stack clean up.
|
|
this.tmplCount = 0;
|
|
this.shortenToLength(1);
|
|
}
|
|
_indexOfTagNames(tagNames, namespace) {
|
|
for (let i = this.stackTop; i >= 0; i--) {
|
|
if (tagNames.has(this.tagIDs[i]) && this.treeAdapter.getNamespaceURI(this.items[i]) === namespace) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
clearBackTo(tagNames, targetNS) {
|
|
const idx = this._indexOfTagNames(tagNames, targetNS);
|
|
this.shortenToLength(idx + 1);
|
|
}
|
|
clearBackToTableContext() {
|
|
this.clearBackTo(TABLE_CONTEXT, NS.HTML);
|
|
}
|
|
clearBackToTableBodyContext() {
|
|
this.clearBackTo(TABLE_BODY_CONTEXT, NS.HTML);
|
|
}
|
|
clearBackToTableRowContext() {
|
|
this.clearBackTo(TABLE_ROW_CONTEXT, NS.HTML);
|
|
}
|
|
remove(element) {
|
|
const idx = this._indexOf(element);
|
|
if (idx >= 0) {
|
|
if (idx === this.stackTop) {
|
|
this.pop();
|
|
}
|
|
else {
|
|
this.items.splice(idx, 1);
|
|
this.tagIDs.splice(idx, 1);
|
|
this.stackTop--;
|
|
this._updateCurrentElement();
|
|
this.handler.onItemPop(element, false);
|
|
}
|
|
}
|
|
}
|
|
//Search
|
|
tryPeekProperlyNestedBodyElement() {
|
|
//Properly nested <body> element (should be second element in stack).
|
|
return this.stackTop >= 1 && this.tagIDs[1] === $.BODY ? this.items[1] : null;
|
|
}
|
|
contains(element) {
|
|
return this._indexOf(element) > -1;
|
|
}
|
|
getCommonAncestor(element) {
|
|
const elementIdx = this._indexOf(element) - 1;
|
|
return elementIdx >= 0 ? this.items[elementIdx] : null;
|
|
}
|
|
isRootHtmlElementCurrent() {
|
|
return this.stackTop === 0 && this.tagIDs[0] === $.HTML;
|
|
}
|
|
//Element in scope
|
|
hasInDynamicScope(tagName, htmlScope) {
|
|
for (let i = this.stackTop; i >= 0; i--) {
|
|
const tn = this.tagIDs[i];
|
|
switch (this.treeAdapter.getNamespaceURI(this.items[i])) {
|
|
case NS.HTML: {
|
|
if (tn === tagName)
|
|
return true;
|
|
if (htmlScope.has(tn))
|
|
return false;
|
|
break;
|
|
}
|
|
case NS.SVG: {
|
|
if (SCOPING_ELEMENTS_SVG.has(tn))
|
|
return false;
|
|
break;
|
|
}
|
|
case NS.MATHML: {
|
|
if (SCOPING_ELEMENTS_MATHML.has(tn))
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
hasInScope(tagName) {
|
|
return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML);
|
|
}
|
|
hasInListItemScope(tagName) {
|
|
return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML_LIST);
|
|
}
|
|
hasInButtonScope(tagName) {
|
|
return this.hasInDynamicScope(tagName, SCOPING_ELEMENTS_HTML_BUTTON);
|
|
}
|
|
hasNumberedHeaderInScope() {
|
|
for (let i = this.stackTop; i >= 0; i--) {
|
|
const tn = this.tagIDs[i];
|
|
switch (this.treeAdapter.getNamespaceURI(this.items[i])) {
|
|
case NS.HTML: {
|
|
if (NUMBERED_HEADERS.has(tn))
|
|
return true;
|
|
if (SCOPING_ELEMENTS_HTML.has(tn))
|
|
return false;
|
|
break;
|
|
}
|
|
case NS.SVG: {
|
|
if (SCOPING_ELEMENTS_SVG.has(tn))
|
|
return false;
|
|
break;
|
|
}
|
|
case NS.MATHML: {
|
|
if (SCOPING_ELEMENTS_MATHML.has(tn))
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
hasInTableScope(tagName) {
|
|
for (let i = this.stackTop; i >= 0; i--) {
|
|
if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) {
|
|
continue;
|
|
}
|
|
switch (this.tagIDs[i]) {
|
|
case tagName: {
|
|
return true;
|
|
}
|
|
case $.TABLE:
|
|
case $.HTML: {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
hasTableBodyContextInTableScope() {
|
|
for (let i = this.stackTop; i >= 0; i--) {
|
|
if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) {
|
|
continue;
|
|
}
|
|
switch (this.tagIDs[i]) {
|
|
case $.TBODY:
|
|
case $.THEAD:
|
|
case $.TFOOT: {
|
|
return true;
|
|
}
|
|
case $.TABLE:
|
|
case $.HTML: {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
hasInSelectScope(tagName) {
|
|
for (let i = this.stackTop; i >= 0; i--) {
|
|
if (this.treeAdapter.getNamespaceURI(this.items[i]) !== NS.HTML) {
|
|
continue;
|
|
}
|
|
switch (this.tagIDs[i]) {
|
|
case tagName: {
|
|
return true;
|
|
}
|
|
case $.OPTION:
|
|
case $.OPTGROUP: {
|
|
break;
|
|
}
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
//Implied end tags
|
|
generateImpliedEndTags() {
|
|
while (this.currentTagId !== undefined && IMPLICIT_END_TAG_REQUIRED.has(this.currentTagId)) {
|
|
this.pop();
|
|
}
|
|
}
|
|
generateImpliedEndTagsThoroughly() {
|
|
while (this.currentTagId !== undefined && IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) {
|
|
this.pop();
|
|
}
|
|
}
|
|
generateImpliedEndTagsWithExclusion(exclusionId) {
|
|
while (this.currentTagId !== undefined &&
|
|
this.currentTagId !== exclusionId &&
|
|
IMPLICIT_END_TAG_REQUIRED_THOROUGHLY.has(this.currentTagId)) {
|
|
this.pop();
|
|
}
|
|
}
|
|
}
|