blob: 812a398342a2db2718828e923d8ee3ac1eb9a395 [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 /**
50 * Get the lower cased field of the item
51 * (used for analyses).
52 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000053 lcField : function () {
54 return this._lcField;
55 },
56
Nils Diewald7148c6f2015-05-04 15:07:53 +000057
58 /**
59 * Get or set the information for action of this item.
60 */
Nils Diewald0e6992a2015-04-14 20:13:52 +000061 action : function (action) {
62 if (arguments.length === 1)
63 this._action = action;
64 return this._action;
65 },
66
67 /**
68 * Check or set if the item is active
69 *
70 * @param {boolean|null} State of activity
71 */
72 active : function (bool) {
73 var cl = this.element().classList;
74 if (bool === undefined)
75 return cl.contains("active");
76 else if (bool)
77 cl.add("active");
78 else
79 cl.remove("active");
80 },
81
82 /**
83 * Check or set if the item is
84 * at the boundary of the menu
85 * list
86 *
87 * @param {boolean|null} State of activity
88 */
89 noMore : function (bool) {
90 var cl = this.element().classList;
91 if (bool === undefined)
92 return cl.contains("no-more");
93 else if (bool)
94 cl.add("no-more");
95 else
96 cl.remove("no-more");
97 },
98
99 /**
100 * Get the document element of the menu item
101 */
102 element : function () {
103 // already defined
104 if (this._element !== undefined)
105 return this._element;
106
107 // Create list item
108 var li = document.createElement("li");
109
110 // Connect action
111 if (this["onclick"] !== undefined) {
112 li["onclick"] = this.onclick.bind(this);
113 };
114
115 // Append template
116 li.appendChild(this.content());
117
118 return this._element = li;
119 },
120
121 /**
122 * Highlight parts of the item
123 *
124 * @param {string} Prefix string for highlights
125 */
126 highlight : function (prefix) {
Akron6ed13992016-05-23 18:06:05 +0200127
128 // The prefix already matches
129 if (this._prefix === prefix)
130 return;
131
132 // There is a prefix but it doesn't match
133 if (this._prefix !== null) {
134 this.lowlight();
135 }
136
Nils Diewald0e6992a2015-04-14 20:13:52 +0000137 var children = this.element().childNodes;
138 for (var i = children.length -1; i >= 0; i--) {
139 this._highlight(children[i], prefix);
140 };
Akron6ed13992016-05-23 18:06:05 +0200141
142 this._prefix = prefix;
143 },
144
145 /**
146 * Remove highlight of the menu item
147 */
148 lowlight : function () {
149 if (this._prefix === null)
150 return;
151
152 var e = this.element();
153
154 var marks = e.getElementsByTagName("mark");
155 for (var i = marks.length - 1; i >= 0; i--) {
156 // Create text node clone
157 var x = document.createTextNode(
Akronacffc652017-12-18 21:21:25 +0100158 marks[i].firstChild.nodeValue
Akron6ed13992016-05-23 18:06:05 +0200159 );
160
161 // Replace with content
162 marks[i].parentNode.replaceChild(
Akronacffc652017-12-18 21:21:25 +0100163 x,
164 marks[i]
Akron6ed13992016-05-23 18:06:05 +0200165 );
166 };
167
168 // Remove consecutive textnodes
169 e.normalize();
170 this._prefix = null;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000171 },
172
173 // Highlight a certain substring of the menu item
Akronacffc652017-12-18 21:21:25 +0100174 _highlight : function (elem, prefixString) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000175 if (elem.nodeType === 3) {
176
177 var text = elem.nodeValue;
178 var textlc = text.toLowerCase();
Akronacffc652017-12-18 21:21:25 +0100179
180 // Split prefixes
181 if (prefixString) {
Akron359c7e12017-12-19 12:06:55 +0100182
183 // ND:
184 // Doing this in a single line can trigger
185 // a deep-recursion in Firefox 57.01, though I don't know why.
186 prefixString = prefixString.trim();
187 var prefixes = prefixString.split(" ");
Akronacffc652017-12-18 21:21:25 +0100188
189 var prefix;
190 var testPos;
191 var pos = -1;
192 var len = 0;
193
194 // Iterate over all prefixes and get the best one
195 for (var i = 0; i < prefixes.length; i++) {
196
197 // Get first pos of a matching prefix
198 testPos = textlc.indexOf(prefixes[i]);
199 if (testPos < 0)
200 continue;
201
202 if (pos === -1 || testPos < pos) {
203 pos = testPos;
204 len = prefixes[i].length;
205 }
206 else if (testPos === pos && prefixes[i].length > len) {
207 len = prefixes[i].length;
208 };
209 };
210
211 // Matches!
212 if (pos >= 0) {
213
214 // First element
215 if (pos > 0) {
216 elem.parentNode.insertBefore(
217 document.createTextNode(text.substr(0, pos)),
218 elem
219 );
220 };
221
222 // Second element
223 var hl = document.createElement("mark");
224 hl.appendChild(
225 document.createTextNode(text.substr(pos, len))
226 );
227 elem.parentNode.insertBefore(hl, elem);
228
229 // Third element
230 var third = text.substr(pos + len);
231
232 if (third.length > 0) {
233 var thirdE = document.createTextNode(third);
234 elem.parentNode.insertBefore(
235 thirdE,
236 elem
237 );
238 this._highlight(thirdE, prefixString);
239 };
240
241 var p = elem.parentNode;
242 p.removeChild(elem);
243 };
244 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000245 }
246 else {
247 var children = elem.childNodes;
248 for (var i = children.length -1; i >= 0; i--) {
Akronacffc652017-12-18 21:21:25 +0100249 this._highlight(children[i], prefixString);
Nils Diewald0e6992a2015-04-14 20:13:52 +0000250 };
251 };
252 },
253
Nils Diewald0e6992a2015-04-14 20:13:52 +0000254 // Initialize menu item
255 _init : function (params) {
256
Akronc82513b2016-06-11 11:22:36 +0200257 if (params[0] === undefined) {
Nils Diewald0e6992a2015-04-14 20:13:52 +0000258 throw new Error("Missing parameters");
Akronc82513b2016-06-11 11:22:36 +0200259 };
Nils Diewald0e6992a2015-04-14 20:13:52 +0000260
261 this.content(params[0]);
262
263 if (params.length === 2)
264 this._action = params[1];
265
266 this._lcField = ' ' + this.content().textContent.toLowerCase();
Akron7524be12016-06-01 17:31:33 +0200267 this._prefix = null;
Nils Diewald0e6992a2015-04-14 20:13:52 +0000268
269 return this;
270 },
271
272 /**
273 * Return menu list.
274 */
275 menu : function () {
276 return this._menu;
277 }
278});