addHandlers(): void { this.domlistener.addHandler( "included-element", util.classFromOriginalName("*", {}), (root: Node, _tree: Node, _parent: Node, _prev: Node | null, _next: Node | null, el: Element) => { // Skip elements which would already have been removed from the // tree. Unlikely but... if (!root.contains(el)) { return; } this.elementDecorator(root as Element, el); const klass = this.getAdditionalClasses(el); if (klass.length > 0) { el.className += ` ${klass}`; } }); this.domlistener.addHandler( "children-changed", util.classFromOriginalName("*", {}), (root: Node, added: Node[], removed: Node[], _previousSibling: Node | null, _nextSibling: Node | null, el: Element) => { for (const child of added.concat(removed)) { if (isText(child) || (isElement(child) && (child.classList.contains("_real") || child.classList.contains("_phantom_wrap")))) { this.elementDecorator(root as Element, el); break; } } }); this.domlistener.addHandler("text-changed", util.classFromOriginalName("*", {}), (root: Node, node: Text) => { this.elementDecorator( root as Element, node.parentNode! as Element); }); this.domlistener.addHandler("attribute-changed", util.classFromOriginalName("*", {}), (root: Node, el: Element) => { this.elementDecorator(root as Element, el); }); }
mode.getBibliographicalInfo().then((info: BibliographicalInfo) => { const allValues: BibliographicalItem[] = []; for (const key of Object.keys(info)) { allValues.push(info[key]); } const citedValues: BibliographicalItem[] = []; const refs = editor.guiRoot.querySelectorAll("._real.ref"); // tslint:disable-next-line:prefer-for-of for (let refIx = 0; refIx < refs.length; ++refIx) { const ref = refs[refIx]; const origTarget = ref.getAttribute(util.encodeAttrName("target"))!; if (origTarget.lastIndexOf("/bibliography/", 0) !== 0) { continue; } citedValues.push(info[origTarget]); } zoteroEngine.add(allValues); citedEngine.add(citedValues); if (range !== undefined) { ta.setValue(range.toString()); } ta.hideSpinner(); });
export function insertPtr(editor: EditorAPI, data: TargetedTransformationData): void { const caret = editor.caretManager.getDataCaret()!; let parent = caret.node; const index = caret.offset; // The data.target value is the wed ID target of the ptr. We must find this // element and add a data ID. const target = editor.guiRoot.ownerDocument.getElementById(data.target)!; const dataId = data.target.slice(4); target.setAttribute(util.encodeAttrName("xml:id"), dataId); $.data(target, "wed_mirror_node").setAttributeNS( // tslint:disable-next-line:no-http-string "http://www.w3.org/XML/1998/namespace", "xml:id", dataId); const mode = editor.modeTree.getMode(parent); const ename = mode.getAbsoluteResolver().resolveName("ptr")!; const ptr = makeElement(parent.ownerDocument, ename.ns, "ptr", { target: `#${dataId}` }); editor.dataUpdater.insertAt(parent, index, ptr); // The original parent and index information are no necessarily representative // because insertAt can do quite a lot of things to insert the node. parent = ptr.parentNode!; editor.caretManager.setCaret(parent, _indexOf.call(parent.childNodes, ptr)); }
citDecorator(root: Element, el: Element): void { this.elementDecorator(root, el); let ref; let child = el.firstElementChild; while (child !== null) { const next = child.nextElementSibling; if (child.classList.contains("_ref_space") || child.classList.contains("_cit_bullet")) { this.guiUpdater.removeNode(child); } else if (child.classList.contains("ref")) { ref = child; } child = next; } if (ref) { const space = el.ownerDocument.createElement("div"); space.className = "_text _phantom _ref_space"; space.textContent = " "; el.insertBefore(space, ref.nextSibling); } if (el.querySelector(`*[${util.encodeAttrName("xml:lang")}='pi-Latn']`) !== null) { const div = el.ownerDocument.createElement("div"); div.className = "_phantom _text _cit_bullet"; div.style.position = "absolute"; div.style.left = "-1em"; div.textContent = WHEEL; this.guiUpdater.insertNodeAt(el, 0, div); (el as HTMLElement).style.position = "relative"; } }
idDecorator(_root: Element, el: Element): void { const refman = this.refmans.getRefmanForElement(el); if (refman !== null) { let wedId = el.id; if (wedId === "") { const id = el.getAttribute(util.encodeAttrName("xml:id")); const idMan = this._getIDManagerForRefman(refman); wedId = `BTW-${id !== null ? id : idMan.generate()}`; el.id = wedId; } // We have some reference managers that don't derive from ReferenceManager // and thus do not have this method. if (refman instanceof LabelManager) { refman.allocateLabel(wedId); } } }
execute(data: TransformationData): void { const editor = this.editor; const dataCaret = editor.caretManager.getDataCaret(true)!; const mode = editor.modeTree.getMode(dataCaret.node); if (!(mode instanceof Mode)) { throw new Error("expected BTW mode"); } const decorator = editor.modeTree.getDecorator(dataCaret.node); if (!(decorator instanceof BTWDecorator)) { throw new Error("our decorator must be a BTWDecorator"); } const doc = editor.guiRoot.ownerDocument; const mappings = mode.getAbsoluteNamespaceMappings(); const senses = editor.guiRoot.querySelectorAll( util.classFromOriginalName("btw:sense", mappings)); const labels: Element[] = []; const radios: Element[] = []; // tslint:disable-next-line:prefer-for-of for (let i = 0; i < senses.length; ++i) { const sense = senses[i]; let dataNode = $.data(sense, "wed_mirror_node"); const termNodes = btwUtil.termsForSense(sense, mappings); const terms: string[] = []; // tslint:disable-next-line:prefer-for-of for (let tix = 0; tix < termNodes.length; ++tix) { terms.push($.data(termNodes[tix], "wed_mirror_node").textContent); } const senseLabel = decorator.refmans.getSenseLabel(sense); let span = doc.createElement("span"); span.textContent = ` [${senseLabel}] ${terms.join(", ")}`; span.setAttribute("data-wed-id", sense.id); let radio = doc.createElement("input"); radio.type = "radio"; radio.name = "sense"; let div = doc.createElement("div"); div.appendChild(radio); div.appendChild(span); labels.push(div); radios.push(radio); const subsenses = domutil.childrenByClass(sense, "btw:subsense"); for (const subsense of subsenses) { dataNode = $.data(subsense, "wed_mirror_node"); const subsenseLabel = decorator.refmans.getSubsenseLabel(subsense); let child = dataNode.firstElementChild; let explanation; while (child) { if (child.tagName === "btw:explanation") { explanation = child; break; } child = child.nextElementSibling; } span = doc.createElement("span"); span.textContent = ` [${subsenseLabel}] ${explanation.textContent}`; span.setAttribute("data-wed-id", subsense.id); radio = doc.createElement("input"); radio.type = "radio"; radio.name = "sense"; div = doc.createElement("div"); div.appendChild(radio); div.appendChild(span); labels.push(div); radios.push(radio); } } const hyperlinkModal = mode.hyperlinkModal; const primary = hyperlinkModal.getPrimary()[0] as HTMLButtonElement; const body = doc.createElement("div"); for (const label of labels) { body.appendChild(label); } $(radios).on("click.wed", () => { primary.disabled = false; primary.classList.remove("disabled"); }); primary.disabled = true; primary.classList.add("disabled"); hyperlinkModal.setBody(body); hyperlinkModal.modal(() => { const clicked = hyperlinkModal.getClickedAsText(); if (clicked === "Insert") { const id = body.querySelector("input[type='radio']:checked")! .nextElementSibling!.getAttribute("data-wed-id")!; mode.insertPtrTr.execute({ ...data, target: id }); } }); }
execute(data: TransformationData): void { const editor = this.editor; const dataCaret = editor.caretManager.getDataCaret(true)!; const mode = editor.modeTree.getMode(dataCaret.node); if (!(mode instanceof Mode)) { throw new Error("expected BTW mode"); } const doc = editor.guiRoot.ownerDocument; const mappings = mode.getAbsoluteNamespaceMappings(); const examples = editor.guiRoot.querySelectorAll(domutil.toGUISelector( "btw:example, btw:example-explained", mappings)); const labels: Element[] = []; const radios: Element[] = []; // tslint:disable-next-line:prefer-for-of for (let i = 0; i < examples.length; ++i) { const example = examples[i]; const dataNode = $.data(example, "wed_mirror_node"); let child = dataNode.firstElementChild; let cit; while (child) { if (child.tagName === "btw:cit") { cit = child; break; } child = child.nextElementSibling; } const abbr = example.querySelector(util.classFromOriginalName("ref", mappings)); // We skip those examples that do not have a ref in them yet, as links to // them are meaningless. if (abbr === null) { continue; } const abbrCopy = abbr.cloneNode(true) as Element; child = abbrCopy.firstElementChild; while (child) { const next = child.nextElementSibling; if (child.classList.contains("_gui")) { abbrCopy.removeChild(child); } child = next; } const span = doc.createElement("span"); span.setAttribute("data-wed-id", example.id); span.textContent = ` ${abbrCopy.textContent} ${cit.textContent}`; const radio = doc.createElement("input"); radio.type = "radio"; radio.name = "example"; const div = doc.createElement("div"); div.appendChild(radio); div.appendChild(span); labels.push(div); radios.push(radio); } const hyperlinkModal = mode.hyperlinkModal; const primary = hyperlinkModal.getPrimary()[0] as HTMLButtonElement; const body = doc.createElement("div"); for (const label of labels) { body.appendChild(label); } $(radios).on("click.wed", () => { primary.disabled = false; primary.classList.remove("disabled"); }); primary.disabled = true; primary.classList.add("disabled"); hyperlinkModal.setBody(body); hyperlinkModal.modal(() => { const clicked = hyperlinkModal.getClickedAsText(); if (clicked === "Insert") { const id = body.querySelector("input[type='radio']:checked")! .nextElementSibling!.getAttribute("data-wed-id")!; mode.insertPtrTr.execute({ ...data, target: id }); } }); }
classFromOriginalName(name: string): string { return util.classFromOriginalName(name, this.mapping); }
protected dispatch(root: Element, el: Element): void { const klass = this.getAdditionalClasses(el); if (klass.length !== 0) { el.className += ` ${klass}`; } const name = util.getOriginalName(el); let skipDefault = false; switch (name) { case "btw:overview": case "btw:sense-discrimination": case "btw:historico-semantical-data": case "btw:credits": this.headingDecorator.unitHeadingDecorator(el); break; case "btw:definition": case "btw:english-renditions": case "btw:english-rendition": case "btw:etymology": case "btw:contrastive-section": case "btw:antonyms": case "btw:cognates": case "btw:conceptual-proximates": case "btw:other-citations": case "btw:citations": this.headingDecorator.sectionHeadingDecorator(el); break; case "btw:semantic-fields": this.headingDecorator.sectionHeadingDecorator(el); break; case "btw:sf": this.sfDecorator(root, el); skipDefault = true; break; case "ptr": this.ptrDecorator(root, el); break; case "foreign": this.languageDecorator(el); break; case "ref": this.refDecorator(root, el); break; case "btw:example": this.idDecorator(root, el); break; case "btw:cit": this.citDecorator(root, el); skipDefault = true; // citDecorator calls elementDecorator break; case "btw:explanation": this.explanationDecorator(root, el); skipDefault = true; // explanationDecorator calls elementDecorator break; case "btw:none": this.noneDecorator(el); // THIS ELEMENT DOES NOT GET THE REGULAR DECORATION. skipDefault = true; break; default: break; } if (!skipDefault) { this.elementDecorator(root, el); } }
// tslint:disable-next-line:cyclomatic-complexity max-func-body-length linkingDecorator(root: Element, el: Element, isPtr: boolean): void { let origTarget = el.getAttribute(util.encodeAttrName("target")); // XXX This should become an error one day. The only reason we need this now // is that some of the early test files had <ref> elements without targets. if (origTarget === null) { origTarget = ""; } origTarget = origTarget.trim(); const doc = root.ownerDocument; if (origTarget.lastIndexOf("#", 0) === 0) { // Internal target // Add BTW in front because we want the target used by wed. const targetId = origTarget.replace(/#(.*)$/, "#BTW-$1"); const text = doc.createElement("div"); text.className = "_text _phantom _linking_deco"; const a = doc.createElement("a"); a.className = "_phantom"; a.setAttribute("href", targetId); text.appendChild(a); if (isPtr) { // _linking_deco is used locally to make this function idempotent { let child = el.firstElementChild; while (child !== null) { const next = child.nextElementSibling; if (child.classList.contains("_linking_deco")) { this.guiUpdater.removeNode(child); break; // There is only one. } child = next; } } const refman = this.refmans.getRefmanForElement(el); // Find the referred element. Slice to drop the #. let target = doc.getElementById(targetId.slice(1)); // An undefined or null refman can happen when first decorating the // document. let label; if (refman !== null) { if (refman instanceof LabelManager) { if (refman.name === "sense" || refman.name === "subsense") { label = refman.idToLabel(targetId.slice(1)); label = label !== undefined ? `[${label}]` : undefined; } } else { // An empty target can happen when first decorating the document. if (target !== null) { label = refman.getPositionalLabel( this.editor.toDataNode(el) as Element, this.editor.toDataNode(target) as Element); } } } if (label === undefined) { label = targetId; } a.textContent = label; // A ptr contains only attributes, no text, so we can just append. const pair = this.mode.nodesAroundEditableContents(el); this.guiUpdater.insertBefore(el, text, pair[1]); if (target !== null) { const targetName = util.getOriginalName(target); // Reduce the target to something sensible for tooltip text. if (targetName === "btw:sense") { const terms = target.querySelectorAll(this.mapped.toGUISelector( this.senseTooltipSelector)); let html = ""; for (let i = 0; i < terms.length; ++i) { const term = terms[i]; html += term.innerHTML; if (i < terms.length - 1) { html += ", "; } } target = target.ownerDocument.createElement("div"); // tslint:disable-next-line:no-inner-html target.innerHTML = html; } else if (targetName === "btw:subsense") { let child = target.firstElementChild; while (child !== null) { if (child.classList.contains("btw:explanation")) { target = child.cloneNode(true) as HTMLElement; break; } child = child.nextElementSibling; } } else if (targetName === "btw:example") { target = null; } if (target !== null) { const nodes = target.querySelectorAll(".head, ._gui, ._explanation_number"); // tslint:disable-next-line:prefer-for-of for (let nodeIx = 0; nodeIx < nodes.length; ++nodeIx) { const node = nodes[nodeIx]; node.parentNode!.removeChild(node); } tooltip($(text), { title: `<div>${target.innerHTML}</div>`, html: true, container: "body", trigger: "hover" }); } } } else { throw new Error("internal error: ref with unexpected target"); } } else { // External target const biblPrefix = "/bibliography/"; if (origTarget.lastIndexOf(biblPrefix, 0) === 0) { // Bibliographical reference... if (isPtr) { throw new Error("internal error: bibliographic " + "reference recorded as ptr"); } const targetId = origTarget; // It is okay to skip the tree updater for these operations. let child = el.firstElementChild; while (child !== null) { const next = child.nextElementSibling; if (child.classList.contains("_ref_abbr") || child.classList.contains("_ref_paren")) { this.guiUpdater.removeNode(child); } child = next; } const abbr = doc.createElement("div"); abbr.className = "_text _phantom _ref_abbr"; this.guiUpdater.insertBefore(el, abbr, el.firstChild); const open = doc.createElement("div"); open.className = "_phantom _decoration_text _ref_paren " + "_open_ref_paren _start_wrapper"; open.textContent = "("; this.guiUpdater.insertBefore(el, open, abbr); const close = doc.createElement("div"); close.className = "_phantom _decoration_text " + "_ref_paren _close_ref_paren _end_wrapper"; close.textContent = ")"; this.guiUpdater.insertBefore(el, close, null); // tslint:disable-next-line:no-floating-promises this.fetchAndFillBiblData(targetId, el, abbr); } } }