Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(130)

Side by Side Diff: appengine/monorail/static/js/framework/framework-menu.js

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 /* Copyright 2016 The Chromium Authors. All Rights Reserved.
2 *
3 * Use of this source code is governed by a BSD-style
4 * license that can be found in the LICENSE file or at
5 * https://developers.google.com/open-source/licenses/bsd
6 */
7
8 /**
9 * @fileoverview This file represents a standalone, reusable drop down menu
10 * widget that can be attached to any element on a given page. It supports
11 * multiple instances of the widget on a page. It has no dependencies. Usage
12 * is as simple as creating a new Menu object and supplying it with a target
13 * element.
14 */
15
16 /**
17 * The entry point and constructor for the Menu object. Creating
18 * a valid instance of this object will insert a drop down menu
19 * near the element supplied as the target, attach all the necessary
20 * events and insert the necessary elements on the page.
21 *
22 * @param {Element} target the target element on the page to which
23 * the drop down menu will be placed near.
24 * @param {Function=} opt_onShow function to execute every time the
25 * menu is made visible, most likely through a click on the target.
26 * @constructor
27 */
28 var Menu = function(target, opt_onShow) {
29 this.iid = Menu.instance.length;
30 Menu.instance[this.iid] = this;
31 this.target = target;
32 this.onShow = opt_onShow || null;
33
34 // An optional trigger element on the page that can be used to trigger
35 // the drop-down. Currently hard-coded to be the same as the target element.
36 this.trigger = target;
37 this.items = [];
38 this.onOpenEvents = [];
39 this.menu = this.createElement('div', 'menuDiv instance' + this.iid);
40 this.targetId = this.target.getAttribute('id');
41 var menuId = (this.targetId != null) ?
42 'menuDiv-' + this.targetId : 'menuDiv-instance' + this.iid;
43 this.menu.setAttribute('id', menuId);
44 this.menu.role = 'listbox';
45 this.hide();
46 this.addCategory('default');
47 this.addEvent(this.trigger, 'click', this.wrap(this.toggle));
48 this.addEvent(window, 'resize', this.wrap(this.adjustSizeAndLocation));
49
50 // Hide the menu if a user clicks outside the menu widget
51 this.addEvent(document, 'click', this.wrap(this.hide));
52 this.addEvent(this.menu, 'click', this.stopPropagation());
53 this.addEvent(this.trigger, 'click', this.stopPropagation());
54 };
55
56 // A reference to the element or node that the drop down
57 // will appear next to
58 Menu.prototype.target = null;
59
60 // Element ID of the target. ID will be assigned to the newly created
61 // menu div based on the target ID. A default ID will be
62 // assigned If there is no ID on the target.
63 Menu.prototype.targetId = null;
64
65 /**
66 * A reference to the element or node that will trigger
67 * the drop down to appear. If not specified, this value
68 * will be the same as <Menu Instance>.target
69 * @type {Element}
70 */
71 Menu.prototype.trigger = null;
72
73 // A reference to the event type that will "open" the
74 // menu div. By default this is the (on)click method.
75 Menu.prototype.triggerType = null;
76
77 // A reference to the element that will appear when the
78 // trigger is clicked.
79 Menu.prototype.menu = null;
80
81 /**
82 * Function to execute every time the menu is made shown.
83 * @type {Function}
84 */
85 Menu.prototype.onShow = null;
86
87 // A list of category divs. By default these categories
88 // are set to display none until at least one element
89 // is placed within them.
90 Menu.prototype.categories = null;
91
92 // An id used to track timed intervals
93 Menu.prototype.thread = -1;
94
95 // The static instance id (iid) denoting which menu in the
96 // list of Menu.instance items is this instantiated object.
97 Menu.prototype.iid = -1;
98
99 // A counter to indicate the number of items added with
100 // addItem(). After 5 items, a height is set on the menu
101 // and a scroll bar will appear.
102 Menu.prototype.items = null;
103
104 // A flag to detect whether or not a scroll bar has been added
105 Menu.prototype.scrolls = false;
106
107 // onOpen event handlers; each function in this list will
108 // be executed and passed the executing instance as a
109 // parameter before the menu is to be displayed.
110 Menu.prototype.onOpenEvents = null;
111
112 /**
113 * An extended short-cut for document.createElement(); this
114 * method allows the creation of an element, the assignment
115 * of one or more class names and the ability to set the
116 * content of the created element all with one function call.
117 * @param {string} element name of the element to create. Examples would
118 * be 'div' or 'a'.
119 * @param {string} opt_className an optional string to assign to the
120 * newly created element's className property.
121 * @param {string|Element} opt_content either a snippet of HTML or a HTML
122 * element that is to be appended to the newly created element.
123 * @return {Element} a reference to the newly created element.
124 */
125 Menu.prototype.createElement = function(element, opt_className, opt_content) {
126 var div = document.createElement(element);
127 div.className = opt_className;
128 if (opt_content) {
129 this.append(opt_content, div);
130 }
131 return div;
132 };
133
134 /**
135 * Uses a fairly browser agnostic approach to applying a callback to
136 * an element on the page.
137 *
138 * @param {Element|EventTarget} element a reference to an element on the page to
139 * which to attach and event.
140 * @param {string} eventType a browser compatible event type as a string
141 * without the sometimes assumed on- prefix. Examples: 'click',
142 * 'mousedown', 'mouseover', etc...
143 * @param {Function} callback a function reference to invoke when the
144 * the event occurs.
145 */
146 Menu.prototype.addEvent = function(element, eventType, callback) {
147 if (element.addEventListener) {
148 element.addEventListener(eventType, callback, false);
149 } else {
150 try {
151 element.attachEvent('on' + eventType, callback);
152 } catch (e) {
153 element['on' + eventType] = callback;
154 }
155 }
156 };
157
158 /**
159 * Similar to addEvent, this provides a specialied handler for onOpen
160 * events that apply to this instance of the Menu class. The supplied
161 * callbacks are appended to an internal array and called in order
162 * every time the menu is opened. The array can be accessed via
163 * menuInstance.onOpenEvents.
164 */
165 Menu.prototype.addOnOpen = function(eventCallback) {
166 var eventIndex = this.onOpenEvents.length;
167 this.onOpenEvents.push(eventCallback);
168 return eventIndex;
169 };
170
171 /**
172 * Used throughout the code, this method wraps any supplied function
173 * in a closure that calls the supplied function in the context of either
174 * the optional thisObj parameter or instance of the menu this function
175 * is called from.
176 * @param {Function} callback the function to wrap and embed context
177 * within.
178 * @param {Object} opt_thisObj an alternate 'this' object to use instead
179 * of this instance of Menu.
180 */
181 Menu.prototype.wrap = function(callback, opt_thisObj) {
182 var closured_callback = callback;
183 var this_object = opt_thisObj || this;
184 return (function() {
185 closured_callback.apply(this_object);
186 });
187 };
188
189 /**
190 * This method will create a div with the classes .menuCategory and the
191 * name of the category as supplied in the first parameter. It then, if
192 * a title is supplied, creates a title div and appends it as well. The
193 * optional title is styled with the .categoryTitle and category name
194 * class.
195 *
196 * Categories are stored within the menu object instance for programmatic
197 * manipulation in the array, menuInstance.categories. Note also that this
198 * array is doubly linked insofar as that the category div can be accessed
199 * via it's index in the array as well as by instance.categories[category]
200 * where category is the string name supplied when creating the category.
201 *
202 * @param {string} category the string name used to create the category;
203 * used as both a class name and a key into the internal array. It
204 * must be a valid JavaScript variable name.
205 * @param {string|Element} opt_title this optional field is used to visibly
206 * denote the category title. It can be either HTML or an element.
207 * @return {Element} the newly created div.
208 */
209 Menu.prototype.addCategory = function(category, opt_title) {
210 this.categories = this.categories || [];
211 var categoryDiv = this.createElement('div', 'menuCategory ' + category);
212 categoryDiv._categoryName = category;
213 if (opt_title) {
214 var categoryTitle = this.createElement('b', 'categoryTitle ' +
215 category, opt_title);
216 categoryTitle.style.display = 'block';
217 this.append(categoryTitle);
218 categoryDiv._categoryTitle = categoryTitle;
219 }
220 this.append(categoryDiv);
221 this.categories[this.categories.length] = this.categories[category] =
222 categoryDiv;
223
224 return categoryDiv;
225 };
226
227 /**
228 * This method removes the contents of a given category but does not
229 * remove the category itself.
230 */
231 Menu.prototype.emptyCategory = function(category) {
232 if (!this.categories[category]) {
233 return;
234 }
235 var div = this.categories[category];
236 for (var i = div.childNodes.length - 1; i >= 0; i--) {
237 div.removeChild(div.childNodes[i]);
238 }
239 };
240
241 /**
242 * This function is the most drastic of the cleansing functions; it removes
243 * all categories and all menu items and all HTML snippets that have been
244 * added to this instance of the Menu class.
245 */
246 Menu.prototype.clear = function() {
247 for (var i = 0; i < this.categories.length; i++) {
248 // Prevent memory leaks
249 this.categories[this.categories[i]._categoryName] = null;
250 }
251 this.items.splice(0, this.items.length);
252 this.categories.splice(0, this.categories.length);
253 this.categories = [];
254 this.items = [];
255 for (var i = this.menu.childNodes.length - 1; i >= 0; i--) {
256 this.menu.removeChild(this.menu.childNodes[i]);
257 }
258 };
259
260 /**
261 * Passed an instance of a menu item, it will be removed from the menu
262 * object, including any residual array links and possible memory leaks.
263 * @param {Element} item a reference to the menu item to remove.
264 * @return {Element} returns the item removed.
265 */
266 Menu.prototype.removeItem = function(item) {
267 var result = null;
268 for (var i = 0; i < this.items.length; i++) {
269 if (this.items[i] == item) {
270 result = this.items[i];
271 this.items.splice(i, 1);
272 }
273 // Renumber
274 this.items[i].item._index = i;
275 }
276 return result;
277 };
278
279 /**
280 * Removes a category from the menu element and all of its children thus
281 * allowing the Element to be collected by the browsers VM.
282 * @param {string} category the name of the category to retrieve and remove.
283 */
284 Menu.prototype.removeCategory = function(category) {
285 var div = this.categories[category];
286 if (!div || !div.parentNode) {
287 return;
288 }
289 if (div._categoryTitle) {
290 div._categoryTitle.parentNode.removeChild(div._categoryTitle);
291 }
292 div.parentNode.removeChild(div);
293 for (var i = 0; i < this.categories.length; i++) {
294 if (this.categories[i] === div) {
295 this.categories[this.categories[i]._categoryName] = null;
296 this.categories.splice(i, 1);
297 return;
298 }
299 }
300 for (var i = 0; i < div.childNodes.length; i++) {
301 if (div.childNodes[i]._index) {
302 this.items.splice(div.childNodes[i]._index, 1);
303 } else {
304 this.removeItem(div.childNodes[i]);
305 }
306 }
307 };
308
309 /**
310 * This heart of the menu population scheme, the addItem function creates
311 * a combination of elements that visually form up a menu item. If no
312 * category is supplied, the default category is used. The menu item is
313 * an <a> tag with the class .menuItem. The menu item is directly styled
314 * as a block element. Other than that, all styling should be done via a
315 * external CSS definition.
316 *
317 * @param {string|Element} html_or_element a string of HTML text or a
318 * HTML element denoting the contents of the menu item.
319 * @param {string} opt_href the href of the menu item link. This is
320 * the most direct way of defining the menu items function.
321 * [Default: '#'].
322 * @param {string} opt_category the category string name of the category
323 * to append the menu item to. If the category doesn't exist, one will
324 * be created. [Default: 'default'].
325 * @param {string} opt_title used when creating a new category and is
326 * otherwise ignored completely. It is also ignored when supplied if
327 * the named category already exists.
328 * @return {Element} returns the element that was created.
329 */
330 Menu.prototype.addItem = function(html_or_element, opt_href, opt_category,
331 opt_title) {
332 var category = opt_category ? (this.categories[opt_category] ||
333 this.addCategory(opt_category, opt_title)) :
334 this.categories['default'];
335 var menuHref = (opt_href == undefined ? '#' : opt_href);
336 var menuItem = undefined;
337 if (menuHref) {
338 menuItem = this.createElement('a', 'menuItem', html_or_element);
339 } else {
340 menuItem = this.createElement('span', 'menuText', html_or_element);
341 }
342 var itemText = typeof html_or_element == 'string' ? html_or_element :
343 html_or_element.innerText || 'ERROR';
344
345 menuItem.style.display = 'block';
346 if (menuHref) {
347 menuItem.setAttribute('href', menuHref);
348 }
349 menuItem._index = this.items.length;
350 menuItem.role = 'option';
351 this.append(menuItem, category);
352 this.items[this.items.length] = {item: menuItem, text: itemText};
353
354 return menuItem;
355 };
356
357 /**
358 * Adds a visual HTML separator to the menu, optionally creating a
359 * category as per addItem(). See above.
360 * @param {string} opt_category the category string name of the category
361 * to append the menu item to. If the category doesn't exist, one will
362 * be created. [Default: 'default'].
363 * @param {string} opt_title used when creating a new category and is
364 * otherwise ignored completely. It is also ignored when supplied if
365 * the named category already exists.
366 */
367 Menu.prototype.addSeparator = function(opt_category, opt_title) {
368 var category = opt_category ? (this.categories[opt_category] ||
369 this.addCategory(opt_category, opt_title)) :
370 this.categories['default'];
371 var hr = this.createElement('hr', 'menuSeparator');
372 this.append(hr, category);
373 };
374
375 /**
376 * This method performs all the dirty work of positioning the menu. It is
377 * responsible for dynamic sizing, insertion and deletion of scroll bars
378 * and calculation of offscreen width considerations.
379 */
380 Menu.prototype.adjustSizeAndLocation = function() {
381 var style = this.menu.style;
382 style.position = 'absolute';
383
384 var firstCategory = null;
385 for (var i = 0; i < this.categories.length; i++) {
386 this.categories[i].className = this.categories[i].className.
387 replace(/ first/, '');
388 if (this.categories[i].childNodes.length == 0) {
389 this.categories[i].style.display = 'none';
390 } else {
391 this.categories[i].style.display = '';
392 if (!firstCategory) {
393 firstCategory = this.categories[i];
394 firstCategory.className += ' first';
395 }
396 }
397 }
398
399 var alreadyVisible = style.display != 'none' &&
400 style.visibility != 'hidden';
401 var docElemWidth = document.documentElement.clientWidth;
402 var docElemHeight = document.documentElement.clientHeight;
403 var pageSize = {
404 w: (window.innerWidth || docElemWidth && docElemWidth > 0 ?
405 docElemWidth : document.body.clientWidth) || 1,
406 h: (window.innerHeight || docElemHeight && docElemHeight > 0 ?
407 docElemHeight : document.body.clientHeight) || 1
408 };
409 var targetPos = this.find(this.target);
410 var targetSize = {w: this.target.offsetWidth,
411 h: this.target.offsetHeight};
412 var menuSize = {w: this.menu.offsetWidth, h: this.menu.offsetHeight};
413
414 if (!alreadyVisible) {
415 var oldVisibility = style.visibility;
416 var oldDisplay = style.display;
417 style.visibility = 'hidden';
418 style.display = '';
419 style.height = '';
420 style.width = '';
421 menuSize = {w: this.menu.offsetWidth, h: this.menu.offsetHeight};
422 style.display = oldDisplay;
423 style.visibility = oldVisibility;
424 }
425
426 var addScroll = (this.menu.offsetHeight / pageSize.h) > 0.8;
427 if (addScroll) {
428 menuSize.h = parseInt((pageSize.h * 0.8), 10);
429 style.height = menuSize.h + 'px';
430 style.overflowX = 'hidden';
431 style.overflowY = 'auto';
432 } else {
433 style.height = style.overflowY = style.overflowX = '';
434 }
435
436 style.top = (targetPos.y + targetSize.h) + 'px';
437 style.left = targetPos.x + 'px';
438
439 if (menuSize.w < 175) {
440 style.width = '175px';
441 }
442
443 if (addScroll) {
444 style.width = parseInt(style.width, 10) + 13 + 'px';
445 }
446
447 if ((targetPos.x + menuSize.w) > pageSize.w) {
448 style.left = targetPos.x - (menuSize.w - targetSize.w) + 'px';
449 }
450 };
451
452
453 /**
454 * This function is used heavily, internally. It appends text
455 * or the supplied element via appendChild(). If
456 * the opt_target variable is present, the supplied element will be
457 * the container rather than the menu div for this instance.
458 *
459 * @param {string|Element} text_or_element the html or element to insert
460 * into opt_target.
461 * @param {Element} opt_target the target element it should be appended to.
462 *
463 */
464 Menu.prototype.append = function(text_or_element, opt_target) {
465 var element = opt_target || this.menu;
466 if (typeof opt_target == 'string' && this.categories[opt_target]) {
467 element = this.categories[opt_target];
468 }
469 if (typeof text_or_element == 'string') {
470 element.innerText += text_or_element;
471 } else {
472 element.appendChild(text_or_element);
473 }
474 };
475
476 /**
477 * Displays the menu (such as upon mouseover).
478 */
479 Menu.prototype.over = function() {
480 if (this.menu.style.display != 'none') {
481 this.show();
482 }
483 if (this.thread != -1) {
484 clearTimeout(this.thread);
485 this.thread = -1;
486 }
487 };
488
489 /**
490 * Hides the menu (such as upon mouseout).
491 */
492 Menu.prototype.out = function() {
493 if (this.thread != -1) {
494 clearTimeout(this.thread);
495 this.thread = -1;
496 }
497 this.thread = setTimeout(this.wrap(this.hide), 400);
498 };
499
500 /**
501 * Stops event propagation.
502 */
503 Menu.prototype.stopPropagation = function() {
504 return (function(e) {
505 if (!e) {
506 e = window.event;
507 }
508 e.cancelBubble = true;
509 if (e.stopPropagation) {
510 e.stopPropagation();
511 }
512 });
513 };
514
515 /**
516 * Toggles the menu between hide/show.
517 */
518 Menu.prototype.toggle = function() {
519 if (this.menu.style.display == 'none') {
520 this.show();
521 } else {
522 this.hide();
523 }
524 };
525
526 /**
527 * Makes the menu visible, then calls the user-supplied onShow callback.
528 */
529 Menu.prototype.show = function() {
530 if (this.menu.style.display != '') {
531 for (var i = 0; i < this.onOpenEvents.length; i++) {
532 this.onOpenEvents[i].call(null, this);
533 }
534
535 // Invisibly show it first
536 this.menu.style.visibility = 'hidden';
537 this.menu.style.display = '';
538 this.adjustSizeAndLocation();
539 if (this.trigger.nodeName && this.trigger.nodeName == 'A') {
540 this.trigger.blur();
541 }
542 this.menu.style.visibility = 'visible';
543
544 // Hide other menus
545 for (var i = 0; i < Menu.instance.length; i++) {
546 var menuInstance = Menu.instance[i];
547 if (menuInstance != this) {
548 menuInstance.hide();
549 }
550 }
551
552 if (this.onShow) {
553 this.onShow();
554 }
555 }
556 };
557
558 /**
559 * Makes the menu invisible.
560 */
561 Menu.prototype.hide = function() {
562 this.menu.style.display = 'none';
563 };
564
565 Menu.prototype.find = function(element) {
566 var curleft = 0, curtop = 0;
567 if (element.offsetParent) {
568 do {
569 curleft += element.offsetLeft;
570 curtop += element.offsetTop;
571 }
572 while ((element = element.offsetParent) && (element.style &&
573 element.style.position != 'relative' &&
574 element.style.position != 'absolute'));
575 }
576 return {x: curleft, y: curtop};
577 };
578
579 /**
580 * A static array of object instances for global reference.
581 * @type {Array.<Menu>}
582 */
583 Menu.instance = [];
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698