/**
* Tags for single entities/tokens.
* Essentially a helper class for Words; should not be directly instantiated
* by Parsers.
*
* [WordTag] -> Word -> Row
* WordCluster
* Link
*/
class WordTag {
/**
* Creates a new WordTag instance
* @param {String} val - The raw text for this WordTag
* @param {Word} word - The parent Word for this WordTag
* @param {Config~Config} config - The Config object for the parent TAG
* instance
* @param {Boolean} top - True if this WordTag should be drawn above the
* parent Word, false if it should be drawn below
*/
constructor(val, word, config, top = true) {
this.val = val;
this.word = word;
this.config = config;
this.top = top;
if (!word.svg) {
throw "Error: Trying to initialise WordTag on Word without SVG" +
" element";
}
this.draw();
}
/**
* (Re-)draws this WordTag's SVG elements onto the visualisation
*/
draw() {
if (this.svg) {
// Delete remnants of any previous draw
this.remove();
}
// Prepare our SVG elements as a group within the Word's SVG element
this.svg = this.word.svg.group();
// Draw in the SVG text element.
// Note that applying classes to the text element may change its font
// size, and if its font size changes, the anchor point for the resizing
// is the text's baseline (not any of the bounding box sides).
// N.B.: Typographical baselines ignore descenders
this.svgText = this.svg
.text(this.val)
.addClass("tag-element")
.addClass(this.top ? "word-tag" : "word-tag syntax-tag")
.leading(1);
// add click and right-click listeners
let mainSvg = this.word.main.svg;
this.svgText.node.oncontextmenu = (e) => {
e.preventDefault();
mainSvg.fire("tag-right-click", { object: this, event: e });
};
this.svgText.click(() => mainSvg.fire("tag-edit", { object: this }));
// Draws a line / curly bracket between the Word and this WordTag, if
// it's a top tag
this.line = this.svg.path().addClass("tag-element");
this.drawTagLine();
// Centre the WordTag and its line horizontally
// (SVG text elements are positioned on the x-axis by their centres)
this.centre();
// Position the WordTag above/below the main Word
// (It starts with its upper-left corner on the Row's baseline)
let newY;
if (this.top) {
newY =
-this.word.textHeight -
this.svgText.bbox().height -
this.config.wordTopTagPadding;
} else {
newY = this.config.wordBottomTagPadding;
}
this.svgText.y(newY);
this.line.cy((this.svgText.bbox().y2 + this.word.svgText.bbox().y) / 2);
}
/**
* Centres this WordTag and its line horizontally against the base Word's
* current position
* (N.B.: SVG Text elements are positioned on the x-axis by their centres)
*/
centre() {
// Centre the Text element
this.svgText.x(this.word.textRcx);
// Centre the line between the Word and WordTag
this.line.cx(this.svgText.cx());
}
/**
* Removes this WordTag's SVG elements from the visualisation
* If this instance is not deleted, it can be redrawn with the `.draw()`
* method
* @return {*}
*/
remove() {
this.svg.remove();
this.svg = null;
}
/**
* Draws a connecting line between this WordTag and its parent Word, if
* this is a top WordTag.
*/
drawTagLine() {
if (!this.top) {
return;
}
const wordWidth = this.word.textWidth;
if (wordWidth < this.config.wordBraceThreshold) {
// Draw a single vertical line
this.line.plot("M 0,0, 0," + this.config.wordTagLineLength);
} else {
// Draw a curly brace
const height = this.config.wordTagLineLength;
const arm = wordWidth / 2;
this.line.plot(
"M0,0" +
"c" +
[0, height, arm, 0, arm, height] +
"M0,0" +
"c" +
[0, height, -arm, 0, -arm, height]
);
}
}
/**
* Sets the text of this WordTag, or returns this WordTag's SVG text element
* @param val
* @return {*}
*/
text(val) {
if (val === undefined) {
return this.svgText;
}
this.val = val;
this.svgText.text(this.val);
if (this.editingRect) {
let bbox = this.svgText.bbox();
if (bbox.width > 0) {
this.editingRect
.width(bbox.width + 8)
.height(bbox.height + 4)
.x(bbox.x - 4)
.y(bbox.y - 2);
} else {
this.editingRect.width(10).x(-5);
}
}
}
/**
* Returns the width of the bounding box for this WordTag
*/
boxWidth() {
return this.svg.bbox().width;
}
/**
* Returns the width of the bounding box of the WordTag's SVG text element
* @return {Number}
*/
get textWidth() {
return this.svgText.bbox().width;
}
changeEntity(word) {
if (this.word) {
this.word.tag = null;
}
this.word = word;
this.word.tag = this;
this.word.svg.add(this.svg);
}
listenForEdit() {
this.isEditing = true;
let bbox = this.svgText.bbox();
this.svg.addClass("tag-element").addClass("editing");
this.editingRect = this.svg
.rect(bbox.width + 8, bbox.height + 4)
.x(bbox.x - 4)
.y(bbox.y - 2)
.rx(2)
.ry(2)
.back();
}
stopEditing() {
this.isEditing = false;
this.svg.removeClass("editing");
this.editingRect.remove();
this.editingRect = null;
this.val = this.val.trim();
if (!this.val) {
this.remove();
} else {
this.word.alignBox();
}
}
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Debug functions
/**
* Draws the outline of this component's bounding box
*/
drawBbox() {
const bbox = this.svg.bbox();
this.svg
.polyline([
[bbox.x, bbox.y],
[bbox.x2, bbox.y],
[bbox.x2, bbox.y2],
[bbox.x, bbox.y2],
[bbox.x, bbox.y]
])
.fill("none")
.stroke({ width: 1 });
}
/**
* Draws the outline of the text element's bounding box
*/
drawTextBbox() {
const bbox = this.svgText.bbox();
this.svg
.polyline([
[bbox.x, bbox.y],
[bbox.x2, bbox.y],
[bbox.x2, bbox.y2],
[bbox.x, bbox.y2],
[bbox.x, bbox.y]
])
.fill("none")
.stroke({ width: 1 });
}
}
export default WordTag;