define(function(require, module, exports) { return function(apf) { var $setTimeout = setTimeout; var $setInterval = setInterval; apf.popup = { cache: {}, focusFix: { "INPUT": 1, "TEXTAREA": 1, "SELECT": 1 }, setContent: function(cacheId, content, style, width, height) { if (!this.popup) this.init(); this.cache[cacheId] = { content: content, style: style, width: width, height: height }; content.style.position = "absolute"; return content.ownerDocument; }, removeContent: function(cacheId) { this.cache[cacheId] = null; delete this.cache[cacheId]; }, init: function() { //consider using iframe this.popup = {}; apf.addEventListener("hotkey", function(e) { if (e.keyCode == "27" || e.altKey) apf.popup.forceHide(); }); }, show: function(cacheId, options) { if (!this.popup) this.init(); options = apf.extend({ x: 0, y: 0, animate: false, ref: null, width: null, height: null, callback: null, draggable: false, resizable: false, allowTogether: false, autoCorrect: true, noleft: false, setZindex: true }, options); if ((!options.allowTogether || options.allowTogether !== true && options.allowTogether != this.last) && this.last != cacheId && this.cache[this.last] && (!this.cache[this.last].options || this.cache[this.last].options.autohide !== false)) this.hide(); var o = this.cache[cacheId]; o.options = options; var dp, popup = o.content, moveUp = false, moveLeft = false, fixed = false; if (options.setZindex) apf.window.zManager.set(options.zindextype || "popup", o.content); if ((dp = o.content.style.display) && dp.indexOf("none") > -1) o.content.style.display = ""; var x = options.x; var y = options.y; var refNode = options.ref; while (refNode && refNode.nodeType == 1) { if (fixed = apf.getStyle(refNode, "position") == "fixed") break; refNode = refNode.parentNode || refNode.$parentNode; } if (!fixed) { if (refNode) { var pos = apf.getAbsolutePosition(options.ref, o.content.offsetParent || o.content.parentNode); x = (x || 0) + pos[0]; y = (y || 0) + pos[1]; } if (options.width || o.width) popup.style.width = ((options.width || o.width) - 3) + "px"; popup.style.position = "absolute"; popup.style.maxHeight = ""; var parentMenu = this.cache[options.allowTogether]; var pOverflow = apf.getOverflowParent(o.content); var edgeY = (pOverflow == document.documentElement ? window.innerHeight + window.pageYOffset + pOverflow.scrollTop : pOverflow.offsetHeight + pOverflow.scrollTop); moveUp = options.up || options.autoCorrect && (y + (options.height || o.height || o.content.offsetHeight)) > edgeY; var maxHeight = 0; if (moveUp) { var value; var height = (options.height || o.height || o.content.offsetHeight); if (options.ref) value = (pos[1] - height); else value = Math.max(0, edgeY - height); if (!options.up && value < 0) { moveUp = false; popup.style.top = y + "px"; maxHeight = edgeY - y - this.$screenMargin.bottom - 10; } else { var minTop = this.$screenMargin.top + 3; maxHeight = (pos ? pos[1] : edgeY) - minTop - 10; popup.style.top = Math.max(value, minTop) + "px"; } } else { popup.style.top = y + "px"; maxHeight = edgeY - y - this.$screenMargin.bottom - 10; } popup.style.overflowY = "auto"; popup.style.maxHeight = maxHeight ? maxHeight + "px" : ""; if (!options.noleft) { var edgeX = (pOverflow == document.documentElement ? window.innerWidth + window.pageXOffset + pOverflow.scrollLeft : pOverflow.offsetWidth + pOverflow.scrollLeft); moveLeft = options.autoCorrect && (x + (options.width || o.width || o.content.offsetWidth)) > edgeX; if (moveLeft) { var value; if (options.ref) { value = (pos[0] - (options.width || o.width || o.content.offsetWidth)) + (options.ref.offsetWidth); } else { value = (edgeX - (options.width || o.width || o.content.offsetWidth) - (parentMenu ? (edgeX - parentMenu.content.offsetLeft) : 0)) + 5; //parentMenu.width || parentMenu.content.offsetWidth) : 0)); } popup.style.left = value < 0 ? x : (value - 1) + "px"; } else { popup.style.left = x + "px"; } } } else { pos = apf.getAbsolutePosition(options.ref, refNode); y = (y || 0) + pos[1] + refNode.offsetTop; pos[0] += refNode.offsetLeft; popup.style.position = "fixed"; popup.style.top = y + "px"; if (!options.noleft) popup.style.left = x + "px"; } // set a className that specifies the direction, to help skins with // specific styling options. apf.setStyleClass(popup, moveUp ? "upward" : "downward", [moveUp ? "downward" : "upward"]); apf.setStyleClass(popup, moveLeft ? "moveleft" : "moveright", [moveLeft ? "moveright" : "moveleft"]); if (options.animate) { if (options.animate == "fade") { apf.tween.single(popup, { type: 'fade', from: 0, to: 1, anim: apf.tween.NORMAL, steps: options.steps || 15 * apf.animSteps }); } else { var iVal, steps = 7, i = 0; iVal = setInterval(function() { var value = ++i * ((options.height || o.height) / steps); popup.style.height = value + "px"; if (moveUp) popup.style.top = (y - value - (options.y || 0)) + "px"; else (options.container || popup).scrollTop = -1 * (i - steps) * ((options.height || o.height) / steps); popup.style.display = "block"; if (i >= steps) { clearInterval(iVal); if (options.callback) options.callback(popup); } }, 10); } } else { if (!refNode) { if (options.height || o.height) popup.style.height = (options.height || o.height) + "px"; value = (edgeY - (options.height || o.height || o.content.offsetHeight)); popup.style.top = y + (options.height || o.height || o.content.offsetHeight) < edgeY ? y : value + "px"; } popup.style.display = "block"; if (options.callback) options.callback(popup); } $setTimeout(function() { apf.popup.last = cacheId; }); if (options.draggable) { options.id = cacheId; this.makeDraggable(options); } }, hide: function() { if (this.isDragging) return; var o = this.cache[this.last]; if (o) { if (o.content) o.content.style.display = "none"; if (o.options && o.options.onclose) { o.options.onclose(apf.extend(o.options, { htmlNode: o.content })); o.options.onclose = false; } } }, isShowing: function(cacheId) { return this.last && this.last == cacheId && this.cache[this.last] && this.cache[this.last].content.style.display != "none"; }, isDragging: false, makeDraggable: function(options) { if (!apf.Interactive || this.cache[options.id].draggable) return; var oHtml = this.cache[options.id].content; this.cache[options.id].draggable = true; var o = { $propHandlers: {}, minwidth: 10, minheight: 10, maxwidth: 10000, maxheight: 10000, dragOutline: false, resizeOutline: false, draggable: true, resizable: options.resizable, $ext: oHtml, oDrag: oHtml.firstChild }; oHtml.onmousedown = oHtml.firstChild.onmousedown = function(e) { if (!e) e = event; (e || event).cancelBubble = true; }; apf.implement.call(o, apf.Interactive); o.$propHandlers["draggable"].call(o, true); o.$propHandlers["resizable"].call(o, true); }, getCurrentElement: function() { return typeof this.last == "number" && apf.lookup(this.last); }, $mousedownHandler: function(amlNode, e) { if (!this.last || (amlNode && this.last == amlNode.$uniqueId) || !this.cache[this.last]) return; var htmlNode = e.srcElement || e.target; var uId = this.last; while (this.cache[uId]) { if (apf.isChildOf(this.cache[uId].content, htmlNode, true)) return; if (!this.cache[uId].options) return; uId = this.cache[uId].options.allowTogether; } this.forceHide(); }, forceHide: function() { if (document.body.classList.contains("noInput")) return; if (this.last && !apf.plane.current && this.isShowing(this.last) && this.cache[this.last] && this.cache[this.last].options && this.cache[this.last].options.autohide !== false) { var o = apf.lookup(this.last); if (!o) this.last = null; else if (o.dispatchEvent("popuphide") !== false) this.hide(); } }, destroy: function() { for (var cacheId in this.cache) { if (this.cache[cacheId]) { this.cache[cacheId].content.onmousedown = null; apf.destroyHtmlNode(this.cache[cacheId].content); this.cache[cacheId].content = null; this.cache[cacheId] = null; } } if (!this.popup) return; //this.popup.document.body.c = null; //this.popup.document.body.onmouseover = null; }, setMargin: function(m) { for (var i in this.$screenMargin) if (i in m) this.$screenMargin[i] = m[i] || 0; }, $screenMargin: { top: 0, left: 0, right: 0, bottom: 0 } }; /** * This element displays a skinnable menu of items which can be choosen. * * Based on the context of the menu, items can be shown and hidden. * * * #### Example * * ```xml, demo * * * * Tutorials * Contact * * * Tutorials * * Visit Ajax.org * Exit * * * * * File * Edit * * * * * * ``` * * @class apf.menu * @define menu * @selection * @allowchild item, divider, check, radio * * @author Ruben Daniels (ruben AT ajax DOT org) * @version %I%, %G% * @since 0.4 * * @inherits apf.Presentation */ /** * @event display Fires when the contextmenu is shown. */ /** * @event itemclick Fires when a user presses the mouse button while over a child of this element. * @param {Object} e The standard event object. The following property is available: * - `value` ([[String]]): the value of the clicked element. * */ apf.menu = function(struct, tagName) { this.$init(tagName || "menu", apf.NODE_VISIBLE, struct); this.animate = true; }; (function() { this.$focussable = apf.MENU; this.$positioning = "basic"; //var _self = this; //var blurring = false; // *** Properties and Attributes *** // //this.zindex = 10000000; this.visible = false; this.matchhide = false; this.$booleanProperties["animate"] = true; this.$booleanProperties["pinned"] = true; this.$booleanProperties["sticky"] = true; this.$booleanProperties["matchhide"] = true; this.$propHandlers["visible"] = function(value, prop, force, nofocus, hideOpener) { if (!this.$ext) return; if (value) { this.$ext.style.display = "block"; if (this.opener && this.opener.localName.indexOf('item') > -1) this.opener.parentNode.$showingSubMenu = this; } else { this.$ext.style.display = "none"; var lastFocus = apf.menu.lastFocussed; var opener = this.opener; //@todo test this with a list being the opener of the menu if (lastFocus != this.opener && this.opener && this.opener.$blur) this.opener.$blur(); if (this.opener && this.opener.parentNode && this.opener.parentNode.localName == "menu") { if (!this.$hideTree) this.$hideTree = -1; this.opener.parentNode.focus(); } else if (lastFocus) { //We're being hidden because some other object gets focus if (apf.window.$settingFocus) { if (apf.window.$settingFocus != lastFocus && lastFocus.$blur) lastFocus.$blur(); this.$blur(); if (apf.window.$settingFocus.localName != "menu") //not menu walking apf.menu.lastFocussed = null; } //We're being hidden because window looses focus //We're just being hidden else if (this.$hideTree) { if (!this.$hideTree) this.$hideTree = -1; var visTest = (lastFocus.disabled || lastFocus.$ext && !lastFocus.$ext.offsetHeight) // || !lastFocus.visible && lastFocus != apf.document.documentElement; if (nofocus || visTest) { if (lastFocus.$blur) lastFocus.$blur(); this.$blur(); apf.document.activeElement = null; if (visTest && apf.window.moveNext() === false) apf.window.$focusRoot(); } else { lastFocus.focus(null, null, true); } apf.menu.lastFocussed = null; } } clearTimeout(this.$submenuTimer); if (this.$showingSubMenu) { this.$showingSubMenu.hide(); this.$showingSubMenu = null; } if (this.opener && this.opener.$submenu) { this.opener.$submenu(true, true); //@todo problem with loosing focus when window looses focus if (this.$hideTree === true && this.opener && this.opener.parentNode && this.opener.parentNode.localName == "menu" && this.opener.parentNode.$hideTree != -1) { this.opener.parentNode.$hideTree = true; this.opener.parentNode.hide(); } this.opener = null; } this.$hideTree = null; if (this.$selected) { apf.setStyleClass(this.$selected.$ext, "", ["hover"]); this.$selected = null; } this.dispatchEvent("hide", { opener: opener }); } }; // *** Public Methods *** // var lastFocus; /** * Shows the menu, optionally within a certain context. * @param {Number} x The left position of the menu. * @param {Number} y The top position of the menu. * @param {Boolean} noanim Whether to animate the showing of this menu. * @param {apf.AmlElement} opener The element that is the context of this menu. * @param {XMLElement} xmlNode The {@link term.datanode data node} that provides data context to the menu child nodes. * @see apf.GuiElement@contextmenu */ this.display = function(x, y, noanim, opener, xmlNode, openMenuId, btnWidth) { this.opener = opener; var lastFocus; if (!apf.menu.lastFocussed) lastFocus = apf.menu.lastFocussed = apf.menu.lastFocussedItem; //Show / hide Child Nodes Based on XML if (xmlNode && !this.disabled) { var last, i, node, nodes = this.childNodes, c = 0, l = nodes.length, result; for (i = 0; i < l; i++) { node = nodes[i]; if (node.nodeType != 1 || node.localName != "item") continue; result = !xmlNode || !node.match || (node.cmatch || (node.cmatch = apf.lm.compile(node.match, { xpathmode: 3, injectself: true })))(xmlNode); if (result) { if (this.matchhide) node.show(); else node.enable(); if (node.localName == "divider" && this.matchhide) { last = node; if (c == 0) node.hide(); c = 0; } else c++; } else { if (this.matchhide) node.hide(); else node.disable(); if (!node.nextSibling && c == 0 && last) last.hide(); } } } if (this.oOverlay) { if (btnWidth) { this.oOverlay.style.display = "block"; this.oOverlay.style.width = btnWidth + "px"; } else this.oOverlay.style.display = "none"; } function afterRender() { if (x === null) { apf.popup.show(this.$uniqueId, { x: 0, y: this.ref ? 0 : opener.$ext.offsetHeight, animate: noanim || !this.animate ? false : "fade", steps: 10, ref: (this.ref || opener).$ext, allowTogether: openMenuId, autohide: !this.pinned, noleft: this.left !== undefined, setZindex: this.zindex ? false : true, up: (this.ref || opener).submenudir == "up" }); } else { //var bodyPos = apf.getAbsolutePosition(document.body); apf.popup.show(this.$uniqueId, { x: x, y: y, animate: noanim || !this.animate ? false : "fade", steps: 10, //ref : this.$ext.offsetParent, allowTogether: openMenuId, autohide: !this.pinned, setZindex: this.zindex ? false : true //autoCorrect : false }); } // var lastFocus = // apf.menu.lastFocus = opener && opener.$focussable === true // ? opener // : apf.menu.lastFocus || apf.document.activeElement; apf.popup.last = null; //if (!apf.isGecko) //This disables keyboard support for gecko - very strange behaviour this.focus(); //Make the component that provides context appear to have focus // second argument is needed for ace tree if (lastFocus && lastFocus != this && lastFocus.$focus) lastFocus.$focus(null, { fromContextMenu: true }); this.xmlReference = xmlNode; //@todo consider renaming this to onshow and onhide this.dispatchEvent("display", { opener: opener }); } this.visible = false; if (!this.parentNode) apf.document.documentElement.appendChild(this); if (this.$rendered !== false) { this.show(); afterRender.call(this); } else { this.addEventListener("afterrender", afterRender); this.show(); } }; /** * Returns the current group value of this element. * @return {String} The current selected value. */ this.getValue = function(group) { return this.getSelected(group).value || ""; }; /** * Retrieves the selected element from a group of radio elements. * @param {String} group The name of the group. * @return {apf.radiobutton} The selected radio element. */ this.getSelected = function(group) { var nodes = this.childNodes; var i, l = nodes.length; for (i = 0; i < l; i++) { if (nodes[i].group != group) continue; if (nodes[i].selected) return nodes[i]; } return false; }; /** * Selects an element within a radio group. * @param {String} group The name of the group. * @param {String} value The value of the item to select. */ this.select = function(group, value) { this.selectedValue = value; var nodes = this.childNodes; var i, l = nodes.length; for (i = 0; i < l; i++) { if (nodes[i].group != group) continue; if (value && (nodes[i].value == value || !nodes[i].value && nodes[i].caption == value)) nodes[i].setProperty("selected", true, false, true); //nodes[i].$handlePropSet("selected", true); else if (nodes[i].selected) nodes[i].setProperty("selected", false, false, true); //nodes[i].$handlePropSet("selected", false); } }; // *** Events *** // this.addEventListener("prop.visible", function() { this.$initChildren(); }, true); this.addEventListener("keydown", function(e) { var node, key = e.keyCode; //var ctrlKey = e.ctrlKey; //var shiftKey = e.shiftKey; switch (key) { case 13: if (!this.$selected) return; node = this.$selected; node.$down(); node.$up(); node.$click(); break; case 27: this.hide(); break; case 38: //UP node = this.$selected && this.$selected.previousSibling || this.lastChild; if (node && node.localName == "divider") node = node.previousSibling; if (!node) return; if (this.$selected) apf.setStyleClass(this.$selected.$ext, "", ["hover"]); apf.setStyleClass(node.$ext, "hover"); this.$selected = node; break; case 40: //DOWN node = this.$selected && this.$selected.nextSibling || this.firstChild; if (node && node.localName == "divider") node = node.nextSibling; if (!node) return; if (this.$selected) apf.setStyleClass(this.$selected.$ext, "", ["hover"]); apf.setStyleClass(node.$ext, "hover"); this.$selected = node; break; case 37: //LEFT //if (this.$selected && this.$selected.submenu) //this.$selected.$submenu(true, true); if (!this.opener) return; if (this.opener.localName == "button") { node = this.opener.previousSibling; while (node && !node.submenu) node = node.previousSibling; if (node) { node.dispatchEvent("mouseover"); var btnMenu = node.parentNode.menuIsPressed; if (btnMenu) { self[btnMenu.submenu].dispatchEvent("keydown", { keyCode: 40 }); } } } else if (this.opener.parentNode.localName == "menu") { //@todo Ahum bad abstraction boundary var op = this.opener; this.hide(); apf.setStyleClass(op.$ext, "hover"); op.parentNode.$showingSubMenu = null; } break; case 39: //RIGHT if (this.$selected && this.$selected.submenu) { this.$selected.$submenu(null, true); this.$showingSubMenu.dispatchEvent("keydown", { keyCode: 40 }); return; } if (this.opener) { var op = this.opener; while (op && op.parentNode && op.parentNode.localName == "menu") op = op.parentNode.opener; if (op && op.localName == "button") { node = op.nextSibling; while (node && !node.submenu) node = node.nextSibling; if (node) { node.dispatchEvent("mouseover"); var btnMenu = node.parentNode.menuIsPressed; if (btnMenu) { (self[btnMenu.submenu] || btnMenu.submenu).dispatchEvent("keydown", { keyCode: 40 }); } return; } } } if (!this.$selected) { arguments.callee.call(this, { keyCode: 40 }); } break; default: return; } return false; }, true); //Hide menu when it looses focus or when the popup hides itself function forceHide(e) { if (this.$showingSubMenu || this.pinned || apf.isChildOf(e.fromElement, e.toElement) || apf.isChildOf(e.toElement, e.fromElement) || apf.isChildOf(this, e.toElement) || (e.name !== "popuphide" && !e.toElement) || e.toElement && apf.popup.cache[e.toElement.$uniqueId]) return; if (this.$hideTree != -1) { this.$hideTree = true; this.hide(); } return false; } this.addEventListener("focus", function(e) { apf.popup.last = this.$uniqueId; if (!apf.menu.lastFocussed) apf.menu.lastFocussed = apf.menu.lastFocussedItem; }); this.addEventListener("blur", forceHide); this.addEventListener("popuphide", forceHide); // *** Init *** // this.$draw = function() { this.$pHtmlNode = document.body; //Build Main Skin this.$ext = this.$getExternal(); this.oOverlay = this.$getLayoutNode("main", "overlay", this.$ext); apf.popup.setContent(this.$uniqueId, this.$ext, "", null, null); // workaround for a chrome bug where clicking on shadow clciks on contents of overflown element this.$ext.addEventListener("mouseup", function(e) { var rect = this.getBoundingClientRect(); if (e.clientY > rect.bottom && rect.height) { e.stopPropagation(); e.preventDefault(); } }, true); }; this.$loadAml = function(x) { this.$int = this.$getLayoutNode("main", "container", this.$ext); }; this.$destroy = function() { apf.popup.removeContent(this.$uniqueId); }; this.$initChildren = function() { var ch = this.childNodes; for (var i = 0; i < ch.length; i++) { var amlNode = ch[i]; if (!amlNode.$amlLoaded) amlNode.dispatchEvent("DOMNodeInsertedIntoDocument"); // sometimes DOMNodeInsertedIntoDocument event handler puts $ext at the end of the popup if (!amlNode.previousSibling || !amlNode.previousSibling.$ext || !amlNode.$ext) continue; if (amlNode.$ext.previousSibling == amlNode.previousSibling.$ext) continue; if (amlNode.$ext.parentNode == amlNode.previousSibling.$ext.parentNode) amlNode.$ext.parentNode.insertBefore(amlNode.$ext, amlNode.previousSibling.$ext.nextSibling); } }; var insertBefore = this.insertBefore; this.insertBefore = function(node, beforeNode) { // if menu is visible call apf insertBefore since it rearranges html nodes // otherwise use fake and much faster method if (this.visible) return insertBefore.call(this, node, beforeNode); if (beforeNode == node) return node; if (!this || this == node) throw new Error("Invalid insertBefore call"); var children = this.childNodes; // if (node.parentNode == this) // children[index] if (node.parentNode) node.removeNode(); var index = beforeNode ? children.indexOf(beforeNode) : children.length; node.parentNode = this; if (beforeNode) { children.splice(index, 0, node); } else { children.push(node); } node.previousSibling = children[index - 1]; node.nextSibling = children[index + 1]; if (node.previousSibling) node.previousSibling.nextSibling = node; else this.firstChild = children[0]; if (node.nextSibling) node.nextSibling.previousSibling = node; else this.lastChild = children[this.childNodes.length - 1]; return node; }; this.appendChild = function(node) { return this.insertBefore(node); }; }).call(apf.menu.prototype = new apf.Presentation()); apf.addEventListener("movefocus", function(e) { var next = e.toElement; if (next && next.localName != "menu") apf.menu.lastFocussedItem = next; }); apf.aml.setElement("menu", apf.menu); /** * Element displaying a divider. For use in toolbars, menus, and such. * @class apf.divider * @define divider * @inherits apf.Presentation */ apf.divider = function(struct, tagName) { this.$init(tagName || "divider", apf.NODE_VISIBLE, struct); }; (function() { this.$focussable = false; this.minwidth = 0; this.minheight = 0; this.$childProperty = "caption"; //@todo apf3.0 fix this this.addEventListener("AMLReparent", function(beforeNode, pNode, withinParent) { if (!this.$amlLoaded) return; if (!withinParent && this.skinName != pNode.skinName) { //@todo for now, assuming dom garbage collection doesn't leak this.loadAml(); } }); /** * @attribute {String} caption the text displayed in the area defined by this * element. */ this.$supportedProperties.push("caption", "value", "for", "textalign"); this.$propHandlers["caption"] = function(value) { if (this.$caption) { this.$setStyleClass(this.$ext, this.$baseCSSname + "Caption"); this.$caption.textContent = value; } else { this.$setStyleClass(this.$ext, "", [this.$baseCSSname + "Caption"]); } }; this.$canLeechSkin = true; /** * @private */ this.$draw = function() { if (this.parentNode.isPaged && this.parentNode.$buttons) this.$pHtmlNode = this.parentNode.$buttons; if (this.$isLeechingSkin) { this.$ext = apf.insertHtmlNode( this.parentNode.$getLayoutNode("divider"), this.$pHtmlNode); } else { this.$ext = this.$getExternal("main"); this.$caption = this.$getLayoutNode("main", "caption", this.$ext); } }; }).call(apf.divider.prototype = new apf.Presentation()); apf.aml.setElement("divider", apf.divider); /** * Represents an item in a menu, displaying a clickable area. * * #### Example * * ```xml, demo * * * * Tutorials * Contact * * * * File * * * * * ``` * * @class apf.item * @selection * @define item * @inherits apf.Presentation * */ /** * @event click Fires when a user presses the mouse button while over this element. * @param {Object} e The standard event object. It contains the following properties: * - xmlContext ([[XMLElement]]): The XML data node that was selected in the opener at the time of showing the context menu. * - opener ([[apf.AmlElement]]): The element that was clicked upon when showing the context menu. */ apf.item = function(struct, tagName) { this.$init(tagName || "item", apf.NODE_VISIBLE, struct); }; (function() { this.$focussable = false; this.$childProperty = "caption"; this.$canLeechSkin = "item"; this.checked = false; this.selected = false; // *** Properties and Attributes *** // this.$booleanProperties["checked"] = true; this.$booleanProperties["selected"] = true; this.$supportedProperties.push("submenu", "value", "match", "group", "icon", "checked", "selected", "disabled", "caption", "type", "values"); /** * @attribute {String} submenu Sets or gets the id of the menu that is shown * when the user hovers over this menu item. * * #### Example * * ```xml * * test * test2 * * * * Sub menu * * * * * File * * * ``` */ this.$propHandlers["submenu"] = function(value) { apf.setStyleClass(this.$ext, "submenu"); }; /** * @attribute {String} value Sets or gets the value of this element. */ /** * @attribute {String} [select] Sets or gets the XPath statement which works on the * XML context of the parent menu element to determine whether this * item is shown. * * #### Example * * This example shows a list: * * ```xml * * Send an E-mail * Call Number * * Remove * * View Pictures * * * * Reboot * * * * Please right-click on this plane * * * * * * * * * * * * * * * * * * * * * * * ``` */ this.$propHandlers["select"] = function(value) { this.select = value ? "self::" + value.split("|").join("|self::") : value; }; /** * @attribute {String} [group] Sets or gets the name of the group this item belongs * to. * * #### Example * * ```xml * * item 1 * item 2 * item 3 * item 4 * * ``` */ this.$propHandlers["group"] = function(value) { if (this.$group && this.$group.$removeRadio) this.$group.$removeRadio(this); if (!value) { this.$group = null; return; } var group = typeof value == "string" ? apf.nameserver.get("group", value) : value; if (!group) { group = apf.nameserver.register("group", value, new apf.$group()); group.setAttribute("id", value); group.dispatchEvent("DOMNodeInsertedIntoDocument"); group.parentNode = this; } this.$group = group; this.$group.$addRadio(this); }; /** * @attribute {String} hotkey Sets or gets the key combination a user can press * to active the function of this element. Use any combination of * [[keys: Ctrl]], [[keys: Shift]], [[keys: Alt]], [[keys: F1]]-[[keys: F12]], and alphanumerical characters. Use a * space, a minus or plus sign as a seperator. * * #### Example * * ```xml * Quit * ``` */ this.$propHandlers["hotkey"] = function(value) { }; /** * @attribute {String} icon Sets or gets the URL of the image used as an icon or * a reference to an iconmap. */ this.$propHandlers["icon"] = function(value) { if (this.$icon) apf.skins.setIcon(this.$icon, value, this.parentNode.iconPath); }; /** * @attribute {String} caption Sets or gets the text displayed on the item. */ this.$propHandlers["caption"] = function(value) { if (this.$caption) apf.setNodeValue(this.$caption, value); }; /** * @attribute {String} type Sets or gets the function of this item. * * Possible values include: * - `"item"` * - `"check"` * - `"radio"` */ this.$propHandlers["type"] = function(value) { apf.setStyleClass(this.$ext, value, ["item", "check", "radio"]); }; this.$propHandlers["values"] = function(value) { this.$values = typeof value == "string" ? value.split("\|") : (value || [1, 0]); this.$propHandlers["value"].call(this, this.value); }; this.$propHandlers["value"] = function(value) { if (this.type != "check") return; value = (typeof value == "string" ? value.trim() : value); var checked; if (this.$values) { checked = (typeof value != "undefined" && value !== null && value.toString() == this.$values[0].toString()); } else { checked = apf.isTrue(value); } if (checked != this.checked) { this.checked = checked; this.$propHandlers.checked.call(this, checked); } }; /** * @attribute {Boolean} checked Sets or gets whether the item is checked. */ this.$propHandlers["checked"] = function(value) { if (this.type != "check") return; if (apf.isTrue(value)) apf.setStyleClass(this.$ext, "checked"); else apf.setStyleClass(this.$ext, "", ["checked"]); if (!this.$values) { if (this.getAttribute("values")) this.$propHandlers["values"].call(this, this.getAttribute("values")); else this.$values = [true, false]; } if (this.$values && this.$values[value ? 0 : 1] != this.value) this.setProperty("value", this.$values ? this.$values[value ? 0 : 1] : true); }; this.select = function() { this.parentNode.select(this.group, this.value != undefined ? this.value : this.caption); }; this.check = function() { this.setProperty("value", this.$values ? this.$values[0] : true); }; this.uncheck = function() { this.setProperty("value", this.$values ? this.$values[1] : false); }; this.$check = function() { apf.setStyleClass(this.$ext, "selected"); }; this.$uncheck = function() { apf.setStyleClass(this.$ext, "", ["selected"]); }; /** * @attribute {Boolean} selected Sets or gets whether the item is selected. */ this.$propHandlers["selected"] = function(value) { if (this.type != "radio") return; if (apf.isTrue(value)) { if (this.$group) this.$group.setProperty("value", this.value); this.$check(); } else this.$uncheck(); }; /** * @attribute {Boolean} disabled Sets or gets whether the item is active. */ this.$propHandlers["disabled"] = function(value) { if (apf.isTrue(value) || value == -1) apf.setStyleClass(this.$ext, "disabled"); else apf.setStyleClass(this.$ext, "", ["disabled"]); }; // *** Dom Hooks *** // //@todo apf3.0 this.addEventListener("AMLReparent", function(beforeNode, pNode, withinParent) { if (!this.$amlLoaded) return; if (!withinParent && this.skinName != pNode.skinName) { //@todo for now, assuming dom garbage collection doesn't leak this.loadAml(); } }); // *** Events *** // this.$down = function() { }; this.$up = function(e) { if (this.type == "radio") this.parentNode.select(this.group, this.value != undefined ? this.value : this.caption); else if (this.type == "check") { this.setProperty("checked", !this.checked); //this.$handlePropSet("checked", !this.checked); } if (this.submenu) { this.$over(null, true); return; } this.parentNode.$hideTree = true; //@todo This statement makes the menu loose focus. if (!this.parentNode.sticky && this.parentNode.localName != "dropdown") this.parentNode.hide();//true not focus?/ this.parentNode.dispatchEvent("itemclick", { value: this.value != undefined ? this.value : this.caption, relatedNode: this, checked: this.checked, selected: this.selected }); //@todo Anim effect here? this.dispatchEvent("click", { xmlContext: (this.parentNode || 0).xmlReference, opener: (this.parentNode || 0).opener, button: e && e.button }); }; this.$click = function() { }; var timer; this.$out = function(e) { if (apf.isChildOf(this.$ext, e.toElement || e.explicitOriginalTarget) || apf.isChildOf(this.$ext, e.srcElement || e.target)) //@todo test FF return; clearTimeout(timer); if (!this.submenu || this.$submenu(true)) { apf.setStyleClass(this.$ext, "", ['hover']); var sel = this.parentNode.$selected; if (sel && sel != this) apf.setStyleClass(sel.$ext, "", ["hover"]); this.parentNode.$selected = null; } }; this.$over = function(e, force) { function selectItem(el) { if (el.parentNode.$selected == el && e) return false; if (el.parentNode.$selected) apf.setStyleClass(el.parentNode.$selected.$ext, "", ["hover"]); apf.setStyleClass(el.$ext, "hover"); el.parentNode.$selected = el; return true; } if (!selectItem(this)) return; var opener = this.parentNode.opener; if (opener && opener.parentNode && opener.parentNode.$showingSubMenu) selectItem(opener); if (!force && (apf.isChildOf(this.$ext, e.toElement || e.explicitOriginalTarget) || apf.isChildOf(this.$ext, e.fromElement || e.target))) //@todo test FF return; var _self = this, ps = this.parentNode.$showingSubMenu; if (ps && ps.name && ps.name == this.submenu) { this.parentNode.$selected = null; this.parentNode.$showingSubMenu = null; _self.$submenu(); return; } clearTimeout(timer); function submenu() { if (ps && ps.visible) { ps.hide(); if (_self.parentNode.$showingSubMenu == ps) _self.parentNode.$showingSubMenu = null; } if (_self.submenu && (!_self.parentNode.opener || _self.parentNode.opener.visible)) _self.$submenu(); } if (force) submenu(); else { timer = $setTimeout(function() { submenu(); timer = null; }, 210); } }; this.$submenu = function(hide, force) { if (!this.submenu) return true; var menu = self[this.submenu] || this.submenu; if (!menu) { return; } if (!hide) { //if (this.parentNode.showingSubMenu == this.submenu) //return; this.parentNode.$showingSubMenu = menu; var pos = apf.getAbsolutePosition(this.$ext, this.parentNode.$ext.offsetParent); menu.display(pos[0] + this.$ext.offsetWidth - 3, pos[1] + 3, true, this, this.parentNode.xmlReference, this.parentNode.$uniqueId); menu.setAttribute("zindex", (this.parentNode.zindex || this.parentNode.$ext.style.zIndex || 1) + 1); } else { if (menu.visible && !force) { return false; } if (this.parentNode.$showingSubMenu == menu) this.parentNode.$showingSubMenu = null; apf.setStyleClass(this.$ext, '', ['hover']); menu.hide(); return true; } }; // *** Init *** // this.$draw = function(isSkinSwitch) { var p = this.parentNode; while (p.$canLeechSkin == "item") p = p.parentNode; this.$ext = this.$getExternal(this.$isLeechingSkin ? "item" //this.type : "main", null, function($ext) { var o = 'var o = apf.lookup(' + this.$uniqueId + '); if (!o || o.disabled) return; o'; $ext.setAttribute("onmouseup", o + '.$up(event)'); $ext.setAttribute("onmousemove", o + '.$over(event)'); $ext.setAttribute("onmouseout", o + '.$out(event)'); $ext.setAttribute("onmousedown", o + '.$down()'); $ext.setAttribute("onclick", o + '.$click()'); }); // getExternal always appends to the end which is wrong when drawing is delayed var next = this.nextSibling && this.nextSibling.$ext; if (next && next.parentNode === this.$ext.parentNode && this.$ext.nextSibling !== next) { next.parentNode.insertBefore(this.$ext, next); } var _self = this; apf.addListener(this.$ext, "mouseover", function(e) { if (!_self.disabled) _self.dispatchEvent("mouseover", { htmlEvent: e }); }); apf.addListener(this.$ext, "mouseout", function(e) { if (!_self.disabled) _self.dispatchEvent("mouseout", { htmlEvent: e }); }); /*p.$getNewContext("item"); var elItem = p.$getLayoutNode("item");*/ //@todo if not elItem try using own skin //this.$ext = apf.insertHtmlNode(elItem, this.parentNode.$container); this.$caption = this.$getLayoutNode("item", "caption", this.$ext); this.$icon = this.$getLayoutNode("item", "icon", this.$ext); this.$hotkey = this.$getLayoutNode("item", "hotkey", this.$ext); if (!isSkinSwitch && this.nextSibling && this.nextSibling.$ext && this.nextSibling.$ext.parentNode == this.$ext.parentNode) { this.$ext.parentNode.insertBefore(this.$ext, this.nextSibling.$ext); } }; /* * @private */ this.addEventListener("DOMNodeInsertedIntoDocument", function(e) { //var x = this.$aml; //this.skinName = this.parentNode.skinName; var isSkinSwitch = this.$ext ? true : false; if (isSkinSwitch) { if (typeof this.checked !== "undefined") this.$handlePropSet("checked", this.checked); else if (typeof this.selected !== "undefined") this.$handlePropSet("selected", this.selected); if (this.disabled) this.$handlePropSet("disabled", this.disabled); if (this.caption) this.$handlePropSet("caption", this.caption); } }); }).call(apf.item.prototype = new apf.Presentation()); apf.aml.setElement("item", apf.item); }; });