blob: e06de1e6ce6f8729d35b04810a785ec8be1cdd6e [file] [log] [blame]
Nils Diewald0e6992a2015-04-14 20:13:52 +00001/*
2 * MenuItems may define:
3 *
4 * onclick: action happen on click and enter.
5 * further: action happen on right arrow
6 */
7
Akronacffc652017-12-18 21:21:25 +01008"use strict";
9
Nils Diewald0e6992a2015-04-14 20:13:52 +000010/**
11 * Item in the Dropdown menu
12 */
13define({
14 /**
15 * Create a new MenuItem object.
16 *
17 * @constructor
18 * @this {MenuItem}
19 * @param {Array.<string>} An array object of name, action and
20 * optionally a description
21 */
22 create : function (params) {
23 return Object.create(this)._init(params);
24 },
25
26 /**
27 * Upgrade this object to another object,
28 * while private data stays intact.
29 *
30 * @param {Object] An object with properties.
31 */
32 upgradeTo : function (props) {
33 for (var prop in props) {
34 this[prop] = props[prop];
35 };
36 return this;
37 },
38
Nils Diewald7148c6f2015-05-04 15:07:53 +000039
40 /**
41 * Get or set the content of the meun item.
42 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000043 content : function (content) {
44 if (arguments.length === 1)
45 this._content = document.createTextNode(content);
46 return this._content;
47 },
48
Nils Diewald7148c6f2015-05-04 15:07:53 +000049 /**
Nils Diewald7148c6f2015-05-04 15:07:53 +000050 * Get or set the information for action of this item.
51 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000052 action : function (action) {
53 if (arguments.length === 1)
54 this._action = action;
55 return this._action;
56 },
Akron52ed22d2018-07-11 17:05:19 +020057
58
59 /**
60 * Get the lower cased field of the item
61 * (used for analyses).
62 */
63 lcField : function () {
64 return this._lcField;
65 },
66
Nils Diewald0e6992a2015-04-14 20:13:52 +000067
68 /**
69 * Check or set if the item is active
70 *
71 * @param {boolean|null} State of activity
72 */
73 active : function (bool) {
74 var cl = this.element().classList;
75 if (bool === undefined)
76 return cl.contains("active");
77 else if (bool)
78 cl.add("active");
79 else
80 cl.remove("active");
81 },
82
83 /**
84 * Check or set if the item is
85 * at the boundary of the menu
86 * list
87 *
88 * @param {boolean|null} State of activity
89 */
90 noMore : function (bool) {
91 var cl = this.element().classList;
92 if (bool === undefined)
93 return cl.contains("no-more");
94 else if (bool)
95 cl.add("no-more");
96 else
97 cl.remove("no-more");
98 },
99
100 /**
101 * Get the document element of the menu item
102 */
103 element : function () {
104 // already defined
105 if (this._element !== undefined)
106 return this._element;
107
108 // Create list item
109 var li = document.createElement("li");
110
111 // Connect action
112 if (this["onclick"] !== undefined) {
113 li["onclick"] = this.onclick.bind(this);
114 };
115
116 // Append template
117 li.appendChild(this.content());
118
119 return this._element = li;
120 },
121
122 /**
123 * Highlight parts of the item
124 *
125 * @param {string} Prefix string for highlights
126 */
127 highlight : function (prefix) {
Akron6ed13992016-05-23 18:06:05 +0200128
129 // The prefix already matches
130 if (this._prefix === prefix)
131 return;
132
133 // There is a prefix but it doesn't match
134 if (this._prefix !== null) {
135 this.lowlight();
136 }
137
Nils Diewald0e6992a2015-04-14 20:13:52 +0000138 var children = this.element().childNodes;
139 for (var i = children.length -1; i >= 0; i--) {
140 this._highlight(children[i], prefix);
141 };
Akron6ed13992016-05-23 18:06:05 +0200142
143 this._prefix = prefix;
144 },
145
146 /**
147 * Remove highlight of the menu item
148 */
149 lowlight : function () {
150 if (this._prefix === null)
151 return;
152
153 var e = this.element();
154
155 var marks = e.getElementsByTagName("mark");
156 for (var i = marks.length - 1; i >= 0; i--) {
157 // Create text node clone
158 var x = document.createTextNode(
Akronacffc652017-12-18 21:21:25 +0100159 marks[i].firstChild.nodeValue
Akron6ed13992016-05-23 18:06:05 +0200160 );
161
162 // Replace with content
163 marks[i].parentNode.replaceChild(
Akronacffc652017-12-18 21:21:25 +0100164 x,
165 marks[i]
Akron6ed13992016-05-23 18:06:05 +0200166 );
167 };
168
169 // Remove consecutive textnodes
170 e.normalize();
171 this._prefix = null;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000172 },
173
Akron52ed22d2018-07-11 17:05:19 +0200174
Nils Diewald0e6992a2015-04-14 20:13:52 +0000175 // Highlight a certain substring of the menu item
Akronacffc652017-12-18 21:21:25 +0100176 _highlight : function (elem, prefixString) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000177 if (elem.nodeType === 3) {
178
179 var text = elem.nodeValue;
180 var textlc = text.toLowerCase();
Akronacffc652017-12-18 21:21:25 +0100181
182 // Split prefixes
183 if (prefixString) {
Akron359c7e12017-12-19 12:06:55 +0100184
185 // ND:
186 // Doing this in a single line can trigger
187 // a deep-recursion in Firefox 57.01, though I don't know why.
188 prefixString = prefixString.trim();
189 var prefixes = prefixString.split(" ");
Akronacffc652017-12-18 21:21:25 +0100190
191 var prefix;
192 var testPos;
193 var pos = -1;
194 var len = 0;
195
196 // Iterate over all prefixes and get the best one
197 for (var i = 0; i < prefixes.length; i++) {
198
199 // Get first pos of a matching prefix
200 testPos = textlc.indexOf(prefixes[i]);
201 if (testPos < 0)
202 continue;
203
204 if (pos === -1 || testPos < pos) {
205 pos = testPos;
206 len = prefixes[i].length;
207 }
208 else if (testPos === pos && prefixes[i].length > len) {
209 len = prefixes[i].length;
210 };
211 };
212
213 // Matches!
214 if (pos >= 0) {
215
216 // First element
217 if (pos > 0) {
218 elem.parentNode.insertBefore(
219 document.createTextNode(text.substr(0, pos)),
220 elem
221 );
222 };
223
224 // Second element
225 var hl = document.createElement("mark");
226 hl.appendChild(
227 document.createTextNode(text.substr(pos, len))
228 );
229 elem.parentNode.insertBefore(hl, elem);
230
231 // Third element
232 var third = text.substr(pos + len);
233
234 if (third.length > 0) {
235 var thirdE = document.createTextNode(third);
236 elem.parentNode.insertBefore(
237 thirdE,
238 elem
239 );
240 this._highlight(thirdE, prefixString);
241 };
242
243 var p = elem.parentNode;
244 p.removeChild(elem);
245 };
246 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000247 }
248 else {
249 var children = elem.childNodes;
250 for (var i = children.length -1; i >= 0; i--) {
Akronacffc652017-12-18 21:21:25 +0100251 this._highlight(children[i], prefixString);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000252 };
253 };
254 },
255
Nils Diewald0e6992a2015-04-14 20:13:52 +0000256 // Initialize menu item
257 _init : function (params) {
258
Akronc82513b2016-06-11 11:22:36 +0200259 if (params[0] === undefined) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000260 throw new Error("Missing parameters");
Akronc82513b2016-06-11 11:22:36 +0200261 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000262
263 this.content(params[0]);
264
Akron52ed22d2018-07-11 17:05:19 +0200265 if (params.length > 1) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000266 this._action = params[1];
267
Akron52ed22d2018-07-11 17:05:19 +0200268 if (params.length > 2)
269 this._onclick = params[2];
270 };
271
Nils Diewald0e6992a2015-04-14 20:13:52 +0000272 this._lcField = ' ' + this.content().textContent.toLowerCase();
Akron7524be12016-06-01 17:31:33 +0200273 this._prefix = null;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000274
275 return this;
276 },
277
Akron52ed22d2018-07-11 17:05:19 +0200278
279 /**
280 * The click action of the menu item.
281 */
282 onclick : function (e) {
283 var m = this.menu();
284
285 // Reset prefix
286 m.prefix("");
287
288 if (this._onclick)
289 this._onclick.apply(this, e);
290
291 m.hide();
292 },
293
294
Nils Diewald0e6992a2015-04-14 20:13:52 +0000295 /**
296 * Return menu list.
297 */
298 menu : function () {
299 return this._menu;
300 }
301});