blob: 7ce74556029a62af787ab83b18fbd5bddb30fd45 [file] [log] [blame]
/*
* MenuItems may define:
*
* onclick: action happen on click and enter.
* further: action happen on right arrow
*/
"use strict";
/**
* Item in the Dropdown menu
*/
define({
/**
* Create a new MenuItem object.
*
* @constructor
* @this {MenuItem}
* @param {Array.<string>} An array object of name, action and
* optionally a description
*/
create : function (params) {
return Object.create(this)._init(params);
},
/**
* Upgrade this object to another object,
* while private data stays intact.
*
* @param {Object] An object with properties.
*/
upgradeTo : function (props) {
for (let prop in props) {
this[prop] = props[prop];
};
return this;
},
/**
* Get or set the content of the meun item.
*/
content : function (content) {
if (arguments.length === 1)
this._content = document.createTextNode(content);
return this._content;
},
/**
* Get or set the information for action of this item.
*/
action : function (action) {
if (arguments.length === 1)
this._action = action;
return this._action;
},
/**
* Get the lower cased field of the item
* (used for analyses).
*/
lcField : function () {
return this._lcField;
},
/**
* Check or set if the item is active
*
* @param {boolean|null} State of activity
*/
active : function (bool) {
const cl = this.element().classList;
if (bool === undefined)
return cl.contains("active");
else if (bool)
cl.add("active");
else
cl.remove("active");
},
/**
* Check or set if the item is
* at the boundary of the menu
* list
*
* @param {boolean|null} State of activity
*/
noMore : function (bool) {
const cl = this.element().classList;
if (bool === undefined)
return cl.contains("no-more");
else if (bool)
cl.add("no-more");
else
cl.remove("no-more");
},
/**
* Get the document element of the menu item
*/
element : function () {
// already defined
if (this._element !== undefined)
return this._element;
// Create list item
const li = document.createElement("li");
// Connect action
if (this["onclick"] !== undefined) {
li["onclick"] = this.onclick.bind(this);
};
// Append template
li.appendChild(this.content());
return this._element = li;
},
/**
* Highlight parts of the item
*
* @param {string} Prefix string for highlights
*/
highlight : function (prefix) {
// The prefix already matches
if (this._prefix === prefix)
return;
// There is a prefix but it doesn't match
if (this._prefix !== null) {
this.lowlight();
}
const children = this.element().childNodes;
for (let i = children.length -1; i >= 0; i--) {
this._highlight(children[i], prefix);
};
this._prefix = prefix;
},
/**
* Remove highlight of the menu item
*/
lowlight : function () {
if (this._prefix === null)
return;
const e = this.element();
const marks = e.getElementsByTagName("mark");
for (let i = marks.length - 1; i >= 0; i--) {
// Replace with content
marks[i].parentNode.replaceChild(
// Create text node clone
document.createTextNode(
marks[i].firstChild.nodeValue
),
marks[i]
);
};
// Remove consecutive textnodes
e.normalize();
this._prefix = null;
},
// Highlight a certain substring of the menu item
_highlight : function (elem, prefixString) {
if (elem.nodeType === 3) {
const text = elem.nodeValue;
const textlc = text.toLowerCase();
// Split prefixes
if (prefixString) {
// ND:
// Doing this in a single line can trigger
// a deep-recursion in Firefox 57.01, though I don't know why.
prefixString = prefixString.trim();
const prefixes = prefixString.split(" ");
let testPos,
pos = -1,
len = 0;
// Iterate over all prefixes and get the best one
// for (var i = 0; i < prefixes.length; i++) {
prefixes.forEach(function(i) {
// Get first pos of a matching prefix
testPos = textlc.indexOf(i);
if (testPos < 0)
return;
if (pos === -1 || testPos < pos) {
pos = testPos;
len = i.length;
}
else if (testPos === pos && i.length > len) {
len = i.length;
};
});
// Matches!
if (pos >= 0) {
// First element
if (pos > 0) {
elem.parentNode.insertBefore(
document.createTextNode(text.substr(0, pos)),
elem
);
};
// Second element
const hl = document.createElement("mark");
hl.appendChild(
document.createTextNode(text.substr(pos, len))
);
elem.parentNode.insertBefore(hl, elem);
// Third element
const third = text.substr(pos + len);
if (third.length > 0) {
const thirdE = document.createTextNode(third);
elem.parentNode.insertBefore(
thirdE,
elem
);
this._highlight(thirdE, prefixString);
};
elem.parentNode.removeChild(elem);
};
};
}
else {
const children = elem.childNodes;
for (let i = children.length -1; i >= 0; i--) {
this._highlight(children[i], prefixString);
};
};
},
// Initialize menu item
_init : function (params) {
if (params[0] === undefined) {
throw new Error("Missing parameters");
};
const t = this;
t.content(params[0]);
if (params.length > 1) {
t._action = params[1];
if (params.length > 2)
t._onclick = params[2];
};
t._lcField = ' ' + t.content().textContent.toLowerCase();
t._prefix = null;
return this;
},
/**
* The click action of the menu item.
*/
onclick : function (e) {
const m = this.menu();
// Reset prefix
m.prefix("");
if (this._onclick)
this._onclick.apply(this, e);
m.hide();
},
/**
* Return menu list.
*/
menu : function () {
return this._menu;
}
});