From 7cbc51667954be132072c0c5583d9aac3a0f95c8 Mon Sep 17 00:00:00 2001 From: Juliusz Chroboczek Date: Wed, 16 Feb 2022 19:08:38 +0100 Subject: [PATCH] Import Contextual.js. https://github.com/LucasReade/Contextual.js --- static/external/contextual/LICENSE | 21 ++ static/external/contextual/contextual.css | 144 +++++++++++++ static/external/contextual/contextual.js | 234 ++++++++++++++++++++++ 3 files changed, 399 insertions(+) create mode 100644 static/external/contextual/LICENSE create mode 100644 static/external/contextual/contextual.css create mode 100644 static/external/contextual/contextual.js diff --git a/static/external/contextual/LICENSE b/static/external/contextual/LICENSE new file mode 100644 index 0000000..ad3ee18 --- /dev/null +++ b/static/external/contextual/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Lucas Reade + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/static/external/contextual/contextual.css b/static/external/contextual/contextual.css new file mode 100644 index 0000000..5d55749 --- /dev/null +++ b/static/external/contextual/contextual.css @@ -0,0 +1,144 @@ +/* Main context menu outer */ +.contextualMenu{ + font-size: 13px; + position: absolute; + padding: 8px 0; + background: var(--contextualMenuBg); + box-shadow: var(--contextualMenuShadow); + border-radius: var(--contextualMenuRadius); + margin:0; + list-style: none; + color: var(--contextualMenuText); +} + +/* Menu seperator item */ +.contextualMenuSeperator{ + display: block; + position: relative; + padding: 5px 5px; +} + .contextualMenuSeperator span{ + display: block; + width:100%; + height:1px; + background: var(--contextualSeperator); + } + +/* Default menu item */ +.contextualMenuItemOuter { + position: relative; +} +.contextualMenuItem{ + display: block; + padding: 5px 8px; + cursor: default; +} + .contextualMenuItem:hover{ + background: var(--contextualHover); + } + .contextualMenuItemIcon{ + float: left; + width:16px; + height: 16px; + } + .contextualMenuItemTitle{ + text-align: left; + line-height: 16px; + display: inline-block; + padding: 0px 0px 0px 7px; + } + .contextualMenuItemTip{ + float: right; + padding: 0px 0px 0px 50px; + text-align: right; + line-height: 16px; + } + .contextualMenuItemOverflow{ + float: right; + width:16px; + height: 16px; + padding: 1px 0px 0px 7px; + } + + .contextualMenuItemOverflow .contextualMenuItemOverflowLine{ + display: block; + height: 1px; + margin: 3px 2px; + background: var(--contextualOverflowIcon); + } + .contextualMenuItemOverflow.hidden{ + display: none; + } + + .contextualMenuItem.disabled{ + opacity: 0.4; + } + .contextualMenuItem.disabled:hover{ + background: none; + } + +/* Submenu item */ +.contextualSubMenu{ + padding: 0; + margin: 0; + background: var(--contextualSubMenuBg); + border-radius: var(--contextualMenuRadius); + width: 100%; + height: auto; + max-height: 1000px; + transition: max-height 0.5s; + overflow: hidden; +} + .contextualSubMenu .contextualMenuItem:hover{ + background: var(--contextualHover); + } + +.contextualMenuHidden{ + max-height: 0; +} + +/* Multi item button */ +.contextualMultiItem{ + display: flex; + position: relative; +} + .contextualMultiItem .contextualMenuItemOuter{ + flex: auto; + display: inline-block; + } + +/* Hover menu */ +.contextualHoverMenuOuter{ + position: relative; +} +.contextualHoverMenuItem{ + display: block; + padding: 5px 8px; + cursor: default; +} +.contextualHoverMenuItem.disabled{ + opacity: 0.4; +} +.contextualHoverMenuItem.disabled:hover{ + background: none; +} +.contextualHoverMenuItem:hover{ + background: var(--contextualHover); +} + +.contextualHoverMenuOuter > .contextualHoverMenu{ + display: none; +} +.contextualHoverMenuOuter:hover > .contextualHoverMenu{ + display: block; + position: absolute; + left: 100%; + top: 0; + background: var(--contextualMenuBg); + box-shadow: var(--contextualMenuShadow); + border-radius: var(--contextualMenuRadius); + padding: 8px 0; + width: 100%; + z-index: 1000; + list-style: none; +} diff --git a/static/external/contextual/contextual.js b/static/external/contextual/contextual.js new file mode 100644 index 0000000..c950fcb --- /dev/null +++ b/static/external/contextual/contextual.js @@ -0,0 +1,234 @@ +class Contextual{ + /** + * Creates a new contextual menu + * @param {object} opts options which build the menu e.g. position and items + * @param {number} opts.width sets the width of the menu including children + * @param {boolean} opts.isSticky sets how the menu apears, follow the mouse or sticky + * @param {Array} opts.items sets the default items in the menu + */ + constructor(opts){ + contextualCore.CloseMenu(); + + this.position = opts.isSticky != null ? opts.isSticky : false; + this.menuControl = contextualCore.CreateEl(``); + this.menuControl.style.width = opts.width != null ? opts.width : '200px'; + opts.items.forEach(i => { + let item = new ContextualItem(i); + this.menuControl.appendChild(item.element); + }); + + if(event != undefined){ + event.stopPropagation() + document.body.appendChild(this.menuControl); + contextualCore.PositionMenu(this.position, event, this.menuControl); + } + + document.onclick = function(e){ + if(!e.target.classList.contains('contextualJs')){ + contextualCore.CloseMenu(); + } + } + } + /** + * Adds item to this contextual menu instance + * @param {ContextualItem} item item to add to the contextual menu + */ + add(item){ + this.menuControl.appendChild(item.element); + } + /** + * Makes this contextual menu visible + */ + show(){ + event.stopPropagation() + document.body.appendChild(this.menuControl); + contextualCore.PositionMenu(this.position, event, this.menuControl); + } + /** + * Hides this contextual menu + */ + hide(){ + event.stopPropagation() + contextualCore.CloseMenu(); + } + /** + * Toggle visibility of menu + */ + toggle(){ + event.stopPropagation() + if(this.menuControl.parentElement != document.body){ + document.body.appendChild(this.menuControl); + contextualCore.PositionMenu(this.position, event, this.menuControl); + }else{ + contextualCore.CloseMenu(); + } + } +} +class ContextualItem{ + element; + /** + * + * @param {Object} opts + * @param {string} [opts.label] + * @param {string} [opts.type] + * @param {string} [opts.markup] + * @param {string} [opts.icon] + * @param {string} [opts.cssIcon] + * @param {string} [opts.shortcut] + * @param {void} [opts.onClick] + * @param {boolean} [opts.enabled] + * @param {Array} [opts.items] + * + */ + constructor(opts){ + switch(opts.type){ + case 'seperator': + this.seperator(); + break; + case 'custom': + this.custom(opts.markup); + break; + case 'multi': + this.multiButton(opts.items); + break; + case 'submenu': + this.subMenu(opts.label, opts.items, (opts.icon !== undefined ? opts.icon : ''), (opts.cssIcon !== undefined ? opts.cssIcon : ''), (opts.enabled !== undefined ? opts.enabled : true)); + break; + case 'hovermenu': + this.hoverMenu(opts.label, opts.items, (opts.icon !== undefined ? opts.icon : ''), (opts.cssIcon !== undefined ? opts.cssIcon : ''), (opts.enabled !== undefined ? opts.enabled : true)); + break; + case 'normal': + default: + this.button(opts.label, opts.onClick, (opts.shortcut !== undefined ? opts.shortcut : ''), (opts.icon !== undefined ? opts.icon : ''), (opts.cssIcon !== undefined ? opts.cssIcon : ''), (opts.enabled !== undefined ? opts.enabled : true)); + } + } + + button(label, onClick, shortcut = '', icon = '', cssIcon = '', enabled = true){ + this.element = contextualCore.CreateEl( ` +
  • +
    + ${icon != ''? `` : `
    `} + ${label == undefined? 'No label' : label} + ${shortcut == ''? '' : shortcut} +
    +
  • `); + + if(enabled == true){ + this.element.addEventListener('click', () => { + event.stopPropagation(); + if(onClick !== undefined){ onClick(); } + contextualCore.CloseMenu(); + }, false); + } + } + custom(markup){ + this.element = contextualCore.CreateEl(`
  • ${markup}
  • `); + } + hoverMenu(label, items, icon = '', cssIcon = '', enabled = true){ + this.element = contextualCore.CreateEl(` +
  • +
    + ${icon != ''? `` : `
    `} + ${label == undefined? 'No label' : label} + > +
    +
      +
    +
  • + `); + + let childMenu = this.element.querySelector('.contextualHoverMenu'), + menuItem = this.element.querySelector('.contextualHoverMenuItem'); + + if(items !== undefined) { + items.forEach(i => { + let item = new ContextualItem(i); + childMenu.appendChild(item.element); + }); + } + if(enabled == true){ + menuItem.addEventListener('mouseenter', () => { + + }); + menuItem.addEventListener('mouseleave', () => { + + }); + } + } + multiButton(buttons) { + this.element = contextualCore.CreateEl(` +
  • +
  • + `); + buttons.forEach(i => { + let item = new ContextualItem(i); + this.element.appendChild(item.element); + }); + } + subMenu(label, items, icon = '', cssIcon = '', enabled = true){ + this.element = contextualCore.CreateEl( ` +
  • +
    + ${icon != ''? `` : `
    `} + ${label == undefined? 'No label' : label} + + + + + +
    +
      +
    +
  • `); + + let childMenu = this.element.querySelector('.contextualSubMenu'), + menuItem = this.element.querySelector('.contextualMenuItem'); + + if(items !== undefined) { + items.forEach(i => { + let item = new ContextualItem(i); + childMenu.appendChild(item.element); + }); + } + if(enabled == true){ + menuItem.addEventListener('click',() => { + menuItem.classList.toggle('SubMenuActive'); + childMenu.classList.toggle('contextualMenuHidden'); + }, false); + } + } + seperator(label, items) { + this.element = contextualCore.CreateEl(`
  • `); + } +} + +const contextualCore = { + PositionMenu: (docked, el, menu) => { + if(docked){ + menu.style.left = ((el.target.offsetLeft + menu.offsetWidth) >= window.innerWidth) ? + ((el.target.offsetLeft - menu.offsetWidth) + el.target.offsetWidth)+"px" + : (el.target.offsetLeft)+"px"; + + menu.style.top = ((el.target.offsetTop + menu.offsetHeight) >= window.innerHeight) ? + (el.target.offsetTop - menu.offsetHeight)+"px" + : (el.target.offsetHeight + el.target.offsetTop)+"px"; + }else{ + menu.style.left = ((el.clientX + menu.offsetWidth) >= window.innerWidth) ? + ((el.clientX - menu.offsetWidth))+"px" + : (el.clientX)+"px"; + + menu.style.top = ((el.clientY + menu.offsetHeight) >= window.innerHeight) ? + (el.clientY - menu.offsetHeight)+"px" + : (el.clientY)+"px"; + } + }, + CloseMenu: () => { + let openMenuItem = document.querySelector('.contextualMenu:not(.contextualMenuHidden)'); + if(openMenuItem != null){ document.body.removeChild(openMenuItem); } + }, + CreateEl: (template) => { + var el = document.createElement('div'); + el.innerHTML = template; + return el.firstElementChild; + } +};