From b3e3c266113b2159b11579086442c8d6a388181d Mon Sep 17 00:00:00 2001 From: 3nda Date: Fri, 7 Jul 2023 17:04:38 +0200 Subject: [PATCH 01/16] dos2unix text/* --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e65d516 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# https://help.github.com/articles/dealing-with-line-endings/ +* text eol=lf From 8d218b39b3aabead286e84e8756a782a629f8ec8 Mon Sep 17 00:00:00 2001 From: 3nda Date: Fri, 7 Jul 2023 17:47:42 +0200 Subject: [PATCH 02/16] refactor: RadialMenu.js, function.prototype -> class --- js/RadialMenu.js | 1235 +++++++++++++++++++++++++--------------------- js/main.js | 8 +- 2 files changed, 666 insertions(+), 577 deletions(-) diff --git a/js/RadialMenu.js b/js/RadialMenu.js index a4ed8a7..b0bbda8 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -1,577 +1,664 @@ 'use strict'; -var DEFAULT_SIZE = 100; -var MIN_SECTORS = 6; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -function RadialMenu(params) { - var self = this; - - self.parent = params.parent || []; - - self.size = params.size || DEFAULT_SIZE; - self.onClick = params.onClick || null; - self.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; - - self.radius = 50; - self.innerRadius = self.radius * 0.4; - self.sectorSpace = self.radius * 0.06; - self.sectorCount = Math.max(self.menuItems.length, MIN_SECTORS); - self.closeOnClick = params.closeOnClick !== undefined ? !!params.closeOnClick : false; - - self.scale = 1; - self.holder = null; - self.parentMenu = []; - self.parentItems = []; - self.levelItems = null; - - self.createHolder(); - self.addIconSymbols(); - - self.currentMenu = null; - document.addEventListener('wheel', self.onMouseWheel.bind(self)); - document.addEventListener('keydown', self.onKeyDown.bind(self)); +/** + * https://github.com/axln/radial-menu-js + * https://github.com/j3nda/radial-menu-js + * About + * Radial menu in pure JavaScript, HTML and SVG + * License: MIT + */ +class RadialMenu +{ + static defaultValues = { + size: 100, // aka DEFAULT_SIZE + minSectors: 6, // aka MIN_SECTORS + radius: { + value: 50, + multiInnerRadius: 0.4, // multiplication for default.radius.value [or params.radius] + multiSectorSpace: 0.06 // multiplication for default.radius.value [or params.radius] + }, + radiusInner: 0.4, + radiusSectorSpace: 0.06, + } + + constructor(params, defaultValues = RadialMenu.defaultValues) + { + this._defaultValues = defaultValues; + this.parent = params.parent || []; + + this.size = params.size || defaultValues.size; + this.onClick = params.onClick || null; + this.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; + + this.radius = params.radius ? params.radius : defaultValues.radius.value; + this.innerRadius = params.innerRadius ? params.innerRadius : this.radius * defaultValues.radius.multiInnerRadius; + this.sectorSpace = params.sectorSpace ? params.sectorSpace : this.radius * defaultValues.radius.multiSectorSpace; + this.sectorCount = Math.max(this.menuItems.length, defaultValues.minSectors); + this.closeOnClick = params.closeOnClick !== undefined ? !!params.closeOnClick : false; + + this.scale = 1;//TODO:?ma smysl mit jiny scale? + this.holder = null; + this.parentMenu = []; + this.parentItems = []; + this.levelItems = null; + + this.createHolder(); + this.addIconSymbols(); + + this.currentMenu = null; + document.addEventListener('wheel', this.onMouseWheel.bind(this));//TODO:?enable/disable? + document.addEventListener('keydown', this.onKeyDown.bind(this));//TODO:?enable/disable? + } + + //TODO:?isOpen(){ return this.currentMenu == null; } + + open() + { + if (!this.currentMenu) + { + this.currentMenu = this.createMenu('menu inner', this.menuItems); + this.holder.appendChild(this.currentMenu); + + // wait DOM commands to apply and then set class to allow transition to take effect + const THIS = this; + this.nextTick(function() + { + THIS.currentMenu.setAttribute('class', 'menu'); + }); + } + } + + close() + { + if (this.currentMenu) + { + let parentMenu; + while (parentMenu = this.parentMenu.pop()) + { + parentMenu.remove(); + } + this.parentItems = []; + + const THIS = this; + this.setClassAndWaitForTransition(this.currentMenu, 'menu inner') + .then(function() + { + THIS.currentMenu.remove(); + THIS.currentMenu = null; + }) + ; + } + } + + getParentMenu() + { + if (this.parentMenu.length > 0) + { + return this.parentMenu[this.parentMenu.length - 1]; + } + return null; + } + + createHolder() + { + this.holder = document.createElement('div'); + this.holder.className = 'menuHolder'; + this.holder.style.width = this.size + 'px'; + this.holder.style.height = this.size + 'px'; + + this.parent.appendChild(this.holder); + } + + showNestedMenu(item) + { + this.parentMenu.push(this.currentMenu); + this.parentItems.push(this.levelItems); + this.currentMenu = this.createMenu('menu inner', item.items, true); + this.holder.appendChild(this.currentMenu); + + // wait DOM commands to apply and then set class to allow transition to take effect + const THIS = this; + this.nextTick(function() + { + THIS.getParentMenu().setAttribute('class', 'menu outer'); + THIS.currentMenu.setAttribute('class', 'menu'); + }); + } + + returnToParentMenu() + { + this.getParentMenu().setAttribute('class', 'menu'); + + const THIS = this; + this.setClassAndWaitForTransition(this.currentMenu, 'menu inner') + .then(function() + { + THIS.currentMenu.remove(); + THIS.currentMenu = THIS.parentMenu.pop(); + THIS.levelItems = THIS.parentItems.pop(); + }) + ; + } + + handleClick() + { + var selectedIndex = this.getSelectedIndex(); + if (selectedIndex >= 0) + { + var item = this.levelItems[selectedIndex]; + if (item.items) + { + this.showNestedMenu(item); + } + else + if (this.onClick)//TODO:???? + { + this.onClick(item); + if (this.closeOnClick) + { + this.close(); + } + } + } + } + + handleCenterClick() + { + if (this.parentItems.length > 0) + { + this.returnToParentMenu(); + } + else + { + this.close(); + } + } + + createCenter(svg, title, icon, size) + { + size = size || 8;//TODO:?default value? + var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('class', 'center'); + + var centerCircle = this.createCircle(0, 0, this.innerRadius - this.sectorSpace / 3); + g.appendChild(centerCircle); + if (text)//TODO:?odkud to bere?text neni param!? + { + var text = this.createText(0, 0, title); + g.appendChild(text); + } + + if (icon) + { + var use = this.createUseTag(0, 0, icon); + use.setAttribute('width', size); + use.setAttribute('height', size); + use.setAttribute('transform', 'translate(-' + this.numberToString(size / 2) + ',-' + this.numberToString(size / 2) + ')'); + g.appendChild(use); + } + svg.appendChild(g); + } + + getIndexOffset() + { + if (this.levelItems.length < this.sectorCount) + { + switch (this.levelItems.length) + { + case 1: + case 2: + case 3: + return -2; + + default: + return -1; + } + } + return -1; + } + + createMenu(classValue, levelItems, nested) + { + const THIS = this; + this.levelItems = levelItems; + + this.sectorCount = Math.max(this.levelItems.length, this._defaultValues.minSectors); + this.scale = this.calcScale(); + + var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('class', classValue); + svg.setAttribute('viewBox', '-50 -50 100 100'); + svg.setAttribute('width', this.size); + svg.setAttribute('height', this.size); + + var angleStep = 360 / this.sectorCount; + var angleShift = angleStep / 2 + 270; + + var indexOffset = this.getIndexOffset(); + + for (var i = 0; i < this.sectorCount; ++i) + { + var startAngle = angleShift + angleStep * i; + var endAngle = angleShift + angleStep * (i + 1); + + var itemIndex = this.resolveLoopIndex(this.sectorCount - i + indexOffset, this.sectorCount); + var item = null; + if (itemIndex >= 0 && itemIndex < this.levelItems.length) + { + item = this.levelItems[itemIndex]; + } + this.appendSectorPath(startAngle, endAngle, svg, item, itemIndex); + } + + if (nested) + { + this.createCenter(svg, 'Close', '#return', 8); //TODO:??magicNumber?? + } + else + { + this.createCenter(svg, 'Close', '#close', 7);//TODO:??magicNumber?? + } + + svg.addEventListener('mousedown', function(event) + { + var className = event.target.parentNode.getAttribute('class').split(' ')[0]; + switch (className) + { + case 'sector': + { + var index = parseInt(event.target.parentNode.getAttribute('data-index')); + if (!isNaN(index)) + { + THIS.setSelectedIndex(index); + } + break; + } + default: + break; + } + }); + svg.addEventListener('click', function(event) + { + var className = event.target.parentNode.getAttribute('class').split(' ')[0]; + switch (className) + { + case 'sector': + { + THIS.handleClick(); + break; + } + case 'center': + { + THIS.handleCenterClick(); + break; + } + default: + break; + } + }); + return svg; + } + + selectDelta(indexDelta) + { + var selectedIndex = this.getSelectedIndex(); + if (selectedIndex < 0) + { + selectedIndex = 0; + } + selectedIndex += indexDelta; + if (selectedIndex < 0) + { + selectedIndex = this.levelItems.length + selectedIndex; + } + else + if (selectedIndex >= this.levelItems.length) + { + selectedIndex -= this.levelItems.length; + } + this.setSelectedIndex(selectedIndex); + }; + + onKeyDown(event) + { + if (!this.currentMenu) + { + return; + } + switch (event.key) + { + case 'Escape': + case 'Backspace': + { + this.handleCenterClick(); + event.preventDefault(); + break; + } + case 'Enter': + { + this.handleClick(); + event.preventDefault(); + break; + } + case 'ArrowRight': + case 'ArrowUp': + { + this.selectDelta(1); + event.preventDefault(); + break; + } + case 'ArrowLeft': + case 'ArrowDown': + { + this.selectDelta(-1); + event.preventDefault(); + break; + } + default: + break; + } + } + + onMouseWheel(event) + { + if (!this.currentMenu) + { + return; + } + var delta = -event.deltaY; + if (delta > 0) + { + this.selectDelta(1); + } + else + { + this.selectDelta(-1); + } + } + + getSelectedNode() + { + var items = this.currentMenu.getElementsByClassName('selected'); + if (items.length > 0) + { + return items[0]; + } + return null; + } + + getSelectedIndex() + { + var selectedNode = this.getSelectedNode(); + if (selectedNode) + { + return parseInt(selectedNode.getAttribute('data-index')); + } + return -1; + }; + + setSelectedIndex(index) + { + if (index >= 0 && index < this.levelItems.length) + { + var items = this.currentMenu.querySelectorAll('g[data-index="' + index + '"]'); + if (items.length > 0) + { + var itemToSelect = items[0]; + var selectedNode = this.getSelectedNode(); + if (selectedNode) + { + selectedNode.setAttribute('class', 'sector'); + } + itemToSelect.setAttribute('class', 'sector selected'); + } + } + } + + createUseTag(x, y, link) + { + var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + + use.setAttribute('x', this.numberToString(x)); + use.setAttribute('y', this.numberToString(y)); + use.setAttribute('width', '10'); + use.setAttribute('height', '10'); + use.setAttribute('fill', 'white'); + use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', link); + + return use; + } + + appendSectorPath(startAngleDeg, endAngleDeg, svg, item, index) + { + var centerPoint = this.getSectorCenter(startAngleDeg, endAngleDeg); + var translate = { + x: this.numberToString((1 - this.scale) * centerPoint.x), + y: this.numberToString((1 - this.scale) * centerPoint.y) + }; + + var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('transform', 'translate(' + translate.x + ' ,' + translate.y + ') scale(' + this.scale + ')'); + + var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', this.createSectorCmds(startAngleDeg, endAngleDeg)); + g.appendChild(path); + + if (item) + { + g.setAttribute('class', 'sector'); + if (index == 0) + { + g.setAttribute('class', 'sector selected'); + } + g.setAttribute('data-id', item.id); + g.setAttribute('data-index', index); + + if (item.title) + { + var text = this.createText(centerPoint.x, centerPoint.y, item.title); + if (item.icon) + { + text.setAttribute('transform', 'translate(0,8)'); + } + else + { + text.setAttribute('transform', 'translate(0,2)'); + } + g.appendChild(text); + } + + if (item.icon) + { + var use = this.createUseTag(centerPoint.x, centerPoint.y, item.icon); + if (item.title) + { + use.setAttribute('transform', 'translate(-5,-8)'); + } + else + { + use.setAttribute('transform', 'translate(-5,-5)'); + } + g.appendChild(use); + } + + } + else + { + g.setAttribute('class', 'dummy'); + } + svg.appendChild(g); + }; + + createSectorCmds(startAngleDeg, endAngleDeg) + { + var initPoint = this.getDegreePos(startAngleDeg, this.radius); + var path = 'M' + this.pointToString(initPoint); + + var radiusAfterScale = this.radius * (1 / this.scale); + path += 'A' + radiusAfterScale + ' ' + radiusAfterScale + ' 0 0 0' + this.pointToString(this.getDegreePos(endAngleDeg, this.radius)); + path += 'L' + this.pointToString(this.getDegreePos(endAngleDeg, this.innerRadius)); + + var radiusDiff = this.radius - this.innerRadius; + var radiusDelta = (radiusDiff - (radiusDiff * this.scale)) / 2; + var innerRadius = (this.innerRadius + radiusDelta) * (1 / this.scale); + path += 'A' + innerRadius + ' ' + innerRadius + ' 0 0 1 ' + this.pointToString(this.getDegreePos(startAngleDeg, this.innerRadius)); + path += 'Z'; + + return path; + } + + createText(x, y, title) + { + var text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('x', this.numberToString(x)); + text.setAttribute('y', this.numberToString(y)); + text.setAttribute('font-size', '38%'); + text.innerHTML = title; + + return text; + } + + createCircle(x, y, r) + { + var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + + circle.setAttribute('cx', this.numberToString(x)); + circle.setAttribute('cy', this.numberToString(y)); + circle.setAttribute('r', r); + + return circle; + }; + + calcScale() + { + var totalSpace = this.sectorSpace * this.sectorCount; + var circleLength = Math.PI * 2 * this.radius; + var radiusDelta = this.radius - (circleLength - totalSpace) / (Math.PI * 2); + + return (this.radius - radiusDelta) / this.radius; + } + + getSectorCenter(startAngleDeg, endAngleDeg) + { + return this.getDegreePos( + (startAngleDeg + endAngleDeg) / 2, + this.innerRadius + (this.radius - this.innerRadius) / 2 + ); + } + + addIconSymbols() + { + var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('class', 'icons'); + + // return + var returnSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); + returnSymbol.setAttribute('id', 'return'); + returnSymbol.setAttribute('viewBox', '0 0 489.394 489.394'); + + var returnPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + returnPath.setAttribute('d', "M375.789,92.867H166.864l17.507-42.795c3.724-9.132,1-19.574-6.691-25.744c-7.701-6.166-18.538-6.508-26.639-0.879" + + "L9.574,121.71c-6.197,4.304-9.795,11.457-9.563,18.995c0.231,7.533,4.261,14.446,10.71,18.359l147.925,89.823" + + "c8.417,5.108,19.18,4.093,26.481-2.499c7.312-6.591,9.427-17.312,5.219-26.202l-19.443-41.132h204.886" + + "c15.119,0,27.418,12.536,27.418,27.654v149.852c0,15.118-12.299,27.19-27.418,27.19h-226.74c-20.226,0-36.623,16.396-36.623,36.622" + + "v12.942c0,20.228,16.397,36.624,36.623,36.624h226.74c62.642,0,113.604-50.732,113.604-113.379V206.709" + + "C489.395,144.062,438.431,92.867,375.789,92.867z"); + + returnSymbol.appendChild(returnPath); + svg.appendChild(returnSymbol); + + var closeSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); + closeSymbol.setAttribute('id', 'close'); + closeSymbol.setAttribute('viewBox', '0 0 41.756 41.756'); + + var closePath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + closePath.setAttribute('d', "M27.948,20.878L40.291,8.536c1.953-1.953,1.953-5.119,0-7.071c-1.951-1.952-5.119-1.952-7.07,0L20.878,13.809L8.535,1.465" + + "c-1.951-1.952-5.119-1.952-7.07,0c-1.953,1.953-1.953,5.119,0,7.071l12.342,12.342L1.465,33.22c-1.953,1.953-1.953,5.119,0,7.071" + + "C2.44,41.268,3.721,41.755,5,41.755c1.278,0,2.56-0.487,3.535-1.464l12.343-12.342l12.343,12.343" + + "c0.976,0.977,2.256,1.464,3.535,1.464s2.56-0.487,3.535-1.464c1.953-1.953,1.953-5.119,0-7.071L27.948,20.878z"); + closeSymbol.appendChild(closePath); + + svg.appendChild(closeSymbol); + this.holder.appendChild(svg); + } + + getDegreePos(angleDeg, length) + { + return { + x: Math.sin(this.degToRad(angleDeg)) * length, + y: Math.cos(this.degToRad(angleDeg)) * length + }; + } + + pointToString(point) + { + return this.numberToString(point.x) + ' ' + this.numberToString(point.y); + } + + numberToString(n) + { + if (Number.isInteger(n)) + { + return n.toString(); + } + else + if (n) + { + var r = (+n).toFixed(5); + if (r.match(/\./)) + { + r = r.replace(/\.?0+$/, ''); + } + return r; + } + // TODO:?if is NaN? what to do? + } + + resolveLoopIndex(index, length) + { + if (index < 0) + { + index = length + index; + } + + if (index >= length) + { + index = index - length; + } + + if (index < length) + { + return index; + } + return null; + } + + degToRad(deg) + { + return deg * (Math.PI / 180); + } + + setClassAndWaitForTransition(node, newClass) + { + return new Promise(function(resolve) + { + function handler(event) + { + if (event.target == node && event.propertyName == 'visibility') + { + node.removeEventListener('transitionend', handler); + resolve(); + } + } + node.addEventListener('transitionend', handler); + node.setAttribute('class', newClass); + }); + } + + nextTick(fn) + { + setTimeout(fn, 10); + } } - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.open = function () { - var self = this; - if (!self.currentMenu) { - self.currentMenu = self.createMenu('menu inner', self.menuItems); - self.holder.appendChild(self.currentMenu); - - // wait DOM commands to apply and then set class to allow transition to take effect - RadialMenu.nextTick(function () { - self.currentMenu.setAttribute('class', 'menu'); - }); - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.close = function () { - var self = this; - - if (self.currentMenu) { - var parentMenu; - while (parentMenu = self.parentMenu.pop()) { - parentMenu.remove(); - } - self.parentItems = []; - - RadialMenu.setClassAndWaitForTransition(self.currentMenu, 'menu inner').then(function () { - self.currentMenu.remove(); - self.currentMenu = null; - }); - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.getParentMenu = function () { - var self = this; - if (self.parentMenu.length > 0) { - return self.parentMenu[self.parentMenu.length - 1]; - } else { - return null; - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.createHolder = function () { - var self = this; - - self.holder = document.createElement('div'); - self.holder.className = 'menuHolder'; - self.holder.style.width = self.size + 'px'; - self.holder.style.height = self.size + 'px'; - - self.parent.appendChild(self.holder); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.showNestedMenu = function (item) { - var self = this; - self.parentMenu.push(self.currentMenu); - self.parentItems.push(self.levelItems); - self.currentMenu = self.createMenu('menu inner', item.items, true); - self.holder.appendChild(self.currentMenu); - - // wait DOM commands to apply and then set class to allow transition to take effect - RadialMenu.nextTick(function () { - self.getParentMenu().setAttribute('class', 'menu outer'); - self.currentMenu.setAttribute('class', 'menu'); - }); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.returnToParentMenu = function () { - var self = this; - self.getParentMenu().setAttribute('class', 'menu'); - RadialMenu.setClassAndWaitForTransition(self.currentMenu, 'menu inner').then(function () { - self.currentMenu.remove(); - self.currentMenu = self.parentMenu.pop(); - self.levelItems = self.parentItems.pop(); - }); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.handleClick = function () { - var self = this; - - var selectedIndex = self.getSelectedIndex(); - if (selectedIndex >= 0) { - var item = self.levelItems[selectedIndex]; - if (item.items) { - self.showNestedMenu(item); - } else { - if (self.onClick) { - self.onClick(item); - if (self.closeOnClick) { - self.close(); - } - } - } - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.handleCenterClick = function () { - var self = this; - if (self.parentItems.length > 0) { - self.returnToParentMenu(); - } else { - self.close(); - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.createCenter = function (svg, title, icon, size) { - var self = this; - size = size || 8; - var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - g.setAttribute('class', 'center'); - - var centerCircle = self.createCircle(0, 0, self.innerRadius - self.sectorSpace / 3); - g.appendChild(centerCircle); - if (text) { - var text = self.createText(0,0, title); - g.appendChild(text); - } - - if (icon) { - var use = self.createUseTag(0,0, icon); - use.setAttribute('width', size); - use.setAttribute('height', size); - use.setAttribute('transform', 'translate(-' + RadialMenu.numberToString(size / 2) + ',-' + RadialMenu.numberToString(size / 2) + ')'); - g.appendChild(use); - } - - svg.appendChild(g); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.getIndexOffset = function () { - var self = this; - if (self.levelItems.length < self.sectorCount) { - switch (self.levelItems.length) { - case 1: - return -2; - case 2: - return -2; - case 3: - return -2; - default: - return -1; - } - } else { - return -1; - } - -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.createMenu = function (classValue, levelItems, nested) { - var self = this; - - self.levelItems = levelItems; - - self.sectorCount = Math.max(self.levelItems.length, MIN_SECTORS); - self.scale = self.calcScale(); - - var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('class', classValue); - svg.setAttribute('viewBox', '-50 -50 100 100'); - svg.setAttribute('width', self.size); - svg.setAttribute('height', self.size); - - var angleStep = 360 / self.sectorCount; - var angleShift = angleStep / 2 + 270; - - var indexOffset = self.getIndexOffset(); - - for (var i = 0; i < self.sectorCount; ++i) { - var startAngle = angleShift + angleStep * i; - var endAngle = angleShift + angleStep * (i + 1); - - var itemIndex = RadialMenu.resolveLoopIndex(self.sectorCount - i + indexOffset, self.sectorCount); - var item; - if (itemIndex >= 0 && itemIndex < self.levelItems.length) { - item = self.levelItems[itemIndex]; - } else { - item = null; - } - - self.appendSectorPath(startAngle, endAngle, svg, item, itemIndex); - } - - if (nested) { - self.createCenter(svg, 'Close', '#return', 8); - } else { - self.createCenter(svg, 'Close', '#close', 7); - } - - svg.addEventListener('mousedown', function (event) { - var className = event.target.parentNode.getAttribute('class').split(' ')[0]; - switch (className) { - case 'sector': - var index = parseInt(event.target.parentNode.getAttribute('data-index')); - if (!isNaN(index)) { - self.setSelectedIndex(index); - } - break; - default: - } - }); - - svg.addEventListener('click', function (event) { - var className = event.target.parentNode.getAttribute('class').split(' ')[0]; - switch (className) { - case 'sector': - self.handleClick(); - break; - case 'center': - self.handleCenterClick(); - break; - default: - } - }); - return svg; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.selectDelta = function (indexDelta) { - var self = this; - var selectedIndex = self.getSelectedIndex(); - if (selectedIndex < 0) { - selectedIndex = 0; - } - selectedIndex += indexDelta; - - if (selectedIndex < 0) { - selectedIndex = self.levelItems.length + selectedIndex; - } else if (selectedIndex >= self.levelItems.length) { - selectedIndex -= self.levelItems.length; - } - self.setSelectedIndex(selectedIndex); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.onKeyDown = function (event) { - var self = this; - if (self.currentMenu) { - switch (event.key) { - case 'Escape': - case 'Backspace': - self.handleCenterClick(); - event.preventDefault(); - break; - case 'Enter': - self.handleClick(); - event.preventDefault(); - break; - case 'ArrowRight': - case 'ArrowUp': - self.selectDelta(1); - event.preventDefault(); - break; - case 'ArrowLeft': - case 'ArrowDown': - self.selectDelta(-1); - event.preventDefault(); - break; - } - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.onMouseWheel = function (event) { - var self = this; - if (self.currentMenu) { - var delta = -event.deltaY; - - if (delta > 0) { - self.selectDelta(1) - } else { - self.selectDelta(-1) - } - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.getSelectedNode = function () { - var self = this; - var items = self.currentMenu.getElementsByClassName('selected'); - if (items.length > 0) { - return items[0]; - } else { - return null; - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.getSelectedIndex = function () { - var self = this; - var selectedNode = self.getSelectedNode(); - if (selectedNode) { - return parseInt(selectedNode.getAttribute('data-index')); - } else { - return -1; - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.setSelectedIndex = function (index) { - var self = this; - if (index >=0 && index < self.levelItems.length) { - var items = self.currentMenu.querySelectorAll('g[data-index="' + index + '"]'); - if (items.length > 0) { - var itemToSelect = items[0]; - var selectedNode = self.getSelectedNode(); - if (selectedNode) { - selectedNode.setAttribute('class', 'sector'); - } - itemToSelect.setAttribute('class', 'sector selected'); - } - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.createUseTag = function (x, y, link) { - var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); - use.setAttribute('x', RadialMenu.numberToString(x)); - use.setAttribute('y', RadialMenu.numberToString(y)); - use.setAttribute('width', '10'); - use.setAttribute('height', '10'); - use.setAttribute('fill', 'white'); - use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', link); - return use; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.appendSectorPath = function (startAngleDeg, endAngleDeg, svg, item, index) { - var self = this; - - var centerPoint = self.getSectorCenter(startAngleDeg, endAngleDeg); - var translate = { - x: RadialMenu.numberToString((1 - self.scale) * centerPoint.x), - y: RadialMenu.numberToString((1 - self.scale) * centerPoint.y) - }; - - var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - g.setAttribute('transform','translate(' +translate.x + ' ,' + translate.y + ') scale(' + self.scale + ')'); - - var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('d', self.createSectorCmds(startAngleDeg, endAngleDeg)); - g.appendChild(path); - - if (item) { - g.setAttribute('class', 'sector'); - if (index == 0) { - g.setAttribute('class', 'sector selected'); - } - g.setAttribute('data-id', item.id); - g.setAttribute('data-index', index); - - if (item.title) { - var text = self.createText(centerPoint.x, centerPoint.y, item.title); - if (item.icon) { - text.setAttribute('transform', 'translate(0,8)'); - } else { - text.setAttribute('transform', 'translate(0,2)'); - } - - g.appendChild(text); - } - - if (item.icon) { - var use = self.createUseTag(centerPoint.x, centerPoint.y, item.icon); - if (item.title) { - use.setAttribute('transform', 'translate(-5,-8)'); - } else { - use.setAttribute('transform', 'translate(-5,-5)'); - } - - g.appendChild(use); - } - - } else { - g.setAttribute('class', 'dummy'); - } - - svg.appendChild(g); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.createSectorCmds = function (startAngleDeg, endAngleDeg) { - var self = this; - - var initPoint = RadialMenu.getDegreePos(startAngleDeg, self.radius); - var path = 'M' + RadialMenu.pointToString(initPoint); - - var radiusAfterScale = self.radius * (1 / self.scale); - path += 'A' + radiusAfterScale + ' ' + radiusAfterScale + ' 0 0 0' + RadialMenu.pointToString(RadialMenu.getDegreePos(endAngleDeg, self.radius)); - path += 'L' + RadialMenu.pointToString(RadialMenu.getDegreePos(endAngleDeg, self.innerRadius)); - - var radiusDiff = self.radius - self.innerRadius; - var radiusDelta = (radiusDiff - (radiusDiff * self.scale)) / 2; - var innerRadius = (self.innerRadius + radiusDelta) * (1 / self.scale); - path += 'A' + innerRadius + ' ' + innerRadius + ' 0 0 1 ' + RadialMenu.pointToString(RadialMenu.getDegreePos(startAngleDeg, self.innerRadius)); - path += 'Z'; - - return path; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.createText = function (x, y, title) { - var self = this; - var text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - text.setAttribute('text-anchor', 'middle'); - text.setAttribute('x', RadialMenu.numberToString(x)); - text.setAttribute('y', RadialMenu.numberToString(y)); - text.setAttribute('font-size', '38%'); - text.innerHTML = title; - return text; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.createCircle = function (x, y, r) { - var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - circle.setAttribute('cx',RadialMenu.numberToString(x)); - circle.setAttribute('cy',RadialMenu.numberToString(y)); - circle.setAttribute('r',r); - return circle; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.calcScale = function () { - var self = this; - var totalSpace = self.sectorSpace * self.sectorCount; - var circleLength = Math.PI * 2 * self.radius; - var radiusDelta = self.radius - (circleLength - totalSpace) / (Math.PI * 2); - return (self.radius - radiusDelta) / self.radius; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.getSectorCenter = function (startAngleDeg, endAngleDeg) { - var self = this; - return RadialMenu.getDegreePos((startAngleDeg + endAngleDeg) / 2, self.innerRadius + (self.radius - self.innerRadius) / 2); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.prototype.addIconSymbols = function () { - var self = this; - var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('class', 'icons'); - - // return - var returnSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); - returnSymbol.setAttribute('id', 'return'); - returnSymbol.setAttribute('viewBox', '0 0 489.394 489.394'); - var returnPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - returnPath.setAttribute('d', "M375.789,92.867H166.864l17.507-42.795c3.724-9.132,1-19.574-6.691-25.744c-7.701-6.166-18.538-6.508-26.639-0.879" + - "L9.574,121.71c-6.197,4.304-9.795,11.457-9.563,18.995c0.231,7.533,4.261,14.446,10.71,18.359l147.925,89.823" + - "c8.417,5.108,19.18,4.093,26.481-2.499c7.312-6.591,9.427-17.312,5.219-26.202l-19.443-41.132h204.886" + - "c15.119,0,27.418,12.536,27.418,27.654v149.852c0,15.118-12.299,27.19-27.418,27.19h-226.74c-20.226,0-36.623,16.396-36.623,36.622" + - "v12.942c0,20.228,16.397,36.624,36.623,36.624h226.74c62.642,0,113.604-50.732,113.604-113.379V206.709" + - "C489.395,144.062,438.431,92.867,375.789,92.867z"); - returnSymbol.appendChild(returnPath); - svg.appendChild(returnSymbol); - - var closeSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); - closeSymbol.setAttribute('id', 'close'); - closeSymbol.setAttribute('viewBox', '0 0 41.756 41.756'); - - var closePath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - closePath.setAttribute('d', "M27.948,20.878L40.291,8.536c1.953-1.953,1.953-5.119,0-7.071c-1.951-1.952-5.119-1.952-7.07,0L20.878,13.809L8.535,1.465" + - "c-1.951-1.952-5.119-1.952-7.07,0c-1.953,1.953-1.953,5.119,0,7.071l12.342,12.342L1.465,33.22c-1.953,1.953-1.953,5.119,0,7.071" + - "C2.44,41.268,3.721,41.755,5,41.755c1.278,0,2.56-0.487,3.535-1.464l12.343-12.342l12.343,12.343" + - "c0.976,0.977,2.256,1.464,3.535,1.464s2.56-0.487,3.535-1.464c1.953-1.953,1.953-5.119,0-7.071L27.948,20.878z"); - closeSymbol.appendChild(closePath); - svg.appendChild(closeSymbol); - - self.holder.appendChild(svg); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.getDegreePos = function (angleDeg, length) { - return { - x: Math.sin(RadialMenu.degToRad(angleDeg)) * length, - y: Math.cos(RadialMenu.degToRad(angleDeg)) * length - }; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.pointToString = function (point) { - return RadialMenu.numberToString(point.x) + ' ' + RadialMenu.numberToString(point.y); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.numberToString = function (n) { - if (Number.isInteger(n)) { - return n.toString(); - } else if (n) { - var r = (+n).toFixed(5); - if (r.match(/\./)) { - r = r.replace(/\.?0+$/, ''); - } - return r; - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.resolveLoopIndex = function (index, length) { - if (index < 0) { - index = length + index; - } - if (index >= length) { - index = index - length; - } - if (index < length) { - return index; - } else { - return null; - } -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.degToRad = function (deg) { - return deg * (Math.PI / 180); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.setClassAndWaitForTransition = function (node, newClass) { - return new Promise(function (resolve) { - function handler(event) { - if (event.target == node && event.propertyName == 'visibility') { - node.removeEventListener('transitionend', handler); - resolve(); - } - } - node.addEventListener('transitionend', handler); - node.setAttribute('class', newClass); - }); -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -RadialMenu.nextTick = function (fn) { - setTimeout(fn, 10); -}; \ No newline at end of file diff --git a/js/main.js b/js/main.js index ab685bd..5693fae 100644 --- a/js/main.js +++ b/js/main.js @@ -99,13 +99,15 @@ var menuItems = [ ]; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -window.onload = function () { +window.onload = function () +{ var svgMenu = new RadialMenu({ parent : document.body, size : 400, closeOnClick: true, menuItems : menuItems, - onClick : function (item) { + onClick : function (item) + { console.log('You have clicked:', item.id, item.title); } }); @@ -119,4 +121,4 @@ window.onload = function () { closeMenu.onclick = function () { svgMenu.close(); }; -}; \ No newline at end of file +}; From 69b6d0ce7eba018eea390ba05c93155d2f5afa7e Mon Sep 17 00:00:00 2001 From: 3nda Date: Fri, 7 Jul 2023 20:36:19 +0200 Subject: [PATCH 03/16] refactor: RadialMenu.js: var vs {let x const}; polishing and few todo... --- js/RadialMenu.js | 223 ++++++++++++++++++++++++++++++----------------- js/main.js | 10 +-- 2 files changed, 147 insertions(+), 86 deletions(-) diff --git a/js/RadialMenu.js b/js/RadialMenu.js index b0bbda8..2d7d00b 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -9,7 +9,7 @@ */ class RadialMenu { - static defaultValues = { + static _defaultValues = { size: 100, // aka DEFAULT_SIZE minSectors: 6, // aka MIN_SECTORS radius: { @@ -19,23 +19,36 @@ class RadialMenu }, radiusInner: 0.4, radiusSectorSpace: 0.06, + closeOnClick: true, + nested: { + icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) + title: true, + //TODO:?it can show (number of nested menu)? + //TODO:?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? + } } - constructor(params, defaultValues = RadialMenu.defaultValues) + constructor(params) { - this._defaultValues = defaultValues; - this.parent = params.parent || []; + const defaultValues = this.defaultValues = RadialMenu._defaultValues; + this.parent = params.parent || [];//TODO: refactor: ?this.attr = this.merge(defaultValues, params);? this.size = params.size || defaultValues.size; - this.onClick = params.onClick || null; + this.onClick = params.onClick || function(item) + { + this.onClickFallback(item); + }; this.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; this.radius = params.radius ? params.radius : defaultValues.radius.value; this.innerRadius = params.innerRadius ? params.innerRadius : this.radius * defaultValues.radius.multiInnerRadius; this.sectorSpace = params.sectorSpace ? params.sectorSpace : this.radius * defaultValues.radius.multiSectorSpace; this.sectorCount = Math.max(this.menuItems.length, defaultValues.minSectors); - this.closeOnClick = params.closeOnClick !== undefined ? !!params.closeOnClick : false; - + this.closeOnClick = params.closeOnClick !== undefined ? !!params.closeOnClick : defaultValues.closeOnClick; + this.nested = this.merge( + defaultValues.nested, + (params.nested ? params.nested : null) + ); this.scale = 1;//TODO:?ma smysl mit jiny scale? this.holder = null; this.parentMenu = []; @@ -50,6 +63,13 @@ class RadialMenu document.addEventListener('keydown', this.onKeyDown.bind(this));//TODO:?enable/disable? } + onClickFallback(item) + { + console.warn('function onClick(item): is not defined by params! default!'); + console.info(this.constructor.name + ".onClick()"); + console.info(item); + } + //TODO:?isOpen(){ return this.currentMenu == null; } open() @@ -90,6 +110,11 @@ class RadialMenu } } + onClick(item) + { + return item; + } + getParentMenu() { if (this.parentMenu.length > 0) @@ -113,7 +138,7 @@ class RadialMenu { this.parentMenu.push(this.currentMenu); this.parentItems.push(this.levelItems); - this.currentMenu = this.createMenu('menu inner', item.items, true); + this.currentMenu = this.createMenu('menu inner', item.items, item); this.holder.appendChild(this.currentMenu); // wait DOM commands to apply and then set class to allow transition to take effect @@ -142,16 +167,16 @@ class RadialMenu handleClick() { - var selectedIndex = this.getSelectedIndex(); + const selectedIndex = this.getSelectedIndex(); if (selectedIndex >= 0) { - var item = this.levelItems[selectedIndex]; + const item = this.levelItems[selectedIndex]; if (item.items) { this.showNestedMenu(item); } else - if (this.onClick)//TODO:???? + if (this.onClick) { this.onClick(item); if (this.closeOnClick) @@ -174,23 +199,29 @@ class RadialMenu } } - createCenter(svg, title, icon, size) + createCenter(svg, title, icon, size, nested = null) { - size = size || 8;//TODO:?default value? - var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + size = size || 8;//TODO:?magicNumber?default value? + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); g.setAttribute('class', 'center'); - var centerCircle = this.createCircle(0, 0, this.innerRadius - this.sectorSpace / 3); + const centerCircle = this.createCircle(0, 0, this.innerRadius - this.sectorSpace / 3); g.appendChild(centerCircle); - if (text)//TODO:?odkud to bere?text neni param!? + + if (nested && this.nested.title) { - var text = this.createText(0, 0, title); + const text = this.createText(0, +size, nested.title); g.appendChild(text); } if (icon) { - var use = this.createUseTag(0, 0, icon); + if (nested && this.nested.icon) + { + icon = (this.nested.icon === true ? nested.icon : this.nested.icon); + } + const use = this.createUseTag(0, 0, icon); use.setAttribute('width', size); use.setAttribute('height', size); use.setAttribute('transform', 'translate(-' + this.numberToString(size / 2) + ',-' + this.numberToString(size / 2) + ')'); @@ -222,27 +253,25 @@ class RadialMenu const THIS = this; this.levelItems = levelItems; - this.sectorCount = Math.max(this.levelItems.length, this._defaultValues.minSectors); + this.sectorCount = Math.max(this.levelItems.length, this.defaultValues.minSectors); this.scale = this.calcScale(); - var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', classValue); svg.setAttribute('viewBox', '-50 -50 100 100'); svg.setAttribute('width', this.size); svg.setAttribute('height', this.size); - var angleStep = 360 / this.sectorCount; - var angleShift = angleStep / 2 + 270; + const angleStep = 360 / this.sectorCount; + const angleShift = angleStep / 2 + 270; + const indexOffset = this.getIndexOffset(); - var indexOffset = this.getIndexOffset(); - - for (var i = 0; i < this.sectorCount; ++i) + for (let i = 0; i < this.sectorCount; ++i) { - var startAngle = angleShift + angleStep * i; - var endAngle = angleShift + angleStep * (i + 1); - - var itemIndex = this.resolveLoopIndex(this.sectorCount - i + indexOffset, this.sectorCount); - var item = null; + const startAngle = angleShift + angleStep * i; + const endAngle = angleShift + angleStep * (i + 1); + const itemIndex = this.resolveLoopIndex(this.sectorCount - i + indexOffset, this.sectorCount); + let item = null; if (itemIndex >= 0 && itemIndex < this.levelItems.length) { item = this.levelItems[itemIndex]; @@ -252,7 +281,7 @@ class RadialMenu if (nested) { - this.createCenter(svg, 'Close', '#return', 8); //TODO:??magicNumber?? + this.createCenter(svg, 'Back', '#return', 8, nested); //TODO:??magicNumber?? } else { @@ -261,12 +290,12 @@ class RadialMenu svg.addEventListener('mousedown', function(event) { - var className = event.target.parentNode.getAttribute('class').split(' ')[0]; + const className = event.target.parentNode.getAttribute('class').split(' ')[0]; switch (className) { case 'sector': { - var index = parseInt(event.target.parentNode.getAttribute('data-index')); + const index = parseInt(event.target.parentNode.getAttribute('data-index')); if (!isNaN(index)) { THIS.setSelectedIndex(index); @@ -279,7 +308,7 @@ class RadialMenu }); svg.addEventListener('click', function(event) { - var className = event.target.parentNode.getAttribute('class').split(' ')[0]; + const className = event.target.parentNode.getAttribute('class').split(' ')[0]; switch (className) { case 'sector': @@ -301,7 +330,7 @@ class RadialMenu selectDelta(indexDelta) { - var selectedIndex = this.getSelectedIndex(); + let selectedIndex = this.getSelectedIndex(); if (selectedIndex < 0) { selectedIndex = 0; @@ -365,20 +394,13 @@ class RadialMenu { return; } - var delta = -event.deltaY; - if (delta > 0) - { - this.selectDelta(1); - } - else - { - this.selectDelta(-1); - } + const delta = -event.deltaY; + this.selectDelta(delta > 0 ? +1 : -1); } getSelectedNode() { - var items = this.currentMenu.getElementsByClassName('selected'); + const items = this.currentMenu.getElementsByClassName('selected'); if (items.length > 0) { return items[0]; @@ -388,23 +410,23 @@ class RadialMenu getSelectedIndex() { - var selectedNode = this.getSelectedNode(); + const selectedNode = this.getSelectedNode(); if (selectedNode) { return parseInt(selectedNode.getAttribute('data-index')); } return -1; - }; + } setSelectedIndex(index) { if (index >= 0 && index < this.levelItems.length) { - var items = this.currentMenu.querySelectorAll('g[data-index="' + index + '"]'); + const items = this.currentMenu.querySelectorAll('g[data-index="' + index + '"]'); if (items.length > 0) { - var itemToSelect = items[0]; - var selectedNode = this.getSelectedNode(); + const itemToSelect = items[0]; + const selectedNode = this.getSelectedNode(); if (selectedNode) { selectedNode.setAttribute('class', 'sector'); @@ -416,7 +438,7 @@ class RadialMenu createUseTag(x, y, link) { - var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttribute('x', this.numberToString(x)); use.setAttribute('y', this.numberToString(y)); @@ -430,16 +452,16 @@ class RadialMenu appendSectorPath(startAngleDeg, endAngleDeg, svg, item, index) { - var centerPoint = this.getSectorCenter(startAngleDeg, endAngleDeg); - var translate = { + const centerPoint = this.getSectorCenter(startAngleDeg, endAngleDeg); + const translate = { x: this.numberToString((1 - this.scale) * centerPoint.x), - y: this.numberToString((1 - this.scale) * centerPoint.y) + y: this.numberToString((1 - this.scale) * centerPoint.y), }; - var g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); g.setAttribute('transform', 'translate(' + translate.x + ' ,' + translate.y + ') scale(' + this.scale + ')'); - var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', this.createSectorCmds(startAngleDeg, endAngleDeg)); g.appendChild(path); @@ -455,7 +477,7 @@ class RadialMenu if (item.title) { - var text = this.createText(centerPoint.x, centerPoint.y, item.title); + const text = this.createText(centerPoint.x, centerPoint.y, item.title); if (item.icon) { text.setAttribute('transform', 'translate(0,8)'); @@ -469,7 +491,7 @@ class RadialMenu if (item.icon) { - var use = this.createUseTag(centerPoint.x, centerPoint.y, item.icon); + const use = this.createUseTag(centerPoint.x, centerPoint.y, item.icon); if (item.title) { use.setAttribute('transform', 'translate(-5,-8)'); @@ -491,16 +513,17 @@ class RadialMenu createSectorCmds(startAngleDeg, endAngleDeg) { - var initPoint = this.getDegreePos(startAngleDeg, this.radius); - var path = 'M' + this.pointToString(initPoint); + const initPoint = this.getDegreePos(startAngleDeg, this.radius); + let path = 'M' + this.pointToString(initPoint); + const radiusAfterScale = this.radius * (1 / this.scale); - var radiusAfterScale = this.radius * (1 / this.scale); path += 'A' + radiusAfterScale + ' ' + radiusAfterScale + ' 0 0 0' + this.pointToString(this.getDegreePos(endAngleDeg, this.radius)); path += 'L' + this.pointToString(this.getDegreePos(endAngleDeg, this.innerRadius)); - var radiusDiff = this.radius - this.innerRadius; - var radiusDelta = (radiusDiff - (radiusDiff * this.scale)) / 2; - var innerRadius = (this.innerRadius + radiusDelta) * (1 / this.scale); + const radiusDiff = this.radius - this.innerRadius; + const radiusDelta = (radiusDiff - (radiusDiff * this.scale)) / 2; + const innerRadius = (this.innerRadius + radiusDelta) * (1 / this.scale); + path += 'A' + innerRadius + ' ' + innerRadius + ' 0 0 1 ' + this.pointToString(this.getDegreePos(startAngleDeg, this.innerRadius)); path += 'Z'; @@ -509,12 +532,12 @@ class RadialMenu createText(x, y, title) { - var text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('x', this.numberToString(x)); text.setAttribute('y', this.numberToString(y)); - text.setAttribute('font-size', '38%'); + text.setAttribute('font-size', '38%');//TODO:?fontSize? text.innerHTML = title; return text; @@ -522,7 +545,7 @@ class RadialMenu createCircle(x, y, r) { - var circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', this.numberToString(x)); circle.setAttribute('cy', this.numberToString(y)); @@ -533,9 +556,9 @@ class RadialMenu calcScale() { - var totalSpace = this.sectorSpace * this.sectorCount; - var circleLength = Math.PI * 2 * this.radius; - var radiusDelta = this.radius - (circleLength - totalSpace) / (Math.PI * 2); + const totalSpace = this.sectorSpace * this.sectorCount; + const circleLength = Math.PI * 2 * this.radius; + const radiusDelta = this.radius - (circleLength - totalSpace) / (Math.PI * 2); return (this.radius - radiusDelta) / this.radius; } @@ -550,15 +573,15 @@ class RadialMenu addIconSymbols() { - var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 'icons'); // return - var returnSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); + const returnSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); returnSymbol.setAttribute('id', 'return'); returnSymbol.setAttribute('viewBox', '0 0 489.394 489.394'); - var returnPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const returnPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); returnPath.setAttribute('d', "M375.789,92.867H166.864l17.507-42.795c3.724-9.132,1-19.574-6.691-25.744c-7.701-6.166-18.538-6.508-26.639-0.879" + "L9.574,121.71c-6.197,4.304-9.795,11.457-9.563,18.995c0.231,7.533,4.261,14.446,10.71,18.359l147.925,89.823" + "c8.417,5.108,19.18,4.093,26.481-2.499c7.312-6.591,9.427-17.312,5.219-26.202l-19.443-41.132h204.886" + @@ -569,11 +592,12 @@ class RadialMenu returnSymbol.appendChild(returnPath); svg.appendChild(returnSymbol); - var closeSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); + // close + const closeSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); closeSymbol.setAttribute('id', 'close'); closeSymbol.setAttribute('viewBox', '0 0 41.756 41.756'); - var closePath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + const closePath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); closePath.setAttribute('d', "M27.948,20.878L40.291,8.536c1.953-1.953,1.953-5.119,0-7.071c-1.951-1.952-5.119-1.952-7.07,0L20.878,13.809L8.535,1.465" + "c-1.951-1.952-5.119-1.952-7.07,0c-1.953,1.953-1.953,5.119,0,7.071l12.342,12.342L1.465,33.22c-1.953,1.953-1.953,5.119,0,7.071" + "C2.44,41.268,3.721,41.755,5,41.755c1.278,0,2.56-0.487,3.535-1.464l12.343-12.342l12.343,12.343" + @@ -606,7 +630,7 @@ class RadialMenu else if (n) { - var r = (+n).toFixed(5); + let r = (+n).toFixed(5);//TODO:?magicNumber? if (r.match(/\./)) { r = r.replace(/\.?0+$/, ''); @@ -620,14 +644,12 @@ class RadialMenu { if (index < 0) { - index = length + index; + return length + index; } - if (index >= length) { - index = index - length; + return index - length; } - if (index < length) { return index; @@ -659,6 +681,45 @@ class RadialMenu nextTick(fn) { - setTimeout(fn, 10); + setTimeout(fn, 10);//TODO:?magicNumber? + } + + isObject(item) + { + return (item && typeof item === 'object' && !Array.isArray(item)); + } + + /** + * Deep merge two objects. + * -- https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge?page=1&tab=scoredesc#tab-top + * @param target + * @param ...sources + */ + merge(target, ...sources) + { + if (!sources.length) + { + return target; + } + const source = sources.shift(); + if (this.isObject(target) && this.isObject(source)) + { + for (const key in source) + { + if (this.isObject(source[key])) + { + if (!target[key]) + { + Object.assign(target, {[key]: {}}); + } + this.merge(target[key], source[key]); + } + else + { + Object.assign(target, { [key]: source[key] }); + } + } + } + return this.merge(target, ...sources); } } diff --git a/js/main.js b/js/main.js index 5693fae..2751252 100644 --- a/js/main.js +++ b/js/main.js @@ -105,11 +105,11 @@ window.onload = function () parent : document.body, size : 400, closeOnClick: true, - menuItems : menuItems, - onClick : function (item) - { - console.log('You have clicked:', item.id, item.title); - } + menuItems : menuItems + // onClick : function (item) + // { + // console.log('You have clicked:', item.id, item.title); + // } }); var openMenu = document.getElementById('openMenu'); From f95bd5009dd2ff9aec32b0337de6407638af9bb8 Mon Sep 17 00:00:00 2001 From: 3nda Date: Fri, 7 Jul 2023 22:20:24 +0200 Subject: [PATCH 04/16] refactor: RadialMenu.js: add closeOnClickOutside and right-click(context-menu) --- README.md | 25 ++++++-- index.html | 6 ++ js/RadialMenu.js | 158 ++++++++++++++++++++++++++++++++++++----------- js/main.js | 35 +++++++---- 4 files changed, 171 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index d986b9f..d52add1 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,14 @@ ## Controls - 1. Go to [https://axln.github.io/radial-menu-js/index.html](https://axln.github.io/radial-menu-js/index.html). - + 1. Go to [radial-menu-js/index.html](https://axln.github.io/radial-menu-js/index.html). 2. Click Open Menu button. - 3. You can use mouse, mouse wheel and keyboard for navigation: * Arrow keys and mouse wheel to select menu item. * Enter to choose the selected menu item. * Esc/Backspace to return to parent menu and close menu. - ## Usage Example +## Usage Example ```javascript var svgMenu = new RadialMenu({ @@ -60,5 +58,24 @@ var svgMenu = new RadialMenu({ }; ``` +## Configuration + +```json +{ + closeOnClick: true, // boolean; will menu.close(), after item is selected. [default: true && menu.onClickFallback();] + closeOnClickOutside: true, // true or function(); it will menu.close(), when item is not selected and click is outside of menu. [default: true] + + nested: { // nested ~ inner-menu behavior + icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) + title: true // show nested title? + } +} +``` + ## License MIT + +## Contributors + +* [Alexey Nesterenko](https://github.com/axln) +* [Jan Smid](https://github.com/j3nda) diff --git a/index.html b/index.html index d40c029..819ae69 100644 --- a/index.html +++ b/index.html @@ -226,6 +226,12 @@

Radial Menu Test

+

+ + You can try also "Context Menu" + → right-mouse-click. + +

\ No newline at end of file diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 2d7d00b..4674f52 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -1,11 +1,10 @@ 'use strict'; /** - * https://github.com/axln/radial-menu-js - * https://github.com/j3nda/radial-menu-js - * About * Radial menu in pure JavaScript, HTML and SVG * License: MIT + * -- https://github.com/axln/radial-menu-js + * -- https://github.com/j3nda/radial-menu-js */ class RadialMenu { @@ -19,10 +18,11 @@ class RadialMenu }, radiusInner: 0.4, radiusSectorSpace: 0.06, - closeOnClick: true, + closeOnClick: true, // true or function(); will close(); after item is selected. [default: onClickFallback()] + closeOnClickOutside: true, // true or function(); it will close(); when item is not selected and click is outside of menu. [default: true] nested: { icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) - title: true, + title: true, // show nested title? //TODO:?it can show (number of nested menu)? //TODO:?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? } @@ -30,13 +30,15 @@ class RadialMenu constructor(params) { + const THIS = this; const defaultValues = this.defaultValues = RadialMenu._defaultValues; + this.uuid = this.generateUUID(); this.parent = params.parent || [];//TODO: refactor: ?this.attr = this.merge(defaultValues, params);? this.size = params.size || defaultValues.size; this.onClick = params.onClick || function(item) { - this.onClickFallback(item); + THIS.onClickFallback(item); }; this.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; @@ -44,10 +46,17 @@ class RadialMenu this.innerRadius = params.innerRadius ? params.innerRadius : this.radius * defaultValues.radius.multiInnerRadius; this.sectorSpace = params.sectorSpace ? params.sectorSpace : this.radius * defaultValues.radius.multiSectorSpace; this.sectorCount = Math.max(this.menuItems.length, defaultValues.minSectors); - this.closeOnClick = params.closeOnClick !== undefined ? !!params.closeOnClick : defaultValues.closeOnClick; + this.closeOnClick = (params.closeOnClick !== undefined + ? !!params.closeOnClick + : defaultValues.closeOnClick + ); + this.closeOnClickOutside = (params.closeOnClickOutside !== undefined + ? !!params.closeOnClickOutside + : defaultValues.closeOnClickOutside + ); this.nested = this.merge( defaultValues.nested, - (params.nested ? params.nested : null) + (params.nested ? params.nested : {}) ); this.scale = 1;//TODO:?ma smysl mit jiny scale? this.holder = null; @@ -55,14 +64,41 @@ class RadialMenu this.parentItems = []; this.levelItems = null; - this.createHolder(); - this.addIconSymbols(); + this.createHolder();//TODO:?multiple menuHolder? + this.addIconSymbols();//TODO:?iconSymbolsFactory? this.currentMenu = null; document.addEventListener('wheel', this.onMouseWheel.bind(this));//TODO:?enable/disable? document.addEventListener('keydown', this.onKeyDown.bind(this));//TODO:?enable/disable? } + generateUUID() + { + // -- https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid + // License: Public Domain / MIT + let d1 = new Date().getTime();//Timestamp + let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0; // Time in microseconds since page-load or 0 if unsupported + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + .replace(/[xy]/g, function(c) + { + let r = Math.random() * 16; // random number between 0 and 16 + if (d1 > 0) + { + // Use timestamp until depleted + r = (d1 + r) % 16 | 0; + d1 = Math.floor(d1 / 16); + } + else + { + // Use microseconds since page-load if supported + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }) + ; + } + onClickFallback(item) { console.warn('function onClick(item): is not defined by params! default!'); @@ -70,44 +106,91 @@ class RadialMenu console.info(item); } - //TODO:?isOpen(){ return this.currentMenu == null; } + isOpen() + { + return (this.currentMenu !== null); + } - open() + onClickOutside(event, THIS) { - if (!this.currentMenu) + const menu = document.getElementById(THIS.uuid); + if (!menu) { - this.currentMenu = this.createMenu('menu inner', this.menuItems); - this.holder.appendChild(this.currentMenu); - - // wait DOM commands to apply and then set class to allow transition to take effect - const THIS = this; - this.nextTick(function() + return; + } + let target = event.target; + do + { + if (target == menu) { - THIS.currentMenu.setAttribute('class', 'menu'); - }); + // click inside! do nothing... + return; + } + // go up thru DOM + target = target.parentNode; } + while (target); + + // click outside! + if (THIS.closeOnClickOutside && THIS.closeOnClickOutside instanceof Function) + { + THIS.closeOnClickOutside(THIS); + } + THIS.close(); } - close() + open() { - if (this.currentMenu) + if (this.isOpen()) + { + return; + } + this.currentMenu = this.createMenu('menu inner', this.menuItems); + this.holder.appendChild(this.currentMenu); + + // wait DOM commands to apply and then set class to allow transition to take effect + const THIS = this; + this.nextTick(function() { - let parentMenu; - while (parentMenu = this.parentMenu.pop()) + THIS.currentMenu.setAttribute('class', 'menu'); + if (THIS.closeOnClickOutside) { - parentMenu.remove(); + document.addEventListener('click', THIS.closeOnClickOutsideListener = function(event) + { + THIS.onClickOutside(event, THIS); + }); } - this.parentItems = []; + }); + } - const THIS = this; - this.setClassAndWaitForTransition(this.currentMenu, 'menu inner') - .then(function() + close() + { + if (!this.isOpen()) + { + return; + } + let parentMenu; + while (parentMenu = this.parentMenu.pop()) + { + parentMenu.remove(); + } + this.parentItems = []; + + const THIS = this; + this.setClassAndWaitForTransition(this.currentMenu, 'menu inner') + .then(function() + { + if (THIS.currentMenu !== null) { THIS.currentMenu.remove(); - THIS.currentMenu = null; - }) - ; - } + } + THIS.currentMenu = null; + if (THIS.closeOnClickOutside) + { + document.removeEventListener('click', THIS.closeOnClickOutsideListener); + } + }) + ; } onClick(item) @@ -127,6 +210,7 @@ class RadialMenu createHolder() { this.holder = document.createElement('div'); + this.holder.id = this.uuid; this.holder.className = 'menuHolder'; this.holder.style.width = this.size + 'px'; this.holder.style.height = this.size + 'px'; @@ -350,7 +434,7 @@ class RadialMenu onKeyDown(event) { - if (!this.currentMenu) + if (!this.isOpen()) { return; } @@ -390,7 +474,7 @@ class RadialMenu onMouseWheel(event) { - if (!this.currentMenu) + if (!this.isOpen()) { return; } @@ -468,7 +552,7 @@ class RadialMenu if (item) { g.setAttribute('class', 'sector'); - if (index == 0) + if (index === 0) { g.setAttribute('class', 'sector selected'); } diff --git a/js/main.js b/js/main.js index 2751252..50d2dc5 100644 --- a/js/main.js +++ b/js/main.js @@ -101,24 +101,35 @@ var menuItems = [ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// window.onload = function () { - var svgMenu = new RadialMenu({ - parent : document.body, - size : 400, + const svgMenu = new RadialMenu({ + parent: document.body, + size: 400, closeOnClick: true, - menuItems : menuItems - // onClick : function (item) - // { - // console.log('You have clicked:', item.id, item.title); - // } + menuItems: menuItems, + onClick: function(item) + { + console.log('You have clicked:', item.id, item.title); + console.log(item); + } }); - var openMenu = document.getElementById('openMenu'); - openMenu.onclick = function () { + const openMenu = document.getElementById('openMenu'); + openMenu.onclick = function() { svgMenu.open(); }; - var closeMenu = document.getElementById('closeMenu'); - closeMenu.onclick = function () { + const closeMenu = document.getElementById('closeMenu'); + closeMenu.onclick = function() { svgMenu.close(); }; + + document.addEventListener('contextmenu', function(event) + { + event.preventDefault(); + if (svgMenu.isOpen()) + { + return; + } + svgMenu.open(); + }); }; From f1bba2006c3216dac148b4731a3485598fbb1c6c Mon Sep 17 00:00:00 2001 From: 3nda Date: Fri, 7 Jul 2023 22:43:39 +0200 Subject: [PATCH 05/16] polishing... --- README.md | 8 ++++---- js/RadialMenu.js | 9 +++++++++ js/main.js | 16 +++++++--------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d52add1..d6ce836 100644 --- a/README.md +++ b/README.md @@ -60,15 +60,15 @@ var svgMenu = new RadialMenu({ ## Configuration -```json +```json5 { closeOnClick: true, // boolean; will menu.close(), after item is selected. [default: true && menu.onClickFallback();] closeOnClickOutside: true, // true or function(); it will menu.close(), when item is not selected and click is outside of menu. [default: true] nested: { // nested ~ inner-menu behavior - icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) - title: true // show nested title? - } + icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) + title: true // show nested title? + } } ``` diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 4674f52..8db7d9e 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -281,6 +281,15 @@ class RadialMenu { this.close(); } + // FIXME: https://github.com/axln/radial-menu-js/issues/3 + // When you are in a submenu and exit (any way) too fast, + // the event in the function '''RadialMenu.prototype.handleCenterClick''' + // call '''self.returnToParentMenu();''' instead of '''self.close();'''. + // This makes it difficult to tell when the menu is completely closed + // + // radial-menu-js/js/RadialMenu.js + // Line 137 in 6d027fc + // RadialMenu.prototype.handleCenterClick = function () {...} } createCenter(svg, title, icon, size, nested = null) diff --git a/js/main.js b/js/main.js index 50d2dc5..d083eb2 100644 --- a/js/main.js +++ b/js/main.js @@ -105,6 +105,7 @@ window.onload = function () parent: document.body, size: 400, closeOnClick: true, + // closeOnClickOutside: false, menuItems: menuItems, onClick: function(item) { @@ -112,17 +113,14 @@ window.onload = function () console.log(item); } }); - - const openMenu = document.getElementById('openMenu'); - openMenu.onclick = function() { + document.getElementById('openMenu').addEventListener('click', function(event) + { svgMenu.open(); - }; - - const closeMenu = document.getElementById('closeMenu'); - closeMenu.onclick = function() { + }); + document.getElementById('closeMenu').addEventListener('click', function(event) + { svgMenu.close(); - }; - + }); document.addEventListener('contextmenu', function(event) { event.preventDefault(); From b32ba7028014526d79bfb4a6025e00ba359629dc Mon Sep 17 00:00:00 2001 From: 3nda Date: Fri, 7 Jul 2023 23:50:39 +0200 Subject: [PATCH 06/16] refactor: RadialMenu.js: add configurable css/classes = RadialMenu.defaultValues: { ui: { classes: { menuContainer: "menuHolder", // whole radial-menu container, created dynamically! menu: "menu", menuCreate: "menu inner", // main menu [{menuCreate} inner] menuCreateNested: "menu outer", // nested menu [{menuCreate} outer] itemSelected: "selected", // item, which is [selected] itemIcon: "icons", // item's icon itemSector: "sector", // item, which is active itemDummy: "dummy", // item, which is not active buttonCenter: "center", // button (close, back) ~ centered! }, icons: { back: {title: "Back", icon: "#return"}, close: {title: "Close", icon: "#close"}, } } } --- js/RadialMenu.js | 135 ++++++++++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 55 deletions(-) diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 8db7d9e..061952b 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -25,6 +25,23 @@ class RadialMenu title: true, // show nested title? //TODO:?it can show (number of nested menu)? //TODO:?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? + }, + ui: { + classes: { + menuContainer: "menuHolder", // whole radial-menu container, created dynamically! + menu: "menu", + menuCreate: "menu inner", // main menu [{menuCreate} inner] + menuCreateNested: "menu outer", // nested menu [{menuCreate} outer] + itemSelected: "selected", // item, which is [selected] + itemIcon: "icons", // item's icon + itemSector: "sector", // item, which is active + itemDummy: "dummy", // item, which is not active + buttonCenter: "center", // button (close, back) ~ centered! + }, + icons: { + back: {title: "Back", icon: "#return"}, + close: {title: "Close", icon: "#close"}, + } } } @@ -36,35 +53,33 @@ class RadialMenu this.parent = params.parent || [];//TODO: refactor: ?this.attr = this.merge(defaultValues, params);? this.size = params.size || defaultValues.size; - this.onClick = params.onClick || function(item) - { - THIS.onClickFallback(item); - }; this.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; this.radius = params.radius ? params.radius : defaultValues.radius.value; this.innerRadius = params.innerRadius ? params.innerRadius : this.radius * defaultValues.radius.multiInnerRadius; this.sectorSpace = params.sectorSpace ? params.sectorSpace : this.radius * defaultValues.radius.multiSectorSpace; this.sectorCount = Math.max(this.menuItems.length, defaultValues.minSectors); - this.closeOnClick = (params.closeOnClick !== undefined - ? !!params.closeOnClick - : defaultValues.closeOnClick - ); + this.closeOnClick = params.closeOnClick !== undefined ? !!params.closeOnClick : defaultValues.closeOnClick; this.closeOnClickOutside = (params.closeOnClickOutside !== undefined - ? !!params.closeOnClickOutside + ? (params.closeOnClickOutside instanceof Function ? params.closeOnClickOutside : !!params.closeOnClickOutside) : defaultValues.closeOnClickOutside ); + this.onClick = params.onClick || this.onClickFallback; this.nested = this.merge( defaultValues.nested, - (params.nested ? params.nested : {}) + params.nested || {} + ); + this.ui = this.merge( + defaultValues.ui, + params.nested || {} ); - this.scale = 1;//TODO:?ma smysl mit jiny scale? + this.scale = 1;//TODO:?do we need different scale? this.holder = null; this.parentMenu = []; this.parentItems = []; this.levelItems = null; - this.createHolder();//TODO:?multiple menuHolder? + this.createHolder(this.ui.classes.menuContainer); this.addIconSymbols();//TODO:?iconSymbolsFactory? this.currentMenu = null; @@ -101,9 +116,23 @@ class RadialMenu onClickFallback(item) { - console.warn('function onClick(item): is not defined by params! default!'); - console.info(this.constructor.name + ".onClick()"); + console.info(this.constructor.name + ".onClickFallback(item):"); console.info(item); + throw "onClick: function(item) {...}; // must be defined by params or default!"; + } + + onClickCallback(item) + { + if (this.closeOnClick) + { + this.close(); + } + if (this.onClick && this.onClick instanceof Function) + { + this.onClick(item); + return; + } + this.onClickFallback(item); } isOpen() @@ -145,14 +174,14 @@ class RadialMenu { return; } - this.currentMenu = this.createMenu('menu inner', this.menuItems); + this.currentMenu = this.createMenu(this.ui.classes.menuCreate, this.menuItems); this.holder.appendChild(this.currentMenu); // wait DOM commands to apply and then set class to allow transition to take effect const THIS = this; this.nextTick(function() { - THIS.currentMenu.setAttribute('class', 'menu'); + THIS.currentMenu.setAttribute('class', THIS.ui.classes.menu); if (THIS.closeOnClickOutside) { document.addEventListener('click', THIS.closeOnClickOutsideListener = function(event) @@ -177,7 +206,7 @@ class RadialMenu this.parentItems = []; const THIS = this; - this.setClassAndWaitForTransition(this.currentMenu, 'menu inner') + this.setClassAndWaitForTransition(this.currentMenu, this.ui.classes.menuCreate) .then(function() { if (THIS.currentMenu !== null) @@ -207,11 +236,11 @@ class RadialMenu return null; } - createHolder() + createHolder(classValue) { this.holder = document.createElement('div'); this.holder.id = this.uuid; - this.holder.className = 'menuHolder'; + this.holder.className = classValue; this.holder.style.width = this.size + 'px'; this.holder.style.height = this.size + 'px'; @@ -222,24 +251,24 @@ class RadialMenu { this.parentMenu.push(this.currentMenu); this.parentItems.push(this.levelItems); - this.currentMenu = this.createMenu('menu inner', item.items, item); + this.currentMenu = this.createMenu(this.ui.classes.menuCreate, item.items, item); this.holder.appendChild(this.currentMenu); // wait DOM commands to apply and then set class to allow transition to take effect const THIS = this; this.nextTick(function() { - THIS.getParentMenu().setAttribute('class', 'menu outer'); - THIS.currentMenu.setAttribute('class', 'menu'); + THIS.getParentMenu().setAttribute('class', THIS.ui.classes.menuCreateNested); + THIS.currentMenu.setAttribute('class', THIS.ui.classes.menu); }); } returnToParentMenu() { - this.getParentMenu().setAttribute('class', 'menu'); + this.getParentMenu().setAttribute('class', this.ui.classes.menu); const THIS = this; - this.setClassAndWaitForTransition(this.currentMenu, 'menu inner') + this.setClassAndWaitForTransition(this.currentMenu, this.ui.classes.menuCreate) .then(function() { THIS.currentMenu.remove(); @@ -258,16 +287,9 @@ class RadialMenu if (item.items) { this.showNestedMenu(item); + return; } - else - if (this.onClick) - { - this.onClick(item); - if (this.closeOnClick) - { - this.close(); - } - } + this.onClickCallback(item); } } @@ -297,7 +319,7 @@ class RadialMenu size = size || 8;//TODO:?magicNumber?default value? const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - g.setAttribute('class', 'center'); + g.setAttribute('class', this.ui.classes.buttonCenter); const centerCircle = this.createCircle(0, 0, this.innerRadius - this.sectorSpace / 3); g.appendChild(centerCircle); @@ -374,19 +396,19 @@ class RadialMenu if (nested) { - this.createCenter(svg, 'Back', '#return', 8, nested); //TODO:??magicNumber?? + this.createCenter(svg, this.ui.icons.back.title, this.ui.icons.back.icon, 8, nested); //TODO:??magicNumber?? } else { - this.createCenter(svg, 'Close', '#close', 7);//TODO:??magicNumber?? + this.createCenter(svg, this.ui.icons.close.title, this.ui.icons.close.icon, 7);//TODO:??magicNumber?? } svg.addEventListener('mousedown', function(event) { - const className = event.target.parentNode.getAttribute('class').split(' ')[0]; - switch (className) + const classNames = event.target.parentNode.getAttribute('class').split(' '); + for (let i = 0; i < classNames.length; i++) { - case 'sector': + if (classNames[i] === THIS.ui.classes.itemSector) { const index = parseInt(event.target.parentNode.getAttribute('data-index')); if (!isNaN(index)) @@ -395,27 +417,23 @@ class RadialMenu } break; } - default: - break; } }); svg.addEventListener('click', function(event) { - const className = event.target.parentNode.getAttribute('class').split(' ')[0]; - switch (className) + const classNames = event.target.parentNode.getAttribute('class').split(' '); + for (let i = 0; i < classNames.length; i++) { - case 'sector': + if (classNames[i] === THIS.ui.classes.itemSector) { THIS.handleClick(); break; } - case 'center': + if (classNames[i] === THIS.ui.classes.buttonCenter) { THIS.handleCenterClick(); break; } - default: - break; } }); return svg; @@ -447,6 +465,7 @@ class RadialMenu { return; } + //TODO:?enable keys?which one? switch (event.key) { case 'Escape': @@ -465,7 +484,7 @@ class RadialMenu case 'ArrowRight': case 'ArrowUp': { - this.selectDelta(1); + this.selectDelta(+1); event.preventDefault(); break; } @@ -493,7 +512,7 @@ class RadialMenu getSelectedNode() { - const items = this.currentMenu.getElementsByClassName('selected'); + const items = this.currentMenu.getElementsByClassName(this.ui.classes.itemSelected); if (items.length > 0) { return items[0]; @@ -522,9 +541,12 @@ class RadialMenu const selectedNode = this.getSelectedNode(); if (selectedNode) { - selectedNode.setAttribute('class', 'sector'); + selectedNode.setAttribute('class', this.ui.classes.itemSector); } - itemToSelect.setAttribute('class', 'sector selected'); + itemToSelect.setAttribute( + 'class', + [this.ui.classes.itemSector, this.ui.classes.itemSelected].join(' ') + ); } } } @@ -560,10 +582,13 @@ class RadialMenu if (item) { - g.setAttribute('class', 'sector'); + g.setAttribute('class', this.ui.classes.itemSector); if (index === 0) { - g.setAttribute('class', 'sector selected'); + g.setAttribute( + 'class', + [this.ui.classes.itemSector, this.ui.classes.itemSelected].join(' ') + ); } g.setAttribute('data-id', item.id); g.setAttribute('data-index', index); @@ -599,7 +624,7 @@ class RadialMenu } else { - g.setAttribute('class', 'dummy'); + g.setAttribute('class', this.ui.classes.itemDummy); } svg.appendChild(g); }; @@ -667,7 +692,7 @@ class RadialMenu addIconSymbols() { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('class', 'icons'); + svg.setAttribute('class', this.ui.classes.itemIcon); // return const returnSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); From 7bb427b8275dfeda1d0e198244d978cbf8eff3b2 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sat, 8 Jul 2023 00:18:21 +0200 Subject: [PATCH 07/16] polishing... --- css/main.css | 6 +++++- js/RadialMenu.js | 46 ++++++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/css/main.css b/css/main.css index 1fe91cd..f545dc0 100644 --- a/css/main.css +++ b/css/main.css @@ -1,7 +1,11 @@ body { font-family: Arial, sans-serif; font-size: 13px; - background: url("../img/background.jpg") no-repeat; + background: url("../img/background.jpg") no-repeat center center fixed; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; } h1 { diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 061952b..04922ea 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -47,14 +47,11 @@ class RadialMenu constructor(params) { - const THIS = this; const defaultValues = this.defaultValues = RadialMenu._defaultValues; this.uuid = this.generateUUID(); this.parent = params.parent || [];//TODO: refactor: ?this.attr = this.merge(defaultValues, params);? - this.size = params.size || defaultValues.size; this.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; - this.radius = params.radius ? params.radius : defaultValues.radius.value; this.innerRadius = params.innerRadius ? params.innerRadius : this.radius * defaultValues.radius.multiInnerRadius; this.sectorSpace = params.sectorSpace ? params.sectorSpace : this.radius * defaultValues.radius.multiSectorSpace; @@ -79,7 +76,7 @@ class RadialMenu this.parentItems = []; this.levelItems = null; - this.createHolder(this.ui.classes.menuContainer); + this.createMenuContainer(this.ui.classes.menuContainer); this.addIconSymbols();//TODO:?iconSymbolsFactory? this.currentMenu = null; @@ -150,7 +147,7 @@ class RadialMenu let target = event.target; do { - if (target == menu) + if (target === menu) { // click inside! do nothing... return; @@ -179,7 +176,7 @@ class RadialMenu // wait DOM commands to apply and then set class to allow transition to take effect const THIS = this; - this.nextTick(function() + this.postRunnable(function() { THIS.currentMenu.setAttribute('class', THIS.ui.classes.menu); if (THIS.closeOnClickOutside) @@ -236,7 +233,7 @@ class RadialMenu return null; } - createHolder(classValue) + createMenuContainer(classValue) { this.holder = document.createElement('div'); this.holder.id = this.uuid; @@ -256,7 +253,7 @@ class RadialMenu // wait DOM commands to apply and then set class to allow transition to take effect const THIS = this; - this.nextTick(function() + this.postRunnable(function() { THIS.getParentMenu().setAttribute('class', THIS.ui.classes.menuCreateNested); THIS.currentMenu.setAttribute('class', THIS.ui.classes.menu); @@ -369,7 +366,7 @@ class RadialMenu this.levelItems = levelItems; this.sectorCount = Math.max(this.levelItems.length, this.defaultValues.minSectors); - this.scale = this.calcScale(); + this.scale = this.calculateScale(); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', classValue); @@ -567,7 +564,7 @@ class RadialMenu appendSectorPath(startAngleDeg, endAngleDeg, svg, item, index) { - const centerPoint = this.getSectorCenter(startAngleDeg, endAngleDeg); + const centerPoint = this.getSectorPosition(startAngleDeg, endAngleDeg); const translate = { x: this.numberToString((1 - this.scale) * centerPoint.x), y: this.numberToString((1 - this.scale) * centerPoint.y), @@ -577,7 +574,7 @@ class RadialMenu g.setAttribute('transform', 'translate(' + translate.x + ' ,' + translate.y + ') scale(' + this.scale + ')'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('d', this.createSectorCmds(startAngleDeg, endAngleDeg)); + path.setAttribute('d', this.createSector(startAngleDeg, endAngleDeg)); g.appendChild(path); if (item) @@ -629,20 +626,20 @@ class RadialMenu svg.appendChild(g); }; - createSectorCmds(startAngleDeg, endAngleDeg) + createSector(startAngleDeg, endAngleDeg) { - const initPoint = this.getDegreePos(startAngleDeg, this.radius); + const initPoint = this.getDegreePosition(startAngleDeg, this.radius); let path = 'M' + this.pointToString(initPoint); const radiusAfterScale = this.radius * (1 / this.scale); - path += 'A' + radiusAfterScale + ' ' + radiusAfterScale + ' 0 0 0' + this.pointToString(this.getDegreePos(endAngleDeg, this.radius)); - path += 'L' + this.pointToString(this.getDegreePos(endAngleDeg, this.innerRadius)); + path += 'A' + radiusAfterScale + ' ' + radiusAfterScale + ' 0 0 0' + this.pointToString(this.getDegreePosition(endAngleDeg, this.radius)); + path += 'L' + this.pointToString(this.getDegreePosition(endAngleDeg, this.innerRadius)); const radiusDiff = this.radius - this.innerRadius; const radiusDelta = (radiusDiff - (radiusDiff * this.scale)) / 2; const innerRadius = (this.innerRadius + radiusDelta) * (1 / this.scale); - path += 'A' + innerRadius + ' ' + innerRadius + ' 0 0 1 ' + this.pointToString(this.getDegreePos(startAngleDeg, this.innerRadius)); + path += 'A' + innerRadius + ' ' + innerRadius + ' 0 0 1 ' + this.pointToString(this.getDegreePosition(startAngleDeg, this.innerRadius)); path += 'Z'; return path; @@ -672,7 +669,7 @@ class RadialMenu return circle; }; - calcScale() + calculateScale() { const totalSpace = this.sectorSpace * this.sectorCount; const circleLength = Math.PI * 2 * this.radius; @@ -681,9 +678,9 @@ class RadialMenu return (this.radius - radiusDelta) / this.radius; } - getSectorCenter(startAngleDeg, endAngleDeg) + getSectorPosition(startAngleDeg, endAngleDeg) { - return this.getDegreePos( + return this.getDegreePosition( (startAngleDeg + endAngleDeg) / 2, this.innerRadius + (this.radius - this.innerRadius) / 2 ); @@ -726,7 +723,7 @@ class RadialMenu this.holder.appendChild(svg); } - getDegreePos(angleDeg, length) + getDegreePosition(angleDeg, length) { return { x: Math.sin(this.degToRad(angleDeg)) * length, @@ -786,7 +783,7 @@ class RadialMenu { function handler(event) { - if (event.target == node && event.propertyName == 'visibility') + if (event.target === node && event.propertyName === 'visibility') { node.removeEventListener('transitionend', handler); resolve(); @@ -797,9 +794,10 @@ class RadialMenu }); } - nextTick(fn) + postRunnable(fn, timeoutMs = 10) { - setTimeout(fn, 10);//TODO:?magicNumber? + //TODO:??idk if i like it. it looks messy due prev. RadialMenu.prototype approach!? + setTimeout(fn, timeoutMs); } isObject(item) @@ -811,7 +809,7 @@ class RadialMenu * Deep merge two objects. * -- https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge?page=1&tab=scoredesc#tab-top * @param target - * @param ...sources + * @param sources */ merge(target, ...sources) { From 229acbc8119d076a690d7b8f73a9c9feb4331612 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sat, 8 Jul 2023 00:40:59 +0200 Subject: [PATCH 08/16] refactor: RadialMenu.js: fix possibility of pre-selected items = menu.js: var menuItems = [ ... { id : 'drive', title: 'Drive', icon: '#drive', selected: true }, ... ] --- js/RadialMenu.js | 2 +- js/main.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 04922ea..c01a105 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -580,7 +580,7 @@ class RadialMenu if (item) { g.setAttribute('class', this.ui.classes.itemSector); - if (index === 0) + if (item.selected && item.selected === true) { g.setAttribute( 'class', diff --git a/js/main.js b/js/main.js index d083eb2..a34164d 100644 --- a/js/main.js +++ b/js/main.js @@ -14,12 +14,13 @@ var menuItems = [ { id : 'drive', title: 'Drive', - icon: '#drive' + icon: '#drive', + selected: true }, { id : 'figth', title: 'Fight', - icon: '#fight' + icon: '#fight', }, { id : 'more', @@ -34,7 +35,8 @@ var menuItems = [ { id : 'sleep', title: 'Sleep', - icon: '#sleep' + icon: '#sleep', + selected: true }, { id : 'shower', From b7dc52cddece577a1ed25c24ee10a27183d0293c Mon Sep 17 00:00:00 2001 From: 3nda Date: Sat, 8 Jul 2023 02:25:14 +0200 Subject: [PATCH 09/16] refactor: RadialMenu.js: add possibility to've multiple RadialMenu(s) with css (open, close, more) functionality --- README.md | 21 ++++++++ css/RadialMenu.css | 2 - css/RadialMenuCustom.css | 112 +++++++++++++++++++++++++++++++++++++++ index.html | 1 + js/RadialMenu.js | 78 ++++++++++++++++++--------- js/main.js | 62 ++++++++++++++++++++-- 6 files changed, 246 insertions(+), 30 deletions(-) create mode 100644 css/RadialMenuCustom.css diff --git a/README.md b/README.md index d6ce836..4319f4c 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,27 @@ var svgMenu = new RadialMenu({ nested: { // nested ~ inner-menu behavior icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) title: true // show nested title? + }, + + ui: { // ui customization + classes: { + menuContainer: "menuHolder", // whole radial-menu container, created dynamically! + menu: "menu", + menuOpen: "open", // menu is visible [open] + menuClose: "close", // menu is not-visible [close] + menuCreateParent: "menu inner", // main menu [{menu} inner] + menuCreateNested: "menu outer", // nested menu [{menu} outer] + itemSelected: "selected", // item, which is [selected] + itemIcon: "icons", // item's icon + itemSector: "sector", // item, which is active + itemSectorNested: "more", // item, which has nested items... [more] + itemDummy: "dummy", // item, which is not active + buttonCenter: "center", // button (close, back) ~ centered! + }, + icons: { + back: {title: "Back", icon: "#return"}, + close: {title: "Close", icon: "#close"}, + } } } ``` diff --git a/css/RadialMenu.css b/css/RadialMenu.css index abd727e..26bfd98 100644 --- a/css/RadialMenu.css +++ b/css/RadialMenu.css @@ -4,7 +4,6 @@ div.menuHolder { position: relative; margin: 10px; } - svg.icons { display: none; } @@ -47,7 +46,6 @@ svg.menu > g.sector > use { svg.menu > g.sector:hover > path { fill: #F9A602D0; - } svg.menu > g.sector.selected > path { diff --git a/css/RadialMenuCustom.css b/css/RadialMenuCustom.css new file mode 100644 index 0000000..f9221a6 --- /dev/null +++ b/css/RadialMenuCustom.css @@ -0,0 +1,112 @@ +div.menuHolder2 +{ /* menu-container */ + user-select: none; + -moz-user-select: none; + margin: 10px; + + position: fixed; + top: 0px; + left: 0px; + z-index: -1; +} + +div.menuHolder2.open2 +{ /* menu-container:open => its visible */ + z-index: 11; +} + +div.menuHolder2.close2 +{ /* menu-container:close => its not visible */ + +} + +svg.icons +{ + display: none; +} + +svg.menu2 +{ + position: absolute; + overflow: visible; + transition: 0.2s; + transition-timing-function: ease-out; +} + +svg.menu2.open2 +{ /* menu-items:open => background, transition, gfx... */ + /* -- https://www.svgbackgrounds.com/*/ + background-color: #ffaa00; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 1600 800'%3E%3Cg %3E%3Cpath fill='%23ffb100' d='M486 705.8c-109.3-21.8-223.4-32.2-335.3-19.4C99.5 692.1 49 703 0 719.8V800h843.8c-115.9-33.2-230.8-68.1-347.6-92.2C492.8 707.1 489.4 706.5 486 705.8z'/%3E%3Cpath fill='%23ffb800' d='M1600 0H0v719.8c49-16.8 99.5-27.8 150.7-33.5c111.9-12.7 226-2.4 335.3 19.4c3.4 0.7 6.8 1.4 10.2 2c116.8 24 231.7 59 347.6 92.2H1600V0z'/%3E%3Cpath fill='%23ffbe00' d='M478.4 581c3.2 0.8 6.4 1.7 9.5 2.5c196.2 52.5 388.7 133.5 593.5 176.6c174.2 36.6 349.5 29.2 518.6-10.2V0H0v574.9c52.3-17.6 106.5-27.7 161.1-30.9C268.4 537.4 375.7 554.2 478.4 581z'/%3E%3Cpath fill='%23ffc500' d='M0 0v429.4c55.6-18.4 113.5-27.3 171.4-27.7c102.8-0.8 203.2 22.7 299.3 54.5c3 1 5.9 2 8.9 3c183.6 62 365.7 146.1 562.4 192.1c186.7 43.7 376.3 34.4 557.9-12.6V0H0z'/%3E%3Cpath fill='%23ffcc00' d='M181.8 259.4c98.2 6 191.9 35.2 281.3 72.1c2.8 1.1 5.5 2.3 8.3 3.4c171 71.6 342.7 158.5 531.3 207.7c198.8 51.8 403.4 40.8 597.3-14.8V0H0v283.2C59 263.6 120.6 255.7 181.8 259.4z'/%3E%3Cpath fill='%23ffd914' d='M1600 0H0v136.3c62.3-20.9 127.7-27.5 192.2-19.2c93.6 12.1 180.5 47.7 263.3 89.6c2.6 1.3 5.1 2.6 7.7 3.9c158.4 81.1 319.7 170.9 500.3 223.2c210.5 61 430.8 49 636.6-16.6V0z'/%3E%3Cpath fill='%23ffe529' d='M454.9 86.3C600.7 177 751.6 269.3 924.1 325c208.6 67.4 431.3 60.8 637.9-5.3c12.8-4.1 25.4-8.4 38.1-12.9V0H288.1c56 21.3 108.7 50.6 159.7 82C450.2 83.4 452.5 84.9 454.9 86.3z'/%3E%3Cpath fill='%23ffef3d' d='M1600 0H498c118.1 85.8 243.5 164.5 386.8 216.2c191.8 69.2 400 74.7 595 21.1c40.8-11.2 81.1-25.2 120.3-41.7V0z'/%3E%3Cpath fill='%23fff852' d='M1397.5 154.8c47.2-10.6 93.6-25.3 138.6-43.8c21.7-8.9 43-18.8 63.9-29.5V0H643.4c62.9 41.7 129.7 78.2 202.1 107.4C1020.4 178.1 1214.2 196.1 1397.5 154.8z'/%3E%3Cpath fill='%23ffff66' d='M1315.3 72.4c75.3-12.6 148.9-37.1 216.8-72.4h-723C966.8 71 1144.7 101 1315.3 72.4z'/%3E%3C/g%3E%3C/svg%3E"); + background-attachment: fixed; + background-size: cover; + + box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.75); + -webkit-box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.75); + -moz-box-shadow: 10px 10px 5px 0px rgba(0, 0, 0, 0.75); + border-radius: 50%; +} + +svg.menu2.inner +{ + transform: scale(0.66) rotate(-10deg); + opacity: 0; + visibility: hidden; +} + +svg.menu2.outer +{ + transform: scale(1.5) rotate(10deg); + opacity: 0; + visibility: hidden; +} + +svg.menu2 > g > path +{ + fill: #00000080; +} + +svg.menu2 > g.sector > path +{ + cursor: pointer; + /*transition: 0.1s;*/ +} + +svg.menu2 > g.sector > text, +svg.menu2 > g.sector > use +{ + cursor: pointer; + fill: white; + + /*text-shadow: 1px 1px 0 #000000A0;*/ +} + +svg.menu2 > g.sector:hover > path +{ + fill: #9B13CDD0; +} + +svg.menu2 > g.sector.selected > path +{ + /*fill: #F9A602D0;*/ + /*fill: #009900D0 !important;*/ + fill: #32CD32D0 !important; +} + +svg.menu2 > g.center:hover > circle +{ + fill: #E70777D0; +} + +svg.menu2 > g.center > circle +{ + cursor: pointer; + fill: #00000080; +} + +svg.menu2 > g.center > text, +svg.menu2 > g.center > use +{ + cursor: pointer; + fill: white; +} diff --git a/index.html b/index.html index 819ae69..0583ced 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ Radial Menu Test + diff --git a/js/RadialMenu.js b/js/RadialMenu.js index c01a105..1a765e6 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -30,11 +30,14 @@ class RadialMenu classes: { menuContainer: "menuHolder", // whole radial-menu container, created dynamically! menu: "menu", - menuCreate: "menu inner", // main menu [{menuCreate} inner] - menuCreateNested: "menu outer", // nested menu [{menuCreate} outer] + menuOpen: "open", // menu is visible [open] + menuClose: "close", // menu is not-visible [close] + menuCreateParent: "menu inner", // main menu [{menu} inner] + menuCreateNested: "menu outer", // nested menu [{menu} outer] itemSelected: "selected", // item, which is [selected] itemIcon: "icons", // item's icon itemSector: "sector", // item, which is active + itemSectorNested: "more", // item, which has nested items... [more] itemDummy: "dummy", // item, which is not active buttonCenter: "center", // button (close, back) ~ centered! }, @@ -47,7 +50,8 @@ class RadialMenu constructor(params) { - const defaultValues = this.defaultValues = RadialMenu._defaultValues; + const defaultValues = this.merge({}, RadialMenu._defaultValues); + this.defaultValues = defaultValues; this.uuid = this.generateUUID(); this.parent = params.parent || [];//TODO: refactor: ?this.attr = this.merge(defaultValues, params);? this.size = params.size || defaultValues.size; @@ -68,7 +72,7 @@ class RadialMenu ); this.ui = this.merge( defaultValues.ui, - params.nested || {} + params.ui || {} ); this.scale = 1;//TODO:?do we need different scale? this.holder = null; @@ -140,7 +144,7 @@ class RadialMenu onClickOutside(event, THIS) { const menu = document.getElementById(THIS.uuid); - if (!menu) + if (!menu || THIS.uuid !== menu.id) { return; } @@ -165,20 +169,23 @@ class RadialMenu THIS.close(); } - open() + open(x = undefined, y = undefined) { if (this.isOpen()) { return; } - this.currentMenu = this.createMenu(this.ui.classes.menuCreate, this.menuItems); + this.currentMenu = this.createMenu(this.ui.classes.menuCreateParent, this.menuItems); this.holder.appendChild(this.currentMenu); // wait DOM commands to apply and then set class to allow transition to take effect const THIS = this; this.postRunnable(function() { - THIS.currentMenu.setAttribute('class', THIS.ui.classes.menu); + THIS.currentMenu.setAttribute( + 'class', + [THIS.ui.classes.menu, THIS.ui.classes.menuOpen].join(' ') + ); if (THIS.closeOnClickOutside) { document.addEventListener('click', THIS.closeOnClickOutsideListener = function(event) @@ -187,6 +194,17 @@ class RadialMenu }); } }); + const menuContainer = document.getElementById(this.uuid); + menuContainer.classList.remove(this.ui.classes.menuClose); + menuContainer.classList.add(this.ui.classes.menuOpen); + if (x !== undefined) + { + menuContainer.style.left = (x - this.size / 2) + "px"; + } + if (y !== undefined) + { + menuContainer.style.top = (y - this.size / 2) + "px"; + } } close() @@ -203,7 +221,7 @@ class RadialMenu this.parentItems = []; const THIS = this; - this.setClassAndWaitForTransition(this.currentMenu, this.ui.classes.menuCreate) + this.setClassAndWaitForTransition(this.currentMenu, this.ui.classes.menuCreateParent) .then(function() { if (THIS.currentMenu !== null) @@ -215,6 +233,9 @@ class RadialMenu { document.removeEventListener('click', THIS.closeOnClickOutsideListener); } + const menuContainer = document.getElementById(THIS.uuid); + menuContainer.classList.remove(THIS.ui.classes.menuOpen); + menuContainer.classList.add(THIS.ui.classes.menuClose); }) ; } @@ -248,7 +269,7 @@ class RadialMenu { this.parentMenu.push(this.currentMenu); this.parentItems.push(this.levelItems); - this.currentMenu = this.createMenu(this.ui.classes.menuCreate, item.items, item); + this.currentMenu = this.createMenu(this.ui.classes.menuCreateParent, item.items, item); this.holder.appendChild(this.currentMenu); // wait DOM commands to apply and then set class to allow transition to take effect @@ -256,16 +277,21 @@ class RadialMenu this.postRunnable(function() { THIS.getParentMenu().setAttribute('class', THIS.ui.classes.menuCreateNested); - THIS.currentMenu.setAttribute('class', THIS.ui.classes.menu); + THIS.currentMenu.setAttribute( + 'class', + [THIS.ui.classes.menu, THIS.ui.classes.menuOpen].join(' ') + ); }); } returnToParentMenu() { - this.getParentMenu().setAttribute('class', this.ui.classes.menu); - + this.getParentMenu().setAttribute( + 'class', + [this.ui.classes.menu, this.ui.classes.menuOpen].join(' ') + ); const THIS = this; - this.setClassAndWaitForTransition(this.currentMenu, this.ui.classes.menuCreate) + this.setClassAndWaitForTransition(this.currentMenu, this.ui.classes.menuCreateParent) .then(function() { THIS.currentMenu.remove(); @@ -364,7 +390,6 @@ class RadialMenu { const THIS = this; this.levelItems = levelItems; - this.sectorCount = Math.max(this.levelItems.length, this.defaultValues.minSectors); this.scale = this.calculateScale(); @@ -536,14 +561,16 @@ class RadialMenu { const itemToSelect = items[0]; const selectedNode = this.getSelectedNode(); + let itemClasses = [this.ui.classes.itemSector, this.ui.classes.itemSelected]; if (selectedNode) { selectedNode.setAttribute('class', this.ui.classes.itemSector); } - itemToSelect.setAttribute( - 'class', - [this.ui.classes.itemSector, this.ui.classes.itemSelected].join(' ') - ); + if (itemToSelect.items && itemToSelect.items.length > 0) + { + itemClasses.push(this.ui.classes.itemSectorNested); + } + itemToSelect.setAttribute('class', itemClasses.join(' ')); } } } @@ -579,14 +606,16 @@ class RadialMenu if (item) { - g.setAttribute('class', this.ui.classes.itemSector); + let itemClasses = [this.ui.classes.itemSector]; if (item.selected && item.selected === true) { - g.setAttribute( - 'class', - [this.ui.classes.itemSector, this.ui.classes.itemSelected].join(' ') - ); + itemClasses.push(this.ui.classes.itemSelected); } + if (item.items && item.items.length > 0) + { + itemClasses.push(this.ui.classes.itemSectorNested); + } + g.setAttribute('class', itemClasses.join(' ')); g.setAttribute('data-id', item.id); g.setAttribute('data-index', index); @@ -617,7 +646,6 @@ class RadialMenu } g.appendChild(use); } - } else { diff --git a/js/main.js b/js/main.js index a34164d..bb97e40 100644 --- a/js/main.js +++ b/js/main.js @@ -107,7 +107,7 @@ window.onload = function () parent: document.body, size: 400, closeOnClick: true, - // closeOnClickOutside: false, + closeOnClickOutside: false, menuItems: menuItems, onClick: function(item) { @@ -123,13 +123,69 @@ window.onload = function () { svgMenu.close(); }); + const svgMenu2 = new RadialMenu({ + parent: document.body, + size: 400, + menuItems: [ + { + id : 'walk2', + title: 'Walk', + icon: '#walk' + }, + { + id : 'more2', + title: 'More...', + icon: '#more', + items: [ + { + id : 'eat2', + title: 'Eat', + icon: '#eat' + }, + { + id : 'sleep2', + title: 'Sleep', + icon: '#sleep', + selected: true + }, + { + id: 'more3', + title: 'More...', + icon: '#more', + items: [ + { + id: 'shower2', + title: 'Take Shower', + icon: '#shower' + }, + { + id: 'workout2', + icon: '#workout', + title: 'Work Out' + }, + ] + } + ] + } + ], + ui: { + classes: { + menuContainer: "menuHolder2", + menu: "menu2", + menuCreate: "menu2 inner", + menuCreateNested: "menu2 outer", + menuOpen: "open2", + menuClose: "close2" + } + } + }); document.addEventListener('contextmenu', function(event) { event.preventDefault(); - if (svgMenu.isOpen()) + if (svgMenu2.isOpen()) { return; } - svgMenu.open(); + svgMenu2.open(event.x, event.y); }); }; From 55e8b781ef25784e6d457bcb6dbe9d444e3a2f43 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sat, 8 Jul 2023 02:40:49 +0200 Subject: [PATCH 10/16] refactor: RadialMenu.js: fix nested center(return-button) inc. show icon-only! --- README.md | 13 ++++++------- js/RadialMenu.js | 32 +++++++++++++++++--------------- js/main.js | 4 ++++ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 4319f4c..1eb678e 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,7 @@ var svgMenu = new RadialMenu({ { closeOnClick: true, // boolean; will menu.close(), after item is selected. [default: true && menu.onClickFallback();] closeOnClickOutside: true, // true or function(); it will menu.close(), when item is not selected and click is outside of menu. [default: true] - - nested: { // nested ~ inner-menu behavior - icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) - title: true // show nested title? - }, - + ui: { // ui customization classes: { menuContainer: "menuHolder", // whole radial-menu container, created dynamically! @@ -88,7 +83,11 @@ var svgMenu = new RadialMenu({ icons: { back: {title: "Back", icon: "#return"}, close: {title: "Close", icon: "#close"}, - } + }, + nested: { // nested ~ inner-menu behavior + icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) + title: true // show nested title? + }, } } ``` diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 1a765e6..722036f 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -20,12 +20,6 @@ class RadialMenu radiusSectorSpace: 0.06, closeOnClick: true, // true or function(); will close(); after item is selected. [default: onClickFallback()] closeOnClickOutside: true, // true or function(); it will close(); when item is not selected and click is outside of menu. [default: true] - nested: { - icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) - title: true, // show nested title? - //TODO:?it can show (number of nested menu)? - //TODO:?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? - }, ui: { classes: { menuContainer: "menuHolder", // whole radial-menu container, created dynamically! @@ -44,6 +38,12 @@ class RadialMenu icons: { back: {title: "Back", icon: "#return"}, close: {title: "Close", icon: "#close"}, + }, + nested: { + icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) + title: true, // show nested title? + //TODO:?it can show (number of nested menu)? + //TODO:?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? } } } @@ -57,8 +57,14 @@ class RadialMenu this.size = params.size || defaultValues.size; this.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; this.radius = params.radius ? params.radius : defaultValues.radius.value; - this.innerRadius = params.innerRadius ? params.innerRadius : this.radius * defaultValues.radius.multiInnerRadius; - this.sectorSpace = params.sectorSpace ? params.sectorSpace : this.radius * defaultValues.radius.multiSectorSpace; + this.innerRadius = params.innerRadius + ? params.innerRadius + : this.radius * (params.multiInnerRadius ? params.multiInnerRadius : defaultValues.radius.multiInnerRadius) + ; + this.sectorSpace = params.sectorSpace + ? params.sectorSpace + : this.radius * (params.multiSectorSpace ? params.multiSectorSpace : defaultValues.radius.multiSectorSpace) + ; this.sectorCount = Math.max(this.menuItems.length, defaultValues.minSectors); this.closeOnClick = params.closeOnClick !== undefined ? !!params.closeOnClick : defaultValues.closeOnClick; this.closeOnClickOutside = (params.closeOnClickOutside !== undefined @@ -66,10 +72,6 @@ class RadialMenu : defaultValues.closeOnClickOutside ); this.onClick = params.onClick || this.onClickFallback; - this.nested = this.merge( - defaultValues.nested, - params.nested || {} - ); this.ui = this.merge( defaultValues.ui, params.ui || {} @@ -347,7 +349,7 @@ class RadialMenu const centerCircle = this.createCircle(0, 0, this.innerRadius - this.sectorSpace / 3); g.appendChild(centerCircle); - if (nested && this.nested.title) + if (nested && this.ui.nested.title) { const text = this.createText(0, +size, nested.title); g.appendChild(text); @@ -355,9 +357,9 @@ class RadialMenu if (icon) { - if (nested && this.nested.icon) + if (nested && this.ui.nested.icon) { - icon = (this.nested.icon === true ? nested.icon : this.nested.icon); + icon = (this.ui.nested.icon === true ? nested.icon : this.ui.nested.icon); } const use = this.createUseTag(0, 0, icon); use.setAttribute('width', size); diff --git a/js/main.js b/js/main.js index bb97e40..f23e2a6 100644 --- a/js/main.js +++ b/js/main.js @@ -168,6 +168,7 @@ window.onload = function () ] } ], + multiInnerRadius: 0.2, ui: { classes: { menuContainer: "menuHolder2", @@ -176,6 +177,9 @@ window.onload = function () menuCreateNested: "menu2 outer", menuOpen: "open2", menuClose: "close2" + }, + nested: { + title: false } } }); From 69066a3924f1d23f254c3a633164e527d7d39f63 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sat, 8 Jul 2023 03:08:28 +0200 Subject: [PATCH 11/16] refactor: RadialMenu.js: fix navigation by{wheel, keys} inc. custom-keys --- README.md | 8 +++++ js/RadialMenu.js | 93 +++++++++++++++++++++++++++++------------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 1eb678e..ea99a39 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,14 @@ var svgMenu = new RadialMenu({ icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) title: true // show nested title? }, + moveByWheel: true, // navigation by mouse-wheel. [default: true] + moveByKeys: { // navigation by keys. [default: true] + enabled: true, + back: ["escape", "backspace"], + select: ["enter"], + forward: ["arrowRight", "arrowUp"], + backward: ["arrowLeft", "arrowDown"] + } } } ``` diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 722036f..8420fd1 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -44,6 +44,14 @@ class RadialMenu title: true, // show nested title? //TODO:?it can show (number of nested menu)? //TODO:?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? + }, + moveByWheel: true, // navigation by mouse-wheel. [default: true] + moveByKeys: { // navigation by keys. [default: true] + enabled: true, + back: ["escape", "backspace"], + select: ["enter"], + forward: ["arrowRight", "arrowUp"], + backward: ["arrowLeft", "arrowDown"] } } } @@ -53,7 +61,7 @@ class RadialMenu const defaultValues = this.merge({}, RadialMenu._defaultValues); this.defaultValues = defaultValues; this.uuid = this.generateUUID(); - this.parent = params.parent || [];//TODO: refactor: ?this.attr = this.merge(defaultValues, params);? + this.parent = params.parent || []; this.size = params.size || defaultValues.size; this.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; this.radius = params.radius ? params.radius : defaultValues.radius.value; @@ -76,7 +84,7 @@ class RadialMenu defaultValues.ui, params.ui || {} ); - this.scale = 1;//TODO:?do we need different scale? + this.scale = 1; this.holder = null; this.parentMenu = []; this.parentItems = []; @@ -86,8 +94,14 @@ class RadialMenu this.addIconSymbols();//TODO:?iconSymbolsFactory? this.currentMenu = null; - document.addEventListener('wheel', this.onMouseWheel.bind(this));//TODO:?enable/disable? - document.addEventListener('keydown', this.onKeyDown.bind(this));//TODO:?enable/disable? + if (this.ui.moveByWheel) + { + document.addEventListener('wheel', this.onMouseWheel.bind(this)); + } + if (this.ui.moveByKeys) + { + document.addEventListener('keydown', this.onKeyDown.bind(this)); + } } generateUUID() @@ -364,7 +378,10 @@ class RadialMenu const use = this.createUseTag(0, 0, icon); use.setAttribute('width', size); use.setAttribute('height', size); - use.setAttribute('transform', 'translate(-' + this.numberToString(size / 2) + ',-' + this.numberToString(size / 2) + ')'); + use.setAttribute( + 'transform', + 'translate(-' + this.numberToString(size / 2) + ',-' + this.numberToString(size / 2) + ')' + ); g.appendChild(use); } svg.appendChild(g); @@ -489,39 +506,43 @@ class RadialMenu { return; } - //TODO:?enable keys?which one? - switch (event.key) + if (this.isKeyDown(event, this.ui.moveByKeys.back)) { - case 'Escape': - case 'Backspace': - { - this.handleCenterClick(); - event.preventDefault(); - break; - } - case 'Enter': - { - this.handleClick(); - event.preventDefault(); - break; - } - case 'ArrowRight': - case 'ArrowUp': - { - this.selectDelta(+1); - event.preventDefault(); - break; - } - case 'ArrowLeft': - case 'ArrowDown': + this.handleCenterClick(); + event.preventDefault(); + return; + } + if (this.isKeyDown(event, this.ui.moveByKeys.select)) + { + this.handleClick(); + event.preventDefault(); + return; + } + if (this.isKeyDown(event, this.ui.moveByKeys.forward)) + { + this.selectDelta(+1); + event.preventDefault(); + return; + } + if (this.isKeyDown(event, this.ui.moveByKeys.backward)) + { + this.selectDelta(-1); + event.preventDefault(); + return; + } + } + + isKeyDown(event, keySet) + { + const keyId = event.key.toLowerCase(); + for(let i = 0; i < keySet.length; i++) + { + if (keyId === keySet[i].toLowerCase()) { - this.selectDelta(-1); - event.preventDefault(); - break; + return true; } - default: - break; } + return false; } onMouseWheel(event) @@ -775,14 +796,14 @@ class RadialMenu else if (n) { - let r = (+n).toFixed(5);//TODO:?magicNumber? + let r = (+n).toFixed(5); if (r.match(/\./)) { r = r.replace(/\.?0+$/, ''); } return r; } - // TODO:?if is NaN? what to do? + return ""; } resolveLoopIndex(index, length) From a4d562aaa6ca18dd95f64fb5dc986e1efe14f2c4 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sat, 8 Jul 2023 16:13:52 +0200 Subject: [PATCH 12/16] polishing... --- README.md | 23 ++-- css/RadialMenuCustom.css | 7 +- index.html | 138 +++++--------------- js/RadialMenu.js | 173 +++++++++++++++---------- js/main.js | 266 ++++++++++++--------------------------- 5 files changed, 236 insertions(+), 371 deletions(-) diff --git a/README.md b/README.md index ea99a39..c432e57 100644 --- a/README.md +++ b/README.md @@ -66,23 +66,24 @@ var svgMenu = new RadialMenu({ closeOnClickOutside: true, // true or function(); it will menu.close(), when item is not selected and click is outside of menu. [default: true] ui: { // ui customization + fontSize: "38%", // text font-size of elements inside {menuContainer}, eg: text in {itemSector} [38%] classes: { - menuContainer: "menuHolder", // whole radial-menu container, created dynamically! - menu: "menu", + menuContainer: "menuHolder", // whole radial-menu container, created dynamically! see: {params.parent} + menuCreate: "menu", + menuCreateParent: "inner", // main menu [{menuCreate} inner] + menuCreateNested: "outer", // nested menu [{menuCreate} outer] menuOpen: "open", // menu is visible [open] menuClose: "close", // menu is not-visible [close] - menuCreateParent: "menu inner", // main menu [{menu} inner] - menuCreateNested: "menu outer", // nested menu [{menu} outer] - itemSelected: "selected", // item, which is [selected] - itemIcon: "icons", // item's icon - itemSector: "sector", // item, which is active + itemSectorActive: "sector", // item, which is active and can be selected itemSectorNested: "more", // item, which has nested items... [more] - itemDummy: "dummy", // item, which is not active - buttonCenter: "center", // button (close, back) ~ centered! + itemSectorDisabled: "dummy", // item, which is not-active/disabled [dummy] + itemSelected: "selected", // item, which is selected [selected] + closeBackButton: "center", // centered {close} or {back} button [centered] + iconsContainer: "icons", // item's icon container [icons] }, - icons: { - back: {title: "Back", icon: "#return"}, + item: { // pre-defined items: {close} and {back} in similar way like: {menuItems} close: {title: "Close", icon: "#close"}, + back: {title: "Back", icon: "#return"}, }, nested: { // nested ~ inner-menu behavior icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) diff --git a/css/RadialMenuCustom.css b/css/RadialMenuCustom.css index f9221a6..e4117da 100644 --- a/css/RadialMenuCustom.css +++ b/css/RadialMenuCustom.css @@ -1,3 +1,4 @@ +@import url("https://fonts.googleapis.com/css?family=Bitter&subset=latin"); div.menuHolder2 { /* menu-container */ user-select: none; @@ -8,6 +9,8 @@ div.menuHolder2 top: 0px; left: 0px; z-index: -1; + + font-family: Bitter; } div.menuHolder2.open2 @@ -47,14 +50,14 @@ svg.menu2.open2 border-radius: 50%; } -svg.menu2.inner +svg.menu2.inner2 { transform: scale(0.66) rotate(-10deg); opacity: 0; visibility: hidden; } -svg.menu2.outer +svg.menu2.outer2 { transform: scale(1.5) rotate(10deg); opacity: 0; diff --git a/index.html b/index.html index 0583ced..8d16dd2 100644 --- a/index.html +++ b/index.html @@ -29,22 +29,17 @@ - + - + - + @@ -54,17 +49,8 @@ - - + + @@ -73,37 +59,18 @@ - + - + - + @@ -127,22 +94,17 @@ - + - + - + @@ -151,22 +113,12 @@ - + - + @@ -174,65 +126,37 @@ - - - - - + + + + + - - - + -

Radial Menu Test

+ +

Radial Menu Test

- - You can try also "Context Menu" - → right-mouse-click. + + You can also try + "Context Menu" + → right-mouse-click(open) + → left-mouse-click(select or close).

- \ No newline at end of file + diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 8420fd1..1494bae 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -1,15 +1,15 @@ 'use strict'; /** - * Radial menu in pure JavaScript, HTML and SVG + * Radial menu in pure JavaScript, HTML and SVG. * License: MIT + * Copyright (c) 2019 Alexey Nesterenko * -- https://github.com/axln/radial-menu-js * -- https://github.com/j3nda/radial-menu-js */ class RadialMenu { static _defaultValues = { - size: 100, // aka DEFAULT_SIZE minSectors: 6, // aka MIN_SECTORS radius: { value: 50, @@ -21,29 +21,31 @@ class RadialMenu closeOnClick: true, // true or function(); will close(); after item is selected. [default: onClickFallback()] closeOnClickOutside: true, // true or function(); it will close(); when item is not selected and click is outside of menu. [default: true] ui: { + fontSize: "38%", // text font-size of elements inside {menuContainer}, eg: text in {itemSector} [38%] classes: { - menuContainer: "menuHolder", // whole radial-menu container, created dynamically! - menu: "menu", + menuContainer: "menuHolder", // whole radial-menu container, created dynamically! see: {params.parent} + menuCreate: "menu", + menuCreateParent: "inner", // main menu [{menuCreate} inner] + menuCreateNested: "outer", // nested menu [{menuCreate} outer] menuOpen: "open", // menu is visible [open] menuClose: "close", // menu is not-visible [close] - menuCreateParent: "menu inner", // main menu [{menu} inner] - menuCreateNested: "menu outer", // nested menu [{menu} outer] - itemSelected: "selected", // item, which is [selected] - itemIcon: "icons", // item's icon - itemSector: "sector", // item, which is active + itemSectorActive: "sector", // item, which is active and can be selected itemSectorNested: "more", // item, which has nested items... [more] - itemDummy: "dummy", // item, which is not active - buttonCenter: "center", // button (close, back) ~ centered! + itemSectorDisabled: "dummy", // item, which is not-active/disabled [dummy] + itemSelected: "selected", // item, which is selected [selected] + closeBackButton: "center", // centered {close} or {back} button [centered] + iconsContainer: "icons", // item's icon container [icons] }, - icons: { - back: {title: "Back", icon: "#return"}, + item: { // pre-defined items: {close} and {back} in similar way like: {menuItems} close: {title: "Close", icon: "#close"}, + back: {title: "Back", icon: "#return"}, + // TODO: [ui/item] fontColor, textColor, ?position? }, nested: { icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) title: true, // show nested title? - //TODO:?it can show (number of nested menu)? - //TODO:?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? + // TODO: [ui] ?it can show (number of nested menu)? + // TODO: [ui] ?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? }, moveByWheel: true, // navigation by mouse-wheel. [default: true] moveByKeys: { // navigation by keys. [default: true] @@ -56,14 +58,20 @@ class RadialMenu } } - constructor(params) + /** + * create RadialMenu + * @param menuItems array of items, eg: [{id: "one", icon: "One"}, {id: "two", title: "two"}, {id: "more", icon: "more", title: "More...", items: [...]}] + * @param sizeInPixels + * @param params custom parameters to override {...} + */ + constructor(menuItems, sizeInPixels, params) { const defaultValues = this.merge({}, RadialMenu._defaultValues); this.defaultValues = defaultValues; this.uuid = this.generateUUID(); - this.parent = params.parent || []; - this.size = params.size || defaultValues.size; - this.menuItems = params.menuItems ? params.menuItems : [{id: 'one', title: 'One'}, {id: 'two', title: 'Two'}]; + this.parent = params.parent || document.body; + this.size = sizeInPixels; + this.menuItems = menuItems; this.radius = params.radius ? params.radius : defaultValues.radius.value; this.innerRadius = params.innerRadius ? params.innerRadius @@ -104,10 +112,14 @@ class RadialMenu } } + /** + * generate UUID + * -- https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid + * -- https://stackoverflow.com/a/8809472/6130410 + * @returns {string} + */ generateUUID() { - // -- https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid - // License: Public Domain / MIT let d1 = new Date().getTime();//Timestamp let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0; // Time in microseconds since page-load or 0 if unsupported return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' @@ -138,26 +150,17 @@ class RadialMenu throw "onClick: function(item) {...}; // must be defined by params or default!"; } - onClickCallback(item) - { - if (this.closeOnClick) - { - this.close(); - } - if (this.onClick && this.onClick instanceof Function) - { - this.onClick(item); - return; - } - this.onClickFallback(item); - } - + /** return true if menu is visible, otherwise returns false. */ isOpen() { return (this.currentMenu !== null); } - onClickOutside(event, THIS) + /** + * handle mouse-click or tap, when its outside of menu. + * -- https://www.w3docs.com/snippets/javascript/how-to-detect-a-click-outside-an-element.html + */ + handleClickOutside(event, THIS) { const menu = document.getElementById(THIS.uuid); if (!menu || THIS.uuid !== menu.id) @@ -191,7 +194,10 @@ class RadialMenu { return; } - this.currentMenu = this.createMenu(this.ui.classes.menuCreateParent, this.menuItems); + this.currentMenu = this.createMenu( + [this.ui.classes.menuCreate, this.ui.classes.menuCreateParent].join(' '), + this.menuItems + ); this.holder.appendChild(this.currentMenu); // wait DOM commands to apply and then set class to allow transition to take effect @@ -200,13 +206,13 @@ class RadialMenu { THIS.currentMenu.setAttribute( 'class', - [THIS.ui.classes.menu, THIS.ui.classes.menuOpen].join(' ') + [THIS.ui.classes.menuCreate, THIS.ui.classes.menuOpen].join(' ') ); if (THIS.closeOnClickOutside) { document.addEventListener('click', THIS.closeOnClickOutsideListener = function(event) { - THIS.onClickOutside(event, THIS); + THIS.handleClickOutside(event, THIS); }); } }); @@ -237,7 +243,10 @@ class RadialMenu this.parentItems = []; const THIS = this; - this.setClassAndWaitForTransition(this.currentMenu, this.ui.classes.menuCreateParent) + this.setClassAndWaitForTransition( + this.currentMenu, + [this.ui.classes.menuCreate, this.ui.classes.menuCreateParent].join(' ') + ) .then(function() { if (THIS.currentMenu !== null) @@ -256,6 +265,7 @@ class RadialMenu ; } + /** default functionality as onClick(): function, which MUST be overridden through params! */ onClick(item) { return item; @@ -285,17 +295,24 @@ class RadialMenu { this.parentMenu.push(this.currentMenu); this.parentItems.push(this.levelItems); - this.currentMenu = this.createMenu(this.ui.classes.menuCreateParent, item.items, item); + this.currentMenu = this.createMenu( + [this.ui.classes.menuCreate, this.ui.classes.menuCreateParent].join(' '), + item.items, + item + ); this.holder.appendChild(this.currentMenu); // wait DOM commands to apply and then set class to allow transition to take effect const THIS = this; this.postRunnable(function() { - THIS.getParentMenu().setAttribute('class', THIS.ui.classes.menuCreateNested); + THIS.getParentMenu().setAttribute( + 'class', + [THIS.ui.classes.menuCreate, THIS.ui.classes.menuCreateNested].join(' ') + ); THIS.currentMenu.setAttribute( 'class', - [THIS.ui.classes.menu, THIS.ui.classes.menuOpen].join(' ') + [THIS.ui.classes.menuCreate, THIS.ui.classes.menuOpen].join(' ') ); }); } @@ -304,10 +321,13 @@ class RadialMenu { this.getParentMenu().setAttribute( 'class', - [this.ui.classes.menu, this.ui.classes.menuOpen].join(' ') + [this.ui.classes.menuCreate, this.ui.classes.menuOpen].join(' ') ); const THIS = this; - this.setClassAndWaitForTransition(this.currentMenu, this.ui.classes.menuCreateParent) + this.setClassAndWaitForTransition( + this.currentMenu, + [this.ui.classes.menuCreate, this.ui.classes.menuCreateParent].join(' ') + ) .then(function() { THIS.currentMenu.remove(); @@ -317,6 +337,7 @@ class RadialMenu ; } + /** handle click inside menu, eg: choosing item. */ handleClick() { const selectedIndex = this.getSelectedIndex(); @@ -328,10 +349,20 @@ class RadialMenu this.showNestedMenu(item); return; } - this.onClickCallback(item); + if (this.closeOnClick) + { + this.close(); + } + if (this.onClick && this.onClick instanceof Function) + { + this.onClick(item); + return; + } + this.onClickFallback(item); } } + /** handle click in the center, eg: close or back-button */ handleCenterClick() { if (this.parentItems.length > 0) @@ -353,19 +384,27 @@ class RadialMenu // RadialMenu.prototype.handleCenterClick = function () {...} } - createCenter(svg, title, icon, size, nested = null) + /** + * create center button, eg: close or back-button + * @param svg parentSvgElement + * @param title + * @param icon + * @param size + * @param nested am i nested? yes, here is my parentItem + */ + createCenter(svg, title, icon, size, nested = undefined) { size = size || 8;//TODO:?magicNumber?default value? const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - g.setAttribute('class', this.ui.classes.buttonCenter); + g.setAttribute('class', this.ui.classes.closeBackButton); const centerCircle = this.createCircle(0, 0, this.innerRadius - this.sectorSpace / 3); g.appendChild(centerCircle); if (nested && this.ui.nested.title) { - const text = this.createText(0, +size, nested.title); + const text = this.createItemText(0, +size, nested); g.appendChild(text); } @@ -405,6 +444,7 @@ class RadialMenu return -1; } + /** create all items for currently visible menu, eg: main menu or nested one */ createMenu(classValue, levelItems, nested) { const THIS = this; @@ -437,11 +477,11 @@ class RadialMenu if (nested) { - this.createCenter(svg, this.ui.icons.back.title, this.ui.icons.back.icon, 8, nested); //TODO:??magicNumber?? + this.createCenter(svg, this.ui.item.back.title, this.ui.item.back.icon, 8, nested); //TODO:??magicNumber?? } else { - this.createCenter(svg, this.ui.icons.close.title, this.ui.icons.close.icon, 7);//TODO:??magicNumber?? + this.createCenter(svg, this.ui.item.close.title, this.ui.item.close.icon, 7);//TODO:??magicNumber?? } svg.addEventListener('mousedown', function(event) @@ -449,7 +489,7 @@ class RadialMenu const classNames = event.target.parentNode.getAttribute('class').split(' '); for (let i = 0; i < classNames.length; i++) { - if (classNames[i] === THIS.ui.classes.itemSector) + if (classNames[i] === THIS.ui.classes.itemSectorActive) { const index = parseInt(event.target.parentNode.getAttribute('data-index')); if (!isNaN(index)) @@ -465,12 +505,12 @@ class RadialMenu const classNames = event.target.parentNode.getAttribute('class').split(' '); for (let i = 0; i < classNames.length; i++) { - if (classNames[i] === THIS.ui.classes.itemSector) + if (classNames[i] === THIS.ui.classes.itemSectorActive) { THIS.handleClick(); break; } - if (classNames[i] === THIS.ui.classes.buttonCenter) + if (classNames[i] === THIS.ui.classes.closeBackButton) { THIS.handleCenterClick(); break; @@ -480,6 +520,7 @@ class RadialMenu return svg; } + /** setSelectedIndex() based on +/- indexDelta, eg: onMouseWheel */ selectDelta(indexDelta) { let selectedIndex = this.getSelectedIndex(); @@ -584,10 +625,10 @@ class RadialMenu { const itemToSelect = items[0]; const selectedNode = this.getSelectedNode(); - let itemClasses = [this.ui.classes.itemSector, this.ui.classes.itemSelected]; + let itemClasses = [this.ui.classes.itemSectorActive, this.ui.classes.itemSelected]; if (selectedNode) { - selectedNode.setAttribute('class', this.ui.classes.itemSector); + selectedNode.setAttribute('class', this.ui.classes.itemSectorActive); } if (itemToSelect.items && itemToSelect.items.length > 0) { @@ -629,7 +670,7 @@ class RadialMenu if (item) { - let itemClasses = [this.ui.classes.itemSector]; + let itemClasses = [this.ui.classes.itemSectorActive]; if (item.selected && item.selected === true) { itemClasses.push(this.ui.classes.itemSelected); @@ -644,7 +685,7 @@ class RadialMenu if (item.title) { - const text = this.createText(centerPoint.x, centerPoint.y, item.title); + const text = this.createItemText(centerPoint.x, centerPoint.y, item); if (item.icon) { text.setAttribute('transform', 'translate(0,8)'); @@ -672,7 +713,7 @@ class RadialMenu } else { - g.setAttribute('class', this.ui.classes.itemDummy); + g.setAttribute('class', this.ui.classes.itemSectorDisabled); } svg.appendChild(g); }; @@ -696,15 +737,15 @@ class RadialMenu return path; } - createText(x, y, title) + createItemText(x, y, item) { const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); text.setAttribute('text-anchor', 'middle'); text.setAttribute('x', this.numberToString(x)); text.setAttribute('y', this.numberToString(y)); - text.setAttribute('font-size', '38%');//TODO:?fontSize? - text.innerHTML = title; + text.setAttribute('font-size', item.fontSize ?? this.ui.fontSize); + text.innerHTML = item.title; return text; } @@ -740,7 +781,7 @@ class RadialMenu addIconSymbols() { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('class', this.ui.classes.itemIcon); + svg.setAttribute('class', this.ui.classes.iconsContainer); // return const returnSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); @@ -771,7 +812,7 @@ class RadialMenu closeSymbol.appendChild(closePath); svg.appendChild(closeSymbol); - this.holder.appendChild(svg); + this.holder.appendChild(svg);//TODO:refactor:return svg; rename createIcon(type); } getDegreePosition(angleDeg, length) @@ -851,6 +892,10 @@ class RadialMenu setTimeout(fn, timeoutMs); } + /** + * return true if its object, otherwise returns false. + * (part of merge() functionality) + */ isObject(item) { return (item && typeof item === 'object' && !Array.isArray(item)); diff --git a/js/main.js b/js/main.js index f23e2a6..d4c06bc 100644 --- a/js/main.js +++ b/js/main.js @@ -1,195 +1,87 @@ 'use strict'; -var menuItems = [ - { - id : 'walk', - title: 'Walk', - icon: '#walk' - }, - { - id : 'run', - title: 'Run', - icon: '#run' - }, - { - id : 'drive', - title: 'Drive', - icon: '#drive', - selected: true - }, - { - id : 'figth', - title: 'Fight', - icon: '#fight', - }, - { - id : 'more', - title: 'More...', - icon: '#more', - items: [ - { - id : 'eat', - title: 'Eat', - icon: '#eat' - }, - { - id : 'sleep', - title: 'Sleep', - icon: '#sleep', - selected: true - }, - { - id : 'shower', - title: 'Take Shower', - icon: '#shower' - }, - { - id : 'workout', - icon : '#workout', - title: 'Work Out' - } - ] - }, - { - id: 'weapon', - title: 'Weapon...', - icon: '#weapon', - items: [ - { - id: 'firearm', - icon: '#firearm', - title: 'Firearm...', - items: [ - { - id: 'glock', - title: 'Glock 22' - }, - { - id: 'beretta', - title: 'Beretta M9' - }, - { - id: 'tt', - title: 'TT' - }, - { - id: 'm16', - title: 'M16 A2' - }, - { - id: 'ak47', - title: 'AK 47' - } - ] - }, - { - id: 'knife', - icon: '#knife', - title: 'Knife' - }, - { - id: 'machete', - icon: '#machete', - title: 'Machete' - }, { - id: 'grenade', - icon: '#grenade', - title: 'Grenade' - } - ] - } +const menuItems = [ + {id: 'walk', title: 'Walk', icon: '#walk'}, + {id: 'run', title: 'Run', icon: '#run'}, + {id: 'drive', title: 'Drive', icon: '#drive', selected: true}, // FYI: pre-selected on 1st show! + {id: 'fight', title: 'Fight', icon: '#fight'}, + {id: 'more', title: 'More...', icon: '#more', items: [ + {id: 'eat', title: 'Eat', icon: '#eat'}, + {id: 'sleep', title: 'Sleep', icon: '#sleep', selected: true}, // FYI: pre-selected on 1st show! + {id: 'shower', title: 'Take Shower', icon: '#shower'}, + {id: 'workout', title: 'Work Out', icon: '#workout'}, + ]}, + {id: 'weapon',title: 'Weapon...',icon: '#weapon',items: [ + {id: 'firearm', icon: '#firearm', title: 'Firearm...', items: [ // FYI: all items without icon! only text! + {id: 'glock', title: 'Glock 22'}, + {id: 'beretta', title: 'Beretta M9'}, + {id: 'tt', title: 'TT'}, + {id: 'm16', title: 'M16 A2'}, + {id: 'ak47', title: 'AK 47'} + ]}, + {id: 'knife', icon: '#knife', title: 'Knife'}, + {id: 'machete', icon: '#machete', title: 'Machete'}, + {id: 'grenade', icon: '#grenade', title: 'Grenade'} + ]} ]; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// window.onload = function () { - const svgMenu = new RadialMenu({ - parent: document.body, - size: 400, - closeOnClick: true, - closeOnClickOutside: false, - menuItems: menuItems, - onClick: function(item) - { - console.log('You have clicked:', item.id, item.title); - console.log(item); - } - }); - document.getElementById('openMenu').addEventListener('click', function(event) - { - svgMenu.open(); - }); - document.getElementById('closeMenu').addEventListener('click', function(event) - { - svgMenu.close(); - }); - const svgMenu2 = new RadialMenu({ - parent: document.body, - size: 400, - menuItems: [ - { - id : 'walk2', - title: 'Walk', - icon: '#walk' - }, - { - id : 'more2', - title: 'More...', - icon: '#more', - items: [ - { - id : 'eat2', - title: 'Eat', - icon: '#eat' - }, - { - id : 'sleep2', - title: 'Sleep', - icon: '#sleep', - selected: true - }, - { - id: 'more3', - title: 'More...', - icon: '#more', - items: [ - { - id: 'shower2', - title: 'Take Shower', - icon: '#shower' - }, - { - id: 'workout2', - icon: '#workout', - title: 'Work Out' - }, - ] - } - ] - } - ], - multiInnerRadius: 0.2, - ui: { - classes: { - menuContainer: "menuHolder2", - menu: "menu2", - menuCreate: "menu2 inner", - menuCreateNested: "menu2 outer", - menuOpen: "open2", - menuClose: "close2" - }, - nested: { - title: false - } - } - }); - document.addEventListener('contextmenu', function(event) - { - event.preventDefault(); - if (svgMenu2.isOpen()) - { - return; - } - svgMenu2.open(event.x, event.y); - }); + const radialMenu = new RadialMenu(menuItems, 400, { + parent: document.body, + closeOnClick: false, + closeOnClickOutside: false, + onClick: function(item) + { + console.log('You have clicked:', item.id, item.title); + console.log(item); + } + }); + document.getElementById('openMenu').addEventListener('click', function(event) + { + radialMenu.open(); + }); + document.getElementById('closeMenu').addEventListener('click', function(event) + { + radialMenu.close(); + }); + const radialContextMenu = new RadialMenu(// 2nd RadialMenu with different {menuItems} + [ + {id: 'walk2', title: 'Walk', icon: '#walk'}, + {id: 'more2', title: 'More...', fontSize: "60%", items: [ // FYI: {more} without icon! only text! fontSize: 60% + {id: 'eat2', title: 'Eat', icon: '#eat'}, + {id: 'sleep2', title: 'Sleep', icon: '#sleep', selected: true}, + {id: 'more3', title: 'More >>', fontSize: "60%", items: [ // FYI: {more} without icon! only text! fontSize: 60% + {id: 'shower2', title: 'Take Shower', icon: '#shower'}, + {id: 'workout2', icon: '#workout', title: 'Work Out'}, + ]} + ]} + ], + 432, + { + multiInnerRadius: 0.2, + ui: { + classes: { + menuContainer: "menuHolder2", + menuCreate: "menu2", + menuCreateParent: "inner2", + menuCreateNested: "outer2", + menuOpen: "open2", + menuClose: "close2" + }, + nested: { + title: false + } + } + }); + document.addEventListener('contextmenu', function(event) + { // right-mouse(as context-menu) opened at position[x,y] of mouse-click + event.preventDefault(); + if (radialContextMenu.isOpen()) + { + return; + } + radialContextMenu.open(event.x, event.y); + }); }; From bb2a2b18ef2f069a829e73baa72b42f6859e5386 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sun, 9 Jul 2023 16:24:28 +0200 Subject: [PATCH 13/16] refactor: RadialMenu.js: {close, back} icons to be able to customize... --- js/RadialMenu.js | 149 +++++++++++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 70 deletions(-) diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 1494bae..9e35b52 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -37,8 +37,28 @@ class RadialMenu iconsContainer: "icons", // item's icon container [icons] }, item: { // pre-defined items: {close} and {back} in similar way like: {menuItems} - close: {title: "Close", icon: "#close"}, - back: {title: "Back", icon: "#return"}, + close: { + title: "Close", + icon: "#close", + symbol: { // default icon fallback... + id: "close", + viewBox: "0 0 41.756 41.756", + paths: [ + "M27.948,20.878L40.291,8.536c1.953-1.953,1.953-5.119,0-7.071c-1.951-1.952-5.119-1.952-7.07,0L20.878,13.809L8.535,1.465c-1.951-1.952-5.119-1.952-7.07,0c-1.953,1.953-1.953,5.119,0,7.071l12.342,12.342L1.465,33.22c-1.953,1.953-1.953,5.119,0,7.071C2.44,41.268,3.721,41.755,5,41.755c1.278,0,2.56-0.487,3.535-1.464l12.343-12.342l12.343,12.343c0.976,0.977,2.256,1.464,3.535,1.464s2.56-0.487,3.535-1.464c1.953-1.953,1.953-5.119,0-7.071L27.948,20.878z" + ] + } + }, + back: { + title: "Back", + icon: "#return", + symbol: { // default icon fallback... + id: "return", + viewBox: "0 0 489.394 489.394", + paths: [ + "M375.789,92.867H166.864l17.507-42.795c3.724-9.132,1-19.574-6.691-25.744c-7.701-6.166-18.538-6.508-26.639-0.879L9.574,121.71c-6.197,4.304-9.795,11.457-9.563,18.995c0.231,7.533,4.261,14.446,10.71,18.359l147.925,89.823c8.417,5.108,19.18,4.093,26.481-2.499c7.312-6.591,9.427-17.312,5.219-26.202l-19.443-41.132h204.886c15.119,0,27.418,12.536,27.418,27.654v149.852c0,15.118-12.299,27.19-27.418,27.19h-226.74c-20.226,0-36.623,16.396-36.623,36.622v12.942c0,20.228,16.397,36.624,36.623,36.624h226.74c62.642,0,113.604-50.732,113.604-113.379V206.709C489.395,144.062,438.431,92.867,375.789,92.867z" + ] + } + }, // TODO: [ui/item] fontColor, textColor, ?position? }, nested: { @@ -99,7 +119,13 @@ class RadialMenu this.levelItems = null; this.createMenuContainer(this.ui.classes.menuContainer); - this.addIconSymbols();//TODO:?iconSymbolsFactory? + + // default icons(close, back) + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('class', this.ui.classes.iconsContainer); + svg.appendChild(this.createItemSymbol(this.ui.item.close.symbol)); + svg.appendChild(this.createItemSymbol(this.ui.item.back.symbol)); + this.holder.appendChild(svg); this.currentMenu = null; if (this.ui.moveByWheel) @@ -386,44 +412,43 @@ class RadialMenu /** * create center button, eg: close or back-button - * @param svg parentSvgElement - * @param title - * @param icon + * @param item as {title: watawaka, icon: matafaka, etc...} * @param size * @param nested am i nested? yes, here is my parentItem */ - createCenter(svg, title, icon, size, nested = undefined) + createCloseBackButton(item, size, nested = undefined) { - size = size || 8;//TODO:?magicNumber?default value? + size = size || 8;//TODO:?magicNumber?default value?8? - const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - g.setAttribute('class', this.ui.classes.closeBackButton); + const group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + group.setAttribute('class', this.ui.classes.closeBackButton); const centerCircle = this.createCircle(0, 0, this.innerRadius - this.sectorSpace / 3); - g.appendChild(centerCircle); + group.appendChild(centerCircle); if (nested && this.ui.nested.title) { const text = this.createItemText(0, +size, nested); - g.appendChild(text); + group.appendChild(text); } - if (icon) + if (item.icon) { + let icon = item.icon; if (nested && this.ui.nested.icon) { icon = (this.ui.nested.icon === true ? nested.icon : this.ui.nested.icon); } - const use = this.createUseTag(0, 0, icon); + const use = this.createItemUse(0, 0, icon); use.setAttribute('width', size); use.setAttribute('height', size); use.setAttribute( 'transform', 'translate(-' + this.numberToString(size / 2) + ',-' + this.numberToString(size / 2) + ')' ); - g.appendChild(use); + group.appendChild(use); } - svg.appendChild(g); + return group; } getIndexOffset() @@ -472,16 +497,22 @@ class RadialMenu { item = this.levelItems[itemIndex]; } - this.appendSectorPath(startAngle, endAngle, svg, item, itemIndex); + svg.appendChild( + this.createItemSector(startAngle, endAngle, item, itemIndex) + ); } if (nested) { - this.createCenter(svg, this.ui.item.back.title, this.ui.item.back.icon, 8, nested); //TODO:??magicNumber?? + svg.appendChild( + this.createCloseBackButton(this.ui.item.back, 8, nested)//TODO:??magicNumber?8? + ); } else { - this.createCenter(svg, this.ui.item.close.title, this.ui.item.close.icon, 7);//TODO:??magicNumber?? + svg.appendChild( + this.createCloseBackButton(this.ui.item.close, 7)//TODO:??magicNumber?7? + ); } svg.addEventListener('mousedown', function(event) @@ -639,21 +670,21 @@ class RadialMenu } } - createUseTag(x, y, link) + createItemUse(x, y, link) { const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); use.setAttribute('x', this.numberToString(x)); use.setAttribute('y', this.numberToString(y)); - use.setAttribute('width', '10'); - use.setAttribute('height', '10'); - use.setAttribute('fill', 'white'); + use.setAttribute('width', '10');//TODO:??magicNumber?10? + use.setAttribute('height', '10');//TODO:??magicNumber?10? + use.setAttribute('fill', 'white');//TODO:??magicNumber?color? use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', link); return use; } - appendSectorPath(startAngleDeg, endAngleDeg, svg, item, index) + createItemSector(startAngleDeg, endAngleDeg, item, index) { const centerPoint = this.getSectorPosition(startAngleDeg, endAngleDeg); const translate = { @@ -661,12 +692,12 @@ class RadialMenu y: this.numberToString((1 - this.scale) * centerPoint.y), }; - const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - g.setAttribute('transform', 'translate(' + translate.x + ' ,' + translate.y + ') scale(' + this.scale + ')'); + const group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + group.setAttribute('transform', 'translate(' + translate.x + ' ,' + translate.y + ') scale(' + this.scale + ')'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('d', this.createSector(startAngleDeg, endAngleDeg)); - g.appendChild(path); + path.setAttribute('d', this.createItemSectorPath(startAngleDeg, endAngleDeg)); + group.appendChild(path); if (item) { @@ -679,9 +710,9 @@ class RadialMenu { itemClasses.push(this.ui.classes.itemSectorNested); } - g.setAttribute('class', itemClasses.join(' ')); - g.setAttribute('data-id', item.id); - g.setAttribute('data-index', index); + group.setAttribute('class', itemClasses.join(' ')); + group.setAttribute('data-id', item.id); + group.setAttribute('data-index', index); if (item.title) { @@ -694,12 +725,12 @@ class RadialMenu { text.setAttribute('transform', 'translate(0,2)'); } - g.appendChild(text); + group.appendChild(text); } if (item.icon) { - const use = this.createUseTag(centerPoint.x, centerPoint.y, item.icon); + const use = this.createItemUse(centerPoint.x, centerPoint.y, item.icon); if (item.title) { use.setAttribute('transform', 'translate(-5,-8)'); @@ -708,17 +739,17 @@ class RadialMenu { use.setAttribute('transform', 'translate(-5,-5)'); } - g.appendChild(use); + group.appendChild(use); } } else { - g.setAttribute('class', this.ui.classes.itemSectorDisabled); + group.setAttribute('class', this.ui.classes.itemSectorDisabled); } - svg.appendChild(g); + return group; }; - createSector(startAngleDeg, endAngleDeg) + createItemSectorPath(startAngleDeg, endAngleDeg) { const initPoint = this.getDegreePosition(startAngleDeg, this.radius); let path = 'M' + this.pointToString(initPoint); @@ -778,41 +809,19 @@ class RadialMenu ); } - addIconSymbols() + createItemSymbol(item) { - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('class', this.ui.classes.iconsContainer); + const symbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); + symbol.setAttribute('id', item.id); + symbol.setAttribute('viewBox', item.viewBox); - // return - const returnSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); - returnSymbol.setAttribute('id', 'return'); - returnSymbol.setAttribute('viewBox', '0 0 489.394 489.394'); - - const returnPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - returnPath.setAttribute('d', "M375.789,92.867H166.864l17.507-42.795c3.724-9.132,1-19.574-6.691-25.744c-7.701-6.166-18.538-6.508-26.639-0.879" + - "L9.574,121.71c-6.197,4.304-9.795,11.457-9.563,18.995c0.231,7.533,4.261,14.446,10.71,18.359l147.925,89.823" + - "c8.417,5.108,19.18,4.093,26.481-2.499c7.312-6.591,9.427-17.312,5.219-26.202l-19.443-41.132h204.886" + - "c15.119,0,27.418,12.536,27.418,27.654v149.852c0,15.118-12.299,27.19-27.418,27.19h-226.74c-20.226,0-36.623,16.396-36.623,36.622" + - "v12.942c0,20.228,16.397,36.624,36.623,36.624h226.74c62.642,0,113.604-50.732,113.604-113.379V206.709" + - "C489.395,144.062,438.431,92.867,375.789,92.867z"); - - returnSymbol.appendChild(returnPath); - svg.appendChild(returnSymbol); - - // close - const closeSymbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); - closeSymbol.setAttribute('id', 'close'); - closeSymbol.setAttribute('viewBox', '0 0 41.756 41.756'); - - const closePath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - closePath.setAttribute('d', "M27.948,20.878L40.291,8.536c1.953-1.953,1.953-5.119,0-7.071c-1.951-1.952-5.119-1.952-7.07,0L20.878,13.809L8.535,1.465" + - "c-1.951-1.952-5.119-1.952-7.07,0c-1.953,1.953-1.953,5.119,0,7.071l12.342,12.342L1.465,33.22c-1.953,1.953-1.953,5.119,0,7.071" + - "C2.44,41.268,3.721,41.755,5,41.755c1.278,0,2.56-0.487,3.535-1.464l12.343-12.342l12.343,12.343" + - "c0.976,0.977,2.256,1.464,3.535,1.464s2.56-0.487,3.535-1.464c1.953-1.953,1.953-5.119,0-7.071L27.948,20.878z"); - closeSymbol.appendChild(closePath); - - svg.appendChild(closeSymbol); - this.holder.appendChild(svg);//TODO:refactor:return svg; rename createIcon(type); + for(let i = 0; i < item.paths.length; i++) + { + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', item.paths[i]); + symbol.appendChild(path); + } + return symbol; } getDegreePosition(angleDeg, length) From 7226cf2d52c89beebdb66b0360cc16ea29b06388 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sun, 9 Jul 2023 16:35:48 +0200 Subject: [PATCH 14/16] polishing... --- README.md | 15 ++++++++--- css/RadialMenuCustom.css | 3 +-- js/RadialMenu.js | 58 +++++++++++++++++++++++----------------- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index c432e57..4f75c6a 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ var svgMenu = new RadialMenu({ menuCreateNested: "outer", // nested menu [{menuCreate} outer] menuOpen: "open", // menu is visible [open] menuClose: "close", // menu is not-visible [close] - itemSectorActive: "sector", // item, which is active and can be selected + itemSectorActive: "sector", // item, which is active and can be selected [sector] itemSectorNested: "more", // item, which has nested items... [more] itemSectorDisabled: "dummy", // item, which is not-active/disabled [dummy] itemSelected: "selected", // item, which is selected [selected] @@ -84,9 +84,15 @@ var svgMenu = new RadialMenu({ item: { // pre-defined items: {close} and {back} in similar way like: {menuItems} close: {title: "Close", icon: "#close"}, back: {title: "Back", icon: "#return"}, + // FYI: + // 1) if u want to change, eg: 'close' icon, just use item.close.icon = '#myIconId' + // 2) if u want to override default 'icon' generation, see: RadialMenu.defaultValues.ui.item.{close, back}.symbol + // 3) to change item's colors, etc use: CSS: + // svg.{menuCreate} > g.{itemSectorActive} > text, + // svg.{menuCreate} > g.{itemSectorActive} > use {...} }, nested: { // nested ~ inner-menu behavior - icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) + icon: "#return", // string(iconId:'#return') or true(for parentItem.icon) title: true // show nested title? }, moveByWheel: true, // navigation by mouse-wheel. [default: true] @@ -102,9 +108,10 @@ var svgMenu = new RadialMenu({ ``` ## License -MIT + +[MIT](LICENSE) ## Contributors -* [Alexey Nesterenko](https://github.com/axln) +* [Alexey Nesterenko](https://github.com/axln) * [Jan Smid](https://github.com/j3nda) diff --git a/css/RadialMenuCustom.css b/css/RadialMenuCustom.css index e4117da..1bb84ca 100644 --- a/css/RadialMenuCustom.css +++ b/css/RadialMenuCustom.css @@ -80,8 +80,7 @@ svg.menu2 > g.sector > use { cursor: pointer; fill: white; - - /*text-shadow: 1px 1px 0 #000000A0;*/ + text-shadow: 1px 1px 0 #000000A0; } svg.menu2 > g.sector:hover > path diff --git a/js/RadialMenu.js b/js/RadialMenu.js index 9e35b52..d3b201d 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -10,7 +10,7 @@ class RadialMenu { static _defaultValues = { - minSectors: 6, // aka MIN_SECTORS + minSectors: 6, radius: { value: 50, multiInnerRadius: 0.4, // multiplication for default.radius.value [or params.radius] @@ -60,9 +60,12 @@ class RadialMenu } }, // TODO: [ui/item] fontColor, textColor, ?position? + // to change item's colors, etc use: CSS: + // svg.{menuCreate} > g.{itemSectorActive} > text, + // svg.{menuCreate} > g.{itemSectorActive} > use {...} }, nested: { - icon: "#return", // string(iconName:'#return') or true(for parentItem.icon) + icon: "#return", // string(iconId:'#return') or true(for parentItem.icon) title: true, // show nested title? // TODO: [ui] ?it can show (number of nested menu)? // TODO: [ui] ?it can combine 'nested.icon' with '#return' icon ~ bestFitForSizes? @@ -87,6 +90,7 @@ class RadialMenu constructor(menuItems, sizeInPixels, params) { const defaultValues = this.merge({}, RadialMenu._defaultValues); + this.defaultValues = defaultValues; this.uuid = this.generateUUID(); this.parent = params.parent || document.body; @@ -118,13 +122,16 @@ class RadialMenu this.parentItems = []; this.levelItems = null; - this.createMenuContainer(this.ui.classes.menuContainer); + // menu container ~ this.holder + this.parent.appendChild( + this.holder = this.createMenuContainer(this.uuid, this.size, this.ui.classes.menuContainer) + ); // default icons(close, back) const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', this.ui.classes.iconsContainer); - svg.appendChild(this.createItemSymbol(this.ui.item.close.symbol)); - svg.appendChild(this.createItemSymbol(this.ui.item.back.symbol)); + svg.appendChild(this.createSvgSymbol(this.ui.item.close.symbol)); + svg.appendChild(this.createSvgSymbol(this.ui.item.back.symbol)); this.holder.appendChild(svg); this.currentMenu = null; @@ -306,15 +313,16 @@ class RadialMenu return null; } - createMenuContainer(classValue) + createMenuContainer(uuid, size, classValue) { - this.holder = document.createElement('div'); - this.holder.id = this.uuid; - this.holder.className = classValue; - this.holder.style.width = this.size + 'px'; - this.holder.style.height = this.size + 'px'; + const container = document.createElement('div'); + + container.id = uuid; + container.className = classValue; + container.style.width = size + 'px'; + container.style.height = size + 'px'; - this.parent.appendChild(this.holder); + return container; } showNestedMenu(item) @@ -389,7 +397,7 @@ class RadialMenu } /** handle click in the center, eg: close or back-button */ - handleCenterClick() + handleClickCloseOrBack() { if (this.parentItems.length > 0) { @@ -423,12 +431,12 @@ class RadialMenu const group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); group.setAttribute('class', this.ui.classes.closeBackButton); - const centerCircle = this.createCircle(0, 0, this.innerRadius - this.sectorSpace / 3); + const centerCircle = this.createSvgCircle(0, 0, this.innerRadius - this.sectorSpace / 3); group.appendChild(centerCircle); if (nested && this.ui.nested.title) { - const text = this.createItemText(0, +size, nested); + const text = this.createSvgText(0, +size, nested); group.appendChild(text); } @@ -439,7 +447,7 @@ class RadialMenu { icon = (this.ui.nested.icon === true ? nested.icon : this.ui.nested.icon); } - const use = this.createItemUse(0, 0, icon); + const use = this.createSvgUse(0, 0, icon); use.setAttribute('width', size); use.setAttribute('height', size); use.setAttribute( @@ -543,7 +551,7 @@ class RadialMenu } if (classNames[i] === THIS.ui.classes.closeBackButton) { - THIS.handleCenterClick(); + THIS.handleClickCloseOrBack(); break; } } @@ -580,7 +588,7 @@ class RadialMenu } if (this.isKeyDown(event, this.ui.moveByKeys.back)) { - this.handleCenterClick(); + this.handleClickCloseOrBack(); event.preventDefault(); return; } @@ -670,7 +678,7 @@ class RadialMenu } } - createItemUse(x, y, link) + createSvgUse(x, y, link) { const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); @@ -716,7 +724,7 @@ class RadialMenu if (item.title) { - const text = this.createItemText(centerPoint.x, centerPoint.y, item); + const text = this.createSvgText(centerPoint.x, centerPoint.y, item); if (item.icon) { text.setAttribute('transform', 'translate(0,8)'); @@ -730,7 +738,7 @@ class RadialMenu if (item.icon) { - const use = this.createItemUse(centerPoint.x, centerPoint.y, item.icon); + const use = this.createSvgUse(centerPoint.x, centerPoint.y, item.icon); if (item.title) { use.setAttribute('transform', 'translate(-5,-8)'); @@ -751,6 +759,8 @@ class RadialMenu createItemSectorPath(startAngleDeg, endAngleDeg) { + // FIXME: if (this.minSectors < 4) it looks weird! itemSectorPath is somehow deformed! + // TODO: createItemSectorPath(more params, regards to created item, eg: {start, end}Angle...); const initPoint = this.getDegreePosition(startAngleDeg, this.radius); let path = 'M' + this.pointToString(initPoint); const radiusAfterScale = this.radius * (1 / this.scale); @@ -768,7 +778,7 @@ class RadialMenu return path; } - createItemText(x, y, item) + createSvgText(x, y, item) { const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); @@ -781,7 +791,7 @@ class RadialMenu return text; } - createCircle(x, y, r) + createSvgCircle(x, y, r) { const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); @@ -809,7 +819,7 @@ class RadialMenu ); } - createItemSymbol(item) + createSvgSymbol(item) { const symbol = document.createElementNS('http://www.w3.org/2000/svg', 'symbol'); symbol.setAttribute('id', item.id); From 1eca5daee1aa555b9bbc1586b8a0e5087d29a2d2 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sun, 9 Jul 2023 21:13:44 +0200 Subject: [PATCH 15/16] bugfix: RadialMenu.{close vs back} -- https://github.com/axln/radial-menu-js/issues/3 --- css/RadialMenuCustom.css | 3 +- js/RadialMenu.js | 162 ++++++++++++++++++++++++--------------- 2 files changed, 104 insertions(+), 61 deletions(-) diff --git a/css/RadialMenuCustom.css b/css/RadialMenuCustom.css index 1bb84ca..9aad1e5 100644 --- a/css/RadialMenuCustom.css +++ b/css/RadialMenuCustom.css @@ -20,7 +20,8 @@ div.menuHolder2.open2 div.menuHolder2.close2 { /* menu-container:close => its not visible */ - + z-index: -1; + visibility: hidden; } svg.icons diff --git a/js/RadialMenu.js b/js/RadialMenu.js index d3b201d..0cd7352 100644 --- a/js/RadialMenu.js +++ b/js/RadialMenu.js @@ -117,14 +117,14 @@ class RadialMenu params.ui || {} ); this.scale = 1; - this.holder = null; - this.parentMenu = []; - this.parentItems = []; - this.levelItems = null; // menu container ~ this.holder this.parent.appendChild( - this.holder = this.createMenuContainer(this.uuid, this.size, this.ui.classes.menuContainer) + this.holder = this.createMenuContainer( + this.uuid, + this.size, + [this.ui.classes.menuContainer, this.ui.classes.menuClose].join(' ') + ) ); // default icons(close, back) @@ -134,7 +134,6 @@ class RadialMenu svg.appendChild(this.createSvgSymbol(this.ui.item.back.symbol)); this.holder.appendChild(svg); - this.currentMenu = null; if (this.ui.moveByWheel) { document.addEventListener('wheel', this.onMouseWheel.bind(this)); @@ -143,6 +142,8 @@ class RadialMenu { document.addEventListener('keydown', this.onKeyDown.bind(this)); } + + this.initialize(); } /** @@ -153,21 +154,21 @@ class RadialMenu */ generateUUID() { - let d1 = new Date().getTime();//Timestamp - let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0; // Time in microseconds since page-load or 0 if unsupported + let d1 = new Date().getTime(); // timestamp + let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0; // time in microseconds since page-load or 0 if unsupported return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' .replace(/[xy]/g, function(c) { let r = Math.random() * 16; // random number between 0 and 16 if (d1 > 0) { - // Use timestamp until depleted + // use timestamp until depleted r = (d1 + r) % 16 | 0; d1 = Math.floor(d1 / 16); } else { - // Use microseconds since page-load if supported + // use microseconds since page-load if supported r = (d2 + r) % 16 | 0; d2 = Math.floor(d2 / 16); } @@ -180,7 +181,7 @@ class RadialMenu { console.info(this.constructor.name + ".onClickFallback(item):"); console.info(item); - throw "onClick: function(item) {...}; // must be defined by params or default!"; + console.error(this.constructor.name + "onClick: function(item) {...}; // must be defined by params or default!"); } /** return true if menu is visible, otherwise returns false. */ @@ -196,7 +197,7 @@ class RadialMenu handleClickOutside(event, THIS) { const menu = document.getElementById(THIS.uuid); - if (!menu || THIS.uuid !== menu.id) + if (!menu || THIS.uuid !== menu.id || !THIS.isOpen()) { return; } @@ -218,7 +219,7 @@ class RadialMenu { THIS.closeOnClickOutside(THIS); } - THIS.close(); + THIS.close(true); } open(x = undefined, y = undefined) @@ -227,10 +228,21 @@ class RadialMenu { return; } + this.initialize(); this.currentMenu = this.createMenu( [this.ui.classes.menuCreate, this.ui.classes.menuCreateParent].join(' '), this.menuItems ); + + const alreadyOpened = this.holder.getElementsByClassName(this.ui.classes.menuCreate); + if (alreadyOpened) + { + // alreadyOpened? remove all... to start from scratch! + while(alreadyOpened.length > 0) + { + alreadyOpened[0].parentNode.removeChild(alreadyOpened[0]); + } + } this.holder.appendChild(this.currentMenu); // wait DOM commands to apply and then set class to allow transition to take effect @@ -262,43 +274,59 @@ class RadialMenu } } - close() + close(force = false) { - if (!this.isOpen()) + if (!force && !this.isOpen()) { return; } - let parentMenu; - while (parentMenu = this.parentMenu.pop()) - { - parentMenu.remove(); - } - this.parentItems = []; - const THIS = this; - this.setClassAndWaitForTransition( + if (this.currentMenu !== null) + { + this.setClassAndWaitForTransition( this.currentMenu, [this.ui.classes.menuCreate, this.ui.classes.menuCreateParent].join(' ') ) - .then(function() + .then(function () { - if (THIS.currentMenu !== null) - { - THIS.currentMenu.remove(); - } - THIS.currentMenu = null; - if (THIS.closeOnClickOutside) - { - document.removeEventListener('click', THIS.closeOnClickOutsideListener); - } - const menuContainer = document.getElementById(THIS.uuid); - menuContainer.classList.remove(THIS.ui.classes.menuOpen); - menuContainer.classList.add(THIS.ui.classes.menuClose); - }) - ; + THIS.initialize(); + }); + } + if (this.closeOnClickOutside) + { + document.removeEventListener('click', this.closeOnClickOutsideListener); + } + this.postRunnable(function() + { + THIS.initialize(); + }, 250); } - /** default functionality as onClick(): function, which MUST be overridden through params! */ + initialize() + { + this.level = 0; + this.currentMenu = null; + this.levelItems = null; + this.parentMenu = []; + this.parentItems = []; + const alreadyOpened = this.holder.getElementsByClassName(this.ui.classes.menuCreate); + if (alreadyOpened) + { + // alreadyOpened? remove all... to start from scratch! + while(alreadyOpened.length > 0) + { + alreadyOpened[0].parentNode.removeChild(alreadyOpened[0]); + } + } + const menuContainer = document.getElementById(this.uuid); + if (menuContainer) + { + menuContainer.classList.remove(this.ui.classes.menuOpen); + menuContainer.classList.add(this.ui.classes.menuClose); + } + } + + /** default functionality as onClick(): function(), which MUST be overridden through params! */ onClick(item) { return item; @@ -327,6 +355,11 @@ class RadialMenu showNestedMenu(item) { + if (!this.isOpen()) + { + return; + } + this.level++; this.parentMenu.push(this.currentMenu); this.parentItems.push(this.levelItems); this.currentMenu = this.createMenu( @@ -340,9 +373,13 @@ class RadialMenu const THIS = this; this.postRunnable(function() { + if (!THIS.isOpen()) + { + return; + } THIS.getParentMenu().setAttribute( 'class', - [THIS.ui.classes.menuCreate, THIS.ui.classes.menuCreateNested].join(' ') + [THIS.ui.classes.menuCreate, THIS.ui.classes.menuCreateNested, THIS.ui.classes.menuClose].join(' ') ); THIS.currentMenu.setAttribute( 'class', @@ -362,11 +399,14 @@ class RadialMenu this.currentMenu, [this.ui.classes.menuCreate, this.ui.classes.menuCreateParent].join(' ') ) - .then(function() - { + .then(function(){ THIS.currentMenu.remove(); THIS.currentMenu = THIS.parentMenu.pop(); THIS.levelItems = THIS.parentItems.pop(); + THIS.currentMenu.setAttribute( + 'class', + [THIS.ui.classes.menuCreate, THIS.ui.classes.menuOpen].join(' ') + ); }) ; } @@ -374,6 +414,10 @@ class RadialMenu /** handle click inside menu, eg: choosing item. */ handleClick() { + if (!this.isOpen()) + { + return; + } const selectedIndex = this.getSelectedIndex(); if (selectedIndex >= 0) { @@ -383,39 +427,33 @@ class RadialMenu this.showNestedMenu(item); return; } + let selectedItem = Object.assign({}, item); if (this.closeOnClick) { - this.close(); + this.close(true); } if (this.onClick && this.onClick instanceof Function) { - this.onClick(item); + this.onClick(selectedItem); return; } - this.onClickFallback(item); + this.onClickFallback(selectedItem); } } /** handle click in the center, eg: close or back-button */ handleClickCloseOrBack() { - if (this.parentItems.length > 0) + if (!this.isOpen()) { - this.returnToParentMenu(); + return; } - else + if (this.parentItems.length > 0) { - this.close(); + this.returnToParentMenu(); + return; } - // FIXME: https://github.com/axln/radial-menu-js/issues/3 - // When you are in a submenu and exit (any way) too fast, - // the event in the function '''RadialMenu.prototype.handleCenterClick''' - // call '''self.returnToParentMenu();''' instead of '''self.close();'''. - // This makes it difficult to tell when the menu is completely closed - // - // radial-menu-js/js/RadialMenu.js - // Line 137 in 6d027fc - // RadialMenu.prototype.handleCenterClick = function () {...} + this.close(true); } /** @@ -480,7 +518,6 @@ class RadialMenu /** create all items for currently visible menu, eg: main menu or nested one */ createMenu(classValue, levelItems, nested) { - const THIS = this; this.levelItems = levelItems; this.sectorCount = Math.max(this.levelItems.length, this.defaultValues.minSectors); this.scale = this.calculateScale(); @@ -523,6 +560,7 @@ class RadialMenu ); } + const THIS = this; svg.addEventListener('mousedown', function(event) { const classNames = event.target.parentNode.getAttribute('class').split(' '); @@ -905,9 +943,13 @@ class RadialMenu }); } + /** + * well, well, well naaacelniku :P + * this will process function(); its very useful to be sure that data will be processed exactly same! + * (basically u want to encapsulate data-manipulation due transitions-shlitz, eg: setClassAndWaitForTransition()) + */ postRunnable(fn, timeoutMs = 10) { - //TODO:??idk if i like it. it looks messy due prev. RadialMenu.prototype approach!? setTimeout(fn, timeoutMs); } From 23a917694c3d9ffd829fb85ffb127d068251e0e0 Mon Sep 17 00:00:00 2001 From: 3nda Date: Sun, 9 Jul 2023 21:43:40 +0200 Subject: [PATCH 16/16] fix: README.md --- README.md | 56 ++++++++++++++++++------------------------------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 4f75c6a..c23afe2 100644 --- a/README.md +++ b/README.md @@ -14,48 +14,28 @@ ## Usage Example ```javascript -var svgMenu = new RadialMenu({ - parent : document.body, - size : 400, - closeOnClick: true, - menuItems : [ - { - id: 'item1', - title: 'Item 1' - }, - { - id: 'item2', - title: 'Item 2' - }, - { - id: 'more', - title: 'More...', - items: [ - { - id: 'subitem1', - title: 'Subitem 1' - }, - { - id: 'item2', - title: 'Subitem 2' - } - ] - } - ], - onClick: function (item) { - console.log('You have clicked:', item); - } +var svgMenu = new RadialMenu([ // menuItems + {id: 'item1', title: 'Item 1'}, + {id: 'item2', title: 'Item 2'}, + {id: 'more', title: 'More...', items: [ + {id: 'subitem1', title: 'Subitem 1'}, + {id: 'item2', title: 'Subitem 2'} + ]}], + 400, // size + { // params + onClick: function (item) { + console.log('You have clicked:', item); + } }); - - var openMenu = document.getElementById('menu'); - openMenu.onclick = function () { +var openMenu = document.getElementById('menu'); +openMenu.onclick = function () { svgMenu.open(); - }; +}; - var closeMenu = document.getElementById('close'); - closeMenu.onclick = function () { +var closeMenu = document.getElementById('close'); +closeMenu.onclick = function () { svgMenu.close(); - }; +}; ``` ## Configuration