Remoto - VFS: paneMenu.js Source File
Remoto - VFS
paneMenu.js
Go to the documentation of this file.
1 
2 define( [
3  'include/modal/modal',
4  'remoto!stdlib:js/include/utils.js',
5  'remoto!stdlib:js/include/objectRegistry.js',
6  'jquery/jquery',
7  'jquery/ui/jquery.ui.core', //for the directory arrow icon
8  "remoto!stdlib:js/paneManager/paneMenu.css",
9  ],
10  function(modal,utils,objectRegistry)
11  {
12  'use strict';
13 
14  var _menuManager;
15  var MAX_ITEM_LENGTH = 50;
16 
38  function paneMenuItem( path, label, callback, data, test, icon, submenu )
39  {
40  this._path = path;
41  this._label = label;
42  this._callback = callback;
43  this._data = data || {};
44  this._test = test || (submenu ? this.submenuEmptyTest.bind(this) : null); //FIXME: a default test for submenus being empty should be implemented, however addItem / addSubmenu are the way it's currently implemented.
45  this._icon = icon;
46  this._submenu = submenu;
47 
48  this._html = null;
49 
50  //console.log(arguments);
51  //console.trace();
52  }
53 
54  paneMenuItem.prototype = {
55 
68  createHTML: function(_modal,id,caller)
69  {
70  if (this._html)
71  { this.activate(_modal,id,caller);
72  return this._html;
73  }
74 
75  var j = $("<div class='paneMenuItem'>");
76  if (typeof this._label === "function")
77  j.text( this._label().toString().substring(0,MAX_ITEM_LENGTH) );
78  else
79  j.text( this._label.substring(0,MAX_ITEM_LENGTH) || "[no name]" );
80 
81  if (this._icon)
82  {
83  var ii = (typeof this._icon ==="function") ? this._icon() : this._icon;
84 
85  if (ii.startsWith("data:image/svg+xml;utf8,"))
86  {
87  ii = $(ii.replace("data:image/svg+xml;utf8,",""));
88  ii.addClass("paneMenuIcon");
89  }
90  else if (ii.startsWith("data:image/"))
91  {
92  ii = $("<img>").attr("src",ii).addClass("paneMenuIcon");
93  }
94 
95  j.prepend( ii );
96  }
97 
98  //if (this._submenu) j.css("padding-right","17px").append("<span class='ui-icon ui-icon-triangle-1-e' style='display:inline-block; position:absolute; top:4px; right:0px;' />");
99  if (this._submenu) j.css("padding-right","17px").append("<span class='subMenu'>&#9656;</span>");
100 
101  //j.bind("mouseleave", false);
102 
103  this._html = j;
104 
105  this.activate(_modal,id,caller);
106 
107  return this._html;
108  },
109 
121  activate: function(_modal,id,caller)
122  {
123  //this._html.find(".paneMenu").remove();
124  //modal.dismissTo(_modal);
125 
126  if (typeof this._label === "function")
127  { this._html.text( this._label() );
128  if (this._icon) this._html.prepend( (typeof this._icon ==="function") ? this._icon() : this._icon);
129  if (this._submenu) this._html.css("padding-right","17px").append("<span class='ui-icon ui-icon-triangle-1-e' style='display:inline-block; position:absolute; top:4px; right:0px;' />");
130  }
131 
132  if (!this._test || this._test===true || (this._test && this._test())) //run test to see if menu item is valid, and link to the modal
133  { //if there is no test, or the test succeeds, enable the button.
134  this._html.removeClass("paneMenuItemDisabled");
135  this._html.unbind("click mouseenter");
136  if (this._submenu)
137  { var THIS = this;
138  this._html.bind("mouseenter", function(e)
139  {
140  var p = (THIS._path instanceof Function) ? THIS._path() : (THIS._path || THIS._data.menu);
141  var s = _menuManager.openMenu( p, THIS._html, "R", caller.data );
142  if (!s) return false;
143  s.chain( { "dismiss": function()
144  {
145  //console.log("called dismiss for "+THIS._label);
146  if (!THIS._html.parent().is(":hover"))
147  return _modal.execEvent("dismiss");
148  else if (!THIS._html.is(":hover"))
149  { modal.dismissTo(_modal);
150  //THIS._html.find(".paneMenu").remove();
151  }
152  },
153  "*": function(eventName, e)
154  {
155  //console.log(arguments);
156  //console.log("called * for "+THIS._label);
157  //console.log(caller);
158  if (THIS._callback)
159  { var p = (typeof eventName == "number") ? _menuManager.getMenu(THIS._data.menu)._items[eventName]._data : eventName;
160  //THIS._callback.call(THIS,_menuManager._paneManager._primaryCell,p);
161  //console.log(p,this);
162  THIS._callback.call(THIS,caller.data,p);
163  //return _modal.execEvent("dismiss"); //fixme
164  }
165  else if (eventName)
166  {
167  var m = (THIS._data.menu instanceof Function) ? THIS._data.menu() : THIS._data.menu;
168  //console.log(m,eventName);
169  var item = _menuManager.getMenu(m)._items[eventName]._data;
170  //console.log(item);
171  return _modal.execEvent("*",item);
172  }
173  },
174  } );
175 
176  return false;
177  }
178  );
179  this._html.bind("mouseleave", function() {
180  modal.dismissTo(_modal);
181  return false;
182  } );
183  }
184  else
185  { //this._html.bind("click mouseup",_modal.execEvent.bind(_modal,id));
186  this._html.bind("click", function(e) { //DON'T add additional bind events... mouseup or touchend would be bad here... we need a "click" to bubble to close parent menus
187  _modal.execEvent(id);
188  //modal.dismissTo(null); //DON'T dismiss menus, the menus will already close on their own
189  //return false; //DON'T return false, let the event bubble so other menus will close in their own time
190  } );
191  }
192  }
193  else
194  { this._html.addClass("paneMenuItemDisabled");
195  this._html.unbind("click mouseenter");
196  }
197  },
198 
209  setMenuItem: function(l,t)
210  {
211  if (l)
212  { this._label = l;
213  this._html = null; //so it redraws
214  this._data["label"] = l;
215  }
216 
217  if (t)
218  { this._data["type"] = t;
219  }
220  },
221 
231  submenuEmptyTest: function()
232  {
233  return false;
234  },
235 
236  getMetadata: function()
237  {
238  //console.log("paneMenuItem getMetadata: "+this._path);
239  objectRegistry.metadataPath(this._path,this);
240  },
241 
242  applyMetadata: function(path,metadata)
243  {
244  //console.log("menu item subscribe/metadata",arguments);
245 
246  if ("icon" in metadata)
247  this._icon = metadata.icon;
248 
249  this._metadata = metadata;
250  //this._path = path;
251 
252  this._html = null; //so it rebuilds/redraws
253  },
254  }
255 
271  function paneMenuSpacer()
272  {
273  this._html = null;
274  }
275 
276  paneMenuSpacer.prototype = {
277 
287  createHTML: function()
288  {
289  if (this._html) return this._html;
290 
291  //return this._html = $("<hr style='padding: 0px; border: 1px solid var(--FG3); -webkit-margin-after:none; -webkit-margin-before: none;'/>");
292  return this._html = $("<hr/>");
293  },
294  }
295 
311  function paneMenu(path,subscribe)
312  {
313  this._path = path || "";
314  this._subscribe = subscribe;
315  this._classes = null;
316  this._defaultHandler = null;
317  this._items = [];
318  this._submenus = {};
319 
320  if (subscribe)
321  objectRegistry.registerObject(this._path,this);
322  }
323 
324  paneMenu.prototype = {
325 
335  applyDiff: function(d)
336  {
337  //console.log("menu diff",arguments);
338  //console.error("diff menu items for "+this._path+" here!");
339  //return;
340 
341  //FIXME: this does not sort out items added later that have an index
342 
343  if (d === null)
344  return;
345 
346  var diff = d;
347  var k = Object.keys(diff);
348  k.sort( function(a,b) {
349  var aa = diff[a].index || a || 0;
350  var bb = diff[b].index || b || 0;
351  if (aa > bb) return 1;
352  if (aa < bb) return -1;
353  return 0;
354  } );
355 
356  var _i,m,p;
357  var path,name;
358  //for (var p in d)
359  for (var i=0;i<k.length;i++)
360  {
361  p = k[i];
362  m = d[p];
363 
364  //console.log(m);
365 
366  name = m.name || utils.fileNameFromPath(p);
367  path = utils.cleanPath(this._path+"/"+p);
368  if (m !== null) //an add
369  {
370  if (this.hasItemWithPath(path))
371  continue;
372 
373  //if it's a container
374  var submenu = false;
375  if ((typeof m === "boolean" && m === true) || (typeof m === "object" && m.container))
376  submenu = true;
377 
378  if (m instanceof Object && m.type === "spacer")
379  {
380  this.addItem("spacer");
381  }
382  else
383  {
384  if (submenu)
385  { //this.addSubMenu( name, path, m.icon );
386  _i = this.addSubMenu( name, path, "<span class='paneMenuIcon'></span>" );
387  var mm = _menuManager.createMenu( path, path, this._subscribe );
388  mm._classes = this._classes;
389  mm._defaultHandler = this._defaultHandler;
390  this._submenus[path] = mm;
391  }
392  else
393  {
394  _i = new paneMenuItem( path,
395  name,
396  m.callback, //if null, will fail over to the submenu's action
397  m, //for data, include everything we got
398  m.test,
399  m.icon,
400  m.submenu
401  );
402 
403  this.addItem( _i );
404  }
405 
406 
407  if (this._subscribe)
408  {
409  /*
410  if (!(m.metadata && m.metadata.action))
411  _i.getMetadata();
412  else
413  _i._metadata = m.metadata;
414  */
415  if (!m.metadata)
416  _i.getMetadata();
417  else
418  _i.applyMetadata(path,m.metadata);
419  }
420 
421  // --or--
422  // m.label = m.name;
423  // this.addItem( m );
424  }
425  }
426  else //remove
427  {
428  //console.error("Remove menu item here: "+p);
429  //console.log(d);
430  this.removeItem(path)
431  }
432  }
433 
434  //console.log(this._items);
435  },
436 
446  removeItem: function(p)//,recursive)
447  {
448  //console.log("remove menu item: "+p);
449 
450  if (typeof p == "number")
451  {
452  this._items.splice(p,1);
453  }
454  else if (typeof p == "string")
455  {
456  for (var i=0;i<this._items.length;i++)
457  { //console.log(this._items[i]._path);
458  if (this._items[i]._path === p)
459  { this._items.splice(i,1);
460  break;
461  }
462  }
463  }
464  else if ((p instanceof paneMenuSpacer) || (p instanceof paneMenuItem))
465  {
466  for (var i=0;i<this._items.length;i++)
467  { if (this._items[i] === p)
468  { this._items.splice(i,1);
469  break;
470  }
471  }
472  }
473  else
474  { console.warn("Unknown removeItem index:");
475  console.warn(p);
476  }
477 
478  if (this._items.length === 0)
479  { //FIXME: in general no trigger is needed, but there may be cases where a trigger is useful, like for submenus
480  //console.warn("The menu is now empty... should probably do something.");
481  //console.trace(this);
482  //console.log(_menuManager);
483  }
484  },
485 
494  removeAllItems: function()
495  {
496  this._items = [];
497  console.log("remove all menu items!");
498  },
499 
512  addItem: function(p,l,t)
513  {
514  var m,_i=null;
515 
516  if (arguments.length == 1) //usually from diffs or spacers, but there are other cases
517  m = p;
518 
519  if (arguments.length == 3) //manual function method
520  { _i = new paneMenuItem( p,
521  l,
522  null,
523  { path: p,
524  label: l,
525  type: t,
526  },
527  true,
528  null
529  );
530  }
531  else if (m === "spacer") //spacer
532  {
533  _i = new paneMenuSpacer();
534  }
535  else if (m instanceof paneMenuItem)
536  {
537  _i = m;
538  }
539  else if (m instanceof Object) //manual object method
540  {
541  _i = new paneMenuItem( m.path,
542  m.label,
543  m.callback,
544  m.data,
545  m.test,
546  m.icon,
547  m.submenu
548  );
549  }
550 
551  this._items.push( _i );
552 
553  if (!_i) //an error of some kind
554  console.error(arguments);
555 
556  return _i; //return the new item
557  },
558 
571  addSubMenu: function(l,m,icon,submenuHandler)
572  {
573  return this.addItem( {
574  path: m,
575  label: l,
576  callback: submenuHandler,
577  data: { menu:m },
578  test: this.submenuTest.bind(this,m),
579  icon: icon,
580  submenu: true,
581  } );
582  },
583 
592  hasItemWithPath: function(p)
593  {
594  for (var i=0;i<this._items.length;i++)
595  if (this._items[i]._path === p)
596  return true;
597 
598  return false;
599  },
600 
609  hasItemWithLabel: function(l)
610  {
611  for (var i=0;i<this._items.length;i++)
612  if (this._items[i]._label === l)
613  return true;
614 
615  return false;
616  },
617 
630  createModalMenu:function(button,position,dismissCallback)
631  {
632  var THIS = this;
633  position = (/^[TRBL]$/.test(position)) ? position : "B"; //sanitize position
634 
635  var m = {
636  exec: function(_modal)
637  {
638  //create menu item
639  var _m = $("<div class='paneMenu' />");
640  var _b = $("<div class='paneMenuGutter' />").appendTo(_m);
641 
642  if (THIS._classes)
643  _m.addClass(THIS._classes);
644 
645  for (var i=0; i<THIS._items.length; i++)
646  _m.append( THIS._items[i].createHTML(_modal,i,this) ); //where this refers to 'm'... the object which contains exec().
647 
648  var _sub = !(modal.showing < 2);
649 
650  //mount the menu, so it's in the dom and sizing can be calculated.
651  if (!_sub)
652  _modal._mount.append(_m);
653  else
654  { button.append && button.append(_m);
655  _modal._jq = _m;
656  }
657 
658  //do positioning
659  var p = (!button.offset) ? { left:button.clientX, top:button.clientY } : button.offset();
660  var d = (!button.offset) ? { w:0, h:0 } :{ w:button.outerWidth(), h:button.outerHeight() };
661  var m = { w:_m.outerWidth(), h:_m.outerHeight() };
662 
663  switch(position)
664  {
665  case "T": _m.offset( { left:p.left, top:(p.top-m.h+1) } ); break;
666  case "R": _m.offset( { left:(p.left+d.w), top:(p.top-1) } ); break;
667  case "B": _m.offset( { left:p.left, top:(p.top+d.h-1) } ); break;
668  case "L": _m.offset( { left:(p.left-m.w+1), top:p.top } ); break;
669  default: console.log("bad menu position: "+position);
670  }
671 
672  var n = _m.offset();
673  var w = { w:$(window).width(), h:$(window).height() };
674 
675  if (n.left < 0) _m.css( "left", p.left+d.w );
676  if (n.left+m.w > w.w) _m.css( "left", (_sub?0:p.left)-m.w+(/[TB]/.test(position)?d.w:0) );
677 
678  if (n.top < 0) _m.css( "top", p.top+d.h );
679  if (n.top+m.h > w.h) _m.css( "top", (_sub?0:p.top)-m.h+1+(/[LR]/.test(position)?d.h:0) );
680 
681  _m.bind("mouseleave", _modal.execEvent.bind(_modal,"dismiss"));
682  _m.bind("mouseover", false );
683  //_modal._mount.bind("click", _modal.execEvent.bind(_modal,"dismiss"));
684  _modal._mount.bind("click", function() { modal.dismissTo(null); /*return false;*/ });
685 
686  if (dismissCallback)
687  _modal.chain( { "*": dismissCallback } );
688 
689  return _modal;
690  },
691  };
692 
693  return m;
694  },
695 
707  modalMenuEvents:function(dismissCallback,defaultCallback)
708  {
709  var THIS = this;
710  var m = {};
711 
712  for (var i=0; i<this._items.length; i++)
713  if (this._items[i]._callback)
714  m[i] = this._items[i]._callback;
715  else
716  { let j = i+0;
717  m[i] = function() { defaultCallback(j,THIS._items[j]); };
718  }
719 
720  m["dismiss"] = dismissCallback || false;
721  //m["dismiss"] = function()
722  //{
723  // console.log("MENU DISMISS!");
724  //};
725 
726  //m["*"] = false;
727  //m["*"] = function(a) {
728  // console.log("submenu catcher!");
729  // console.log(arguments);
730  // return a;
731  //};
732 
733  return m;
734  },
735 
746  submenuTest: function(menu)
747  {
748  var p = (menu instanceof Function) ? menu() : menu;
749  var m = _menuManager.getMenu( p );
750 
751  if (!m) return false;
752 
753  if (m._items.length)
754  return true;
755 
756  return false;
757  },
758 
765  destroy: function()
766  {
767  for (var s in this._submenus)
768  // this._submenus[s].destroy();
769  _menuManager.removeMenu(s);
770 
771  if (this._subscribe)
772  objectRegistry.unregisterObject(this._path,this,true,false,true);
773 
774  this._items = [];
775  this._submenus = {};
776  },
777  }
778 
800  function menuManager()
801  {
802  this._menus = {};
803  }
804 
805  menuManager.prototype = {
806 
817  createMenu: function(m,p,subscribe)
818  {
819  if (this._menus[m])
820  return this._menus[m];
821 
822  return this._menus[m] = new paneMenu(p,subscribe);
823  },
824 
841  openMenu: function(m,b,p,d,c)
842  { var n = this._menus[m];
843  if (!n) { console.error("tried to open nonexistent menu: "+m); console.log(this._menus); return; }
844  if (n._items.length===0) return null;
845  var u = n.createModalMenu(b,p,c);
846  var e = n.modalMenuEvents(null,n._defaultHandler);
847  u.data = d;
848  return modal.exec(u,e,false,modal.showing);
849  },
850 
861  getMenu: function(m)
862  {
863  return this._menus[m];
864  },
865 
875  removeMenu: function(m)
876  {
877  if (this._menus[m])
878  this._menus[m].destroy();
879 
880  delete this._menus[m];
881  },
882 
891  resetMenus: function()
892  {
893  for (var m in this._menus)
894  this._menus[m].destroy();
895 
896  this._menus={};
897  },
898  }
899 
900  _menuManager = new menuManager();
901 
902  return _menuManager;
903  }
904 );
905 
getter id
returns the number of milliseconds since midnight January 1, 1970 UTC
getter position
A getter, returns this._position.
Manages context menus or any other menu.
createMenu(m, p, subscribe)
Create a paneMenu(). You will want to use paneMenu.addItem() on the resulting object.
menuManager()
Construct a menu manager.
resetMenus()
DOCME.
setter name
a setter DOCME
setter type
a setter DOCME
metadataPath(id, o)
Request metadata for a given path.
applyMetadata(id, metadata)
registerObject(id, o, nosubscribe)
Register an object in the registry, and call the pathAddedCallback.
unregisterObject(id, o, silent, nounsubscribe, now)
Unregister an object from the registry.
Create a menu.
modalMenuEvents(dismissCallback, defaultCallback)
addSubMenu(l, m, icon, submenuHandler)
addItem(p, l, t)
submenuTest()
Test if submenu items exist.
removeAllItems()
Remove all items from this menu.
hasItemWithPath(p)
Check to see if an item already exists with a given path.
hasItemWithLabel(l)
Check to see if an item already exists with a given label.
createModalMenu(button, position, dismissCallback)
paneMenu(path, subscribe)
A menu item.
setMenuItem(l, t)
paneMenuItem(path, label, callback, data, test, icon, submenu)
paneMenuSpacer()
DOCME.
getter path
a getter DOCME
applyDiff(diff, user)
createHTML()
Create the HTML contents of the pane.
Utility functions for javascript clients.
cleanPath(path)
Clean and normalize a resource path.
fileNameFromPath(p, orDirIfNeeded)
Retrieve the last part of a file path.
subscribe(paths)
This will aggregate paths for subscription, using a timer of 0.
metadata(paths)