PageSourceSearch

https://q1e.co.uk/resources/lib/dropdown/Dropdown.src.js

js q1e.co.uk collected 2026-10-04 07:03:37 UTC 8,583 bytes, 329 lines download raw bytes

1// PURE
2/*
3
4Dropdown.js
5
6Creates touch-friendly drop-down menus
7
8Created by Stephen Morley - http://code.stephenmorley.org/ - and released under
9the terms of the CC0 1.0 Universal legal code:
10
11http://creativecommons.org/publicdomain/zero/1.0/legalcode
12
13@version        2.0.0       SPK Changes to fix touch / scroll on mobile
14
15*/
16// create the Dropdown object
17var Dropdown = (function(){
18
19  // the delay, in milliseconds
20  var DELAY = 250;
21
22  // the list of menus
23  var menus = [];
24
25  // Initialises the drop-down menus.
26  function initialise(){
27      
28    // listen for touch events on the document if appropriate
29    if ('createTouch' in document){
30      document.body.addEventListener('touchstart', handleTouchStart, false);
31    }
32
33    // loop over the menus, converting them
34    var menus = document.querySelectorAll('ul.dropdown');
35    for (var i = 0; i < menus.length; i ++) applyTo(menus[i]);
36  }
37
38  /* Handles a touch start event. The parameter is:
39   *
40   * e - the event
41   */
42  function handleTouchStart(e){
43
44    // determine whether any menu is open
45    var isOpen = false;
46    for (var i = 0; i < menus.length; i ++){
47      if (menus[i].isOpen) isOpen = true;
48    }
49
50    // return immediately if all menus are closed
51    if (!isOpen) return;
52
53    // move up the document tree until we reach the root node
54    var node = e.target;
55    while (node != null){
56
57      // return immediately if we are inside a drop-down menu
58      if (/\bdropdown\b/.test(node.className)) return;
59
60      // move onto the parent node
61      node = node.parentNode;
62
63    }
64
65    // close all menus
66    close();
67
68  }
69
70  /* Closes all menus except the specified menu. The parameter is:
71   *
72   * menu - a menu not to close; this parameter is optional
73   */
74  function close(menu){
75
76    // loop over the menus, closing them
77    for (var i = 0; i < menus.length; i ++){
78      if (menus[i] != menu) menus[i].close();
79    }
80
81  }
82
83  /* Creates a drop-down menu. The parameter is:
84   *
85   * node - either the DOM node of the menu or the ID of the node
86   */
87  function applyTo(node){
88    // fetch the DOM node if a string was supplied
89    if (typeof node == 'string') node = document.getElementById(node);
90    // create and store the new menu
91    menus.push(new Menu(node));
92
93  }
94
95  /* Creates a drop-down menu. The parameter is:
96   *
97   * node - the DOM node of the menu
98   */
99  function Menu(node){
100
101    // store the node
102    this.node = node;
103
104    // update the class name
105    node.className += ' dropdownJavaScript';
106
107    // listen for mouse events
108    if ('addEventListener' in node){
109      node.addEventListener('mouseover', this.bind(this.handleMouseOver), false);
110      node.addEventListener('mouseout', this.bind(this.handleMouseOut), false);
111      node.addEventListener('click',    this.bind(this.handleClick),    false);
112    }else{
113      node.attachEvent('onmouseover', this.bind(this.handleMouseOver));
114      node.attachEvent('onmouseout',  this.bind(this.handleMouseOut));
115      node.attachEvent('onclick',     this.bind(this.handleClick));
116    }
117
118    // listen for touch events if appropriate
119    if ('createTouch' in document){
120 //       node.addEventListener('touchstart', this.bind(this.handleClick), false);
121        node.addEventListener('touchend', this.bind(this.handleTouchEnd), false);
122    }
123
124  }
125
126  // whether the menu is open
127  Menu.prototype.isOpen = false;
128
129  // the timeout
130  Menu.prototype.timeout = null;
131
132  /* Binds the specified function to the current object. The parameter is:
133   *
134   * f - the function
135   */
136  Menu.prototype.bind = function(f){
137
138    // return the bound function
139    var thisObject = this;
140    return function(){ f.apply(thisObject, arguments); }
141
142  }
143
144  /* Handles a mouse over event. The parameters are:
145   *
146   * e         - the event
147   * immediate - true to open the menu without a delay
148   */
149  Menu.prototype.handleMouseOver = function(e, immediate){
150    // clear the timeout
151    this.clearTimeout();
152    // find the parent list item
153    var item = ('target' in e ? e.target : e.srcElement);
154    while (item.nodeName != 'LI' && item != this.node) item = item.parentNode;
155
156    // if the target is within a list item, set the timeout
157    if (item.nodeName == 'LI'){
158      this.toOpen  = item;
159      this.timeout =
160          window.setTimeout(this.bind(this.open), (immediate ? 0 : DELAY));
161    }
162
163  }
164
165  // Handles a mouse out event.
166  Menu.prototype.handleMouseOut = function(){
167
168    // clear the timeout
169    this.clearTimeout();
170
171    // set the timeout
172    this.timeout = window.setTimeout(this.bind(this.close), DELAY);
173
174  }
175
176  Menu.prototype.handleTouchEnd = function(e){
177  }
178  
179  /* Handles a click event. The parameter is:
180   *
181   * e - the event
182   */
183  Menu.prototype.handleClick = function(e){
184
185    // close any other menus
186    close(this);
187
188    // find the parent list item
189    var item = ('target' in e ? e.target : e.srcElement);
190    while (item.nodeName != 'LI' && item != this.node) item = item.parentNode;
191
192    // check that the target is within a list item
193    if (item.nodeName == 'LI'){
194
195      // check whether the item has a closed submenu
196      var submenu = this.getChildrenByTagName(item, 'UL');
197      if (submenu.length > 0 && !/\bdropdownOpen\b/.test(item.className)){
198          
199          // This is where touch / click to open menu comes
200          // Need to put some sort of check on scrolling
201          // 
202          // 
203        // open the submenu
204        this.handleMouseOver(e, true);
205
206        // prevent the default action
207        if ('preventDefault' in e){
208          e.preventDefault();
209        }else{
210          e.returnValue = false;
211        }
212
213      }
214
215    }
216
217  }
218
219  // Clears the timeout.
220  Menu.prototype.clearTimeout = function(){
221
222    // clear the timeout
223    if (this.timeout){
224      window.clearTimeout(this.timeout);
225      this.timeout = null;
226    }
227  }
228
229  // Opens the last item hovered over.
230  Menu.prototype.open = function(){
231    // store that the menu is open
232    this.isOpen = true;
233
234    // loop over the list items with the same parent
235    var items = this.getChildrenByTagName(this.toOpen.parentNode, 'LI');
236    for (var i = 0; i < items.length; i ++){
237
238      // check whether there is a submenu
239      var submenu = this.getChildrenByTagName(items[i], 'UL');
240      if (submenu.length > 0){
241
242        // check whether the submenu should be opened or closed
243        if (items[i] != this.toOpen){
244
245          // close the submenu
246          items[i].className =
247              items[i].className.replace(/\bdropdownOpen\b/g, '');
248          this.close(items[i]);
249
250        }else if (!/\bdropdownOpen\b/.test(items[i].className)){
251
252          // open the submenu
253          items[i].className += ' dropdownOpen';
254
255          // determine the location of the edges of the submenu
256          var left = 0;
257          var node = submenu[0];
258          while (node){
259            left += node.offsetLeft;
260            node = node.offsetParent;
261          }
262          right = left + submenu[0].offsetWidth;
263
264          // move the submenu to the right of the item if appropriate
265          if (left < 0) items[i].className += ' dropdownLeftToRight';
266
267          // move the submenu to the left of the item if appropriate
268          if (right > document.body.clientWidth){
269            items[i].className += ' dropdownRightToLeft';
270          }
271
272        }
273
274      }
275
276    }
277
278  }
279
280  /* Closes the menus within the specified node. The parameter is:
281   *
282   * node - the node; if omitted, all menus are closed
283   */
284  Menu.prototype.close = function(node){
285
286    // if no node was specified, close all menus
287    if (!node){
288      this.isOpen = false;
289      node        = this.node;
290    }
291
292    // loop over the items, closing their submenus
293    var items = node.getElementsByTagName('li');
294    for (var i = 0; i < items.length; i ++){
295      items[i].className = items[i].className.replace(/\bdropdownOpen\b/g, '');
296    }
297
298  }
299
300  /* Returns an array containing the children of the specified node with the
301   * specified tag name. The parameters are:
302   *
303   * node    - the node
304   * tagName - the tag name
305   */
306  Menu.prototype.getChildrenByTagName = function(node, tagName){
307
308    // initialise the list of children
309    var result = [];
310
311    // loop over the children, adding those with the right tag name to the list
312    for (var i = 0; i < node.childNodes.length; i ++){
313      if (node.childNodes[i].nodeName == tagName){
314        result.push(node.childNodes[i]);
315      }
316    }
317
318    // return the children
319    return result;
320
321  }
322
323  // return the public API
324  return {
325    initialise : initialise,
326    applyTo    : applyTo
327  };
328
329})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.