From 8fdbb909b47ee0f5043e9a5e4549fc2a31875b29 Mon Sep 17 00:00:00 2001 From: "chris.watts90@outlook.com" Date: Sun, 9 Apr 2017 22:02:08 +0100 Subject: [PATCH] add knockout contextmenu plugin code to support data bound context menus #29 --- .../www/css/knockout.contextmenu.css | 59 +++ .../www/js/knockout.contextmenu.js | 364 ++++++++++++++++++ 2 files changed, 423 insertions(+) create mode 100644 DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/css/knockout.contextmenu.css create mode 100644 DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/js/knockout.contextmenu.js diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/css/knockout.contextmenu.css b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/css/knockout.contextmenu.css new file mode 100644 index 0000000..bedb129 --- /dev/null +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/css/knockout.contextmenu.css @@ -0,0 +1,59 @@ +/* knockout.contextmenu v1.0.0 + Nicolás Escalante - nlante@gmail.com + Issues: https://github.com/nescalante/knockout.contextmenu/issues + License: MIT */ + +.context-menu { + position: absolute; + padding: 0; + margin: 0; + z-index: 1030; + background-color: #ffffff; +} +.context-menu ul { + line-height: 1.6; + padding: 0; + margin: 0; + border: 1px solid #dddddd; + box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.3); +} +.context-menu ul > li { + padding: 4px 20px; + margin: 0; + z-index: 1031; + list-style-type: none; + cursor: pointer; + white-space: nowrap; + color: #333333; +} +.context-menu ul > li:hover { + background-color: #eeeeee; +} +.context-menu ul > li.disabled, +.context-menu ul > li.disabled a { + color: #666666; + cursor: default; +} +.context-menu ul > li.checked:before { + position: absolute; + content: "\2713"; + left: 7px; +} +.context-menu ul > li.with-url { + padding: 0; +} +.context-menu ul > li.with-url a { + display: block; + padding: 4px 20px; + text-decoration: none; + color: #333333; +} +.context-menu ul > li.separator { + margin: 4px 0; + padding: 0; + border-bottom: 1px solid #cccccc; + cursor: default; +} +.context-menu ul > li.separator:hover { + background-color: #ffffff; +} \ No newline at end of file diff --git a/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/js/knockout.contextmenu.js b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/js/knockout.contextmenu.js new file mode 100644 index 0000000..ca91eff --- /dev/null +++ b/DataCenter_Windows/WindowsDataCenter/WindowsDataCenter/www/js/knockout.contextmenu.js @@ -0,0 +1,364 @@ +/* knockout.contextmenu v1.0.0 + Nicolás Escalante - nlante@gmail.com + Issues: https://github.com/nescalante/knockout.contextmenu/issues + License: MIT */ + +(function (undefined) { + 'use strict'; + + // client + if (typeof ko !== undefined + '') { + bindContextMenu(ko); + } + + // node + if (typeof module !== undefined + '' && module.exports && typeof require !== undefined + '') { + bindContextMenu(require('knockout')); + } + + function bindContextMenu(ko) { + var currentMenu; + var elementMapping = []; + var utils = ko.utils; + var registerEvent = utils.registerEventHandler; + var isObservable = ko.isObservable; + + registerEvent(document, 'click', function (event) { + var button = event.which || event.button; + if (!event.defaultPrevented && button < 2) { + hideCurrentMenu(); + } + }); + + utils.contextMenu = { + getMenuFor: function (element, event) { + var result = getMapping(element); + + if (result) { + return result.get(event); + } + }, + + openMenuFor: function (element, event) { + var result = getMapping(element); + + if (result) { + return result.open(event); + } + }, + + }; + + ko.bindingHandlers.contextMenu = { + init: function (element, valueAccessor, allBindingsAccessor, viewModel) { + var eventsToHandle = valueAccessor() || {}; + var allBindings = allBindingsAccessor(); + var defaultClass = allBindings.contextMenuClass || 'context-menu'; + var activeElement; + + // bind on click? bind on context click? + if (allBindings.bindMenuOnClick) { + registerEvent(element, 'click', openMenu); + } + + if (allBindings.bindMenuOnContextMenu === undefined || allBindings.bindMenuOnContextMenu) { + registerEvent(element, 'contextmenu', openMenu); + } + + elementMapping.push({ + element: element, + get: function () { + return activeElement; + }, + + open: openMenu, + hide: function () { + if (activeElement) { + activeElement.hide(); + } + }, + }); + + function mouseX(evt) { + if (evt.pageX) { + return evt.pageX; + } else if (evt.clientX) { + return evt.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft); + } else { + return null; + } + } + + function mouseY(evt) { + if (evt.pageY) { + return evt.pageY; + } else if (evt.clientY) { + return evt.clientY + (document.documentElement.scrollTop || document.body.scrollTop); + } else { + return null; + } + } + + function openMenu(event) { + var menuElement; + + activeElement = getMenu(event); + menuElement = activeElement.element; + + hideCurrentMenu(); + + if (menuElement) { + // make visibility hidden, then add to DOM so that we can get the height/width of the menu + menuElement.style.visibility = 'hidden'; + (document.body || document).appendChild(menuElement); + + // set location + if (event) { + var bottomOfViewport = window.innerHeight + window.pageYOffset; + var rightOfViewport = window.innerWidth + window.pageXOffset; + + if (mouseY(event) + menuElement.offsetHeight > bottomOfViewport) { + menuElement.style.top = 1 * (bottomOfViewport - menuElement.offsetHeight - 10) + 'px'; + } else { + menuElement.style.top = mouseY(event) + 'px'; + } + + if (mouseX(event) + menuElement.offsetWidth > rightOfViewport) { + menuElement.style.left = 1 * (rightOfViewport - menuElement.offsetWidth - 10) + 'px'; + } else { + menuElement.style.left = mouseX(event) + 'px'; + } + + event.preventDefault(); + event.stopPropagation(); + } else { + menuElement.style.top = (element.offsetTop + element.offsetHeight) + 'px'; + menuElement.style.left = (element.offsetLeft + element.offsetWidth) + 'px'; + } + + // now set to visible + menuElement.style.visibility = ''; + } + + // replace current menu with the recently created + currentMenu = menuElement; + + return activeElement; + } + + function getMenu(event) { + var menu; + var hasChecks = false; + var elements = []; + var actions = []; + var items = []; + var props = Object.keys( + ko.isObservable(eventsToHandle) ? + eventsToHandle() : + eventsToHandle + ); + + props.forEach(function (eventNameOutsideClosure) { + pushItem(eventNameOutsideClosure); + }); + + if (elements.length) { + menu = document.createElement('div'); + menu.className = defaultClass; + + // you may need padding to menus that has checks + menu.innerHTML = ''; + + // map items to actions + elements.forEach(function (item, index) { + registerEvent(menu.children[0].children[index], 'click', function (event) { + var result = actions[index](viewModel, event); + + if (!result && event) { + event.preventDefault(); + } + }); + }); + } + + return { + element: menu, + items: items, + open: openMenu, + hide: function () { + if (menu && menu.parentNode) { + menu.parentNode.removeChild(menu); + } + + currentMenu = null; + }, + }; + + function pushItem(eventName) { + var item = getMenuProperties(eventName); + var classes = []; + var id = ''; + var liHtml; + + if (item.isVisible) { + hasChecks = hasChecks || item.isBoolean; + + if (item.id) { + id = item.id; + } + + // set css classes + if (item.isChecked) { + classes.push('checked'); + } + + if (item.isDisabled) { + classes.push('disabled'); + } + + if (item.isSeparator) { + classes.push('separator'); + } + + if (item.url) { + classes.push('with-url'); + } + + liHtml = '
  • ' + + item.html + + '
  • '; + + elements.push(liHtml); + actions.push(item.action); + } + + items.push(item); + } + } + + function getMenuProperties(eventName) { + var text = ''; + var html = ''; + var currentEvent = ko.isObservable(eventsToHandle) ? + eventsToHandle()[eventName] : + eventsToHandle[eventName]; + var item = currentEvent || {}; + var id = item.id; + var url = (isObservable(item.url) ? item.url() : item.url); + var isVisible = item.visible === undefined || item.visible === null || + (isObservable(item.visible) && item.visible()) || + (!isObservable(item.visible) && !!item.visible); + var isChecked = false; + var isEnabled = !item.disabled || + (isObservable(item.disabled) && !item.disabled()) || + (isObservable(item.enabled) && item.enabled()) || + (!isObservable(item.enabled) && !!item.enabled); + var isBoolean = false; + var isDisabled = !isEnabled; + var isSeparator = !!currentEvent.separator; + + if (!isSeparator) { + text = isObservable(item.text) ? item.text() : item.text; + + if (!text) { + text = eventName; + } + + if (url) { + html = '' + text + ''; + } else { + html = text; + } + } + + if ((isObservable(item) && typeof item() === 'boolean') || + (isObservable(item.action) && typeof item.action() === 'boolean')) { + isBoolean = true; + + if ((item.action && item.action()) || + (typeof item === 'function' && item())) { + isChecked = true; + } + } + + return { + html: html, + text: text, + url: url, + id: id, + isVisible: isVisible, + isChecked: isChecked, + isEnabled: isEnabled, + isDisabled: isDisabled, + isBoolean: isBoolean, + isSeparator: isSeparator, + action: action, + }; + + function action(viewModel, event) { + var error = eventName + ' option must have an action or an url.'; + + if (isDisabled) { + return false; + } + + // check if option is a boolean + if (isObservable(item) && typeof item() === 'boolean') { + item(!item()); + } + + // is an object? well, lets check it properties + else if (typeof item === 'object') { + // check if has an action or if its a separator + if (!item.action && !url && !isSeparator) { + throw error; + } + + // evaluate action + else if (item.action) { + if (isObservable(item.action) && typeof item.action() === 'boolean') { + item.action(!item.action()); + } else { + item.action(viewModel, event); + } + } + } + + // its not an observable, should be a function + else if (typeof item === 'function') { + item(viewModel, event); + } + + // nothing to do with this + else { + throw error; + } + + return true; + } + } + }, + }; + + function hideCurrentMenu() { + if (currentMenu && currentMenu.parentNode) { + currentMenu.parentNode.removeChild(currentMenu); + } + + currentMenu = null; + } + + function getMapping(element) { + var i = 0; + + for (; i < elementMapping.length; i++) { + if (elementMapping[i].element === element) { + return elementMapping[i]; + } + } + } + } +})(); \ No newline at end of file