UNPKG

figma-plugin-ds

Version:

A UI library with CSS and vanilla JS that match the Figma UI for building plugins.

448 lines (340 loc) 16.3 kB
(function(){ 'use strict'; const selectMenuSelector = 'select-menu'; let elements, optionList, itemHeight, selectedItem; let init = false; //PRIVATE //create the menus const createMenus = function() { // loop through all select menus on screen elements.forEach(function (menu, index) { // setup mutation observer let mutationConfig = { attributes: true, childList: true, subtree: true }; let callback = function(mutationsList, observer) { for(let mutation of mutationsList) { if (mutation.type === 'childList') { selectMenu.init(); } else if (mutation.type === 'attributes') { if (mutation.attributeName === 'value' || mutation.attributeName === 'disabled') { selectMenu.init(); } } } }; let observer = new MutationObserver(callback); observer.observe(menu, mutationConfig); //keep track of menu height, reset to 0 when building new one itemHeight = 0; //create the wrapper for the menu createWrapper(menu); //find out if an item is already selected selectedItem = menu.selectedIndex; //find out if there are option groups present let optionGroups = Array.from(menu.getElementsByTagName('optgroup')); if (optionGroups.length != 0) { //do this if optgroups present //determine if the option groups have labels let hasLabels = (optionGroups[0].label) ? true : false; //loop through every option group optionGroups.forEach(function (group, index) { if (hasLabels) { if (index != 0) { let divider = document.createElement('div'); divider.className = selectMenuSelector + '__divider'; optionList.appendChild(divider); addItemHeight(divider); //prevent clicks on optgroup dividers divider.addEventListener('click', stopProp, false); } //create the divider element w/ a label let divider = document.createElement('div'); divider.textContent = group.label divider.className = selectMenuSelector + '__divider-label'; //add to menu optionList.appendChild(divider); //calculate and add height of divider addItemHeight(divider); //prevent clicks on optgroup dividers divider.addEventListener('click', stopProp, false); } if (index > 0 && !hasLabels) { //create the divider element let divider = document.createElement('div'); divider.className = selectMenuSelector + '__divider'; //add to menu optionList.appendChild(divider); //calculate and add height of divider addItemHeight(divider); //prevent clicks on optgroup dividers divider.addEventListener('click', stopProp, false); } //get children of group let options = Array.from(group.getElementsByTagName('option')); //loop through all options and generate an item options.forEach(option => { createMenuItem(option); }); }); } else { //do this if there are no optgroupss let options = Array.from(menu.options) //loop through all options and generate an item options.forEach(option => { createMenuItem(option); }); } }); } //create the wrapper for the select menu //includes button and generates the wrapper UL for the list of menu items const createWrapper = function(menu) { //add top margin itemHeight += 6; //hide the select menu menu.style.display = 'none'; //set the selected option to correct menu item if not set if (menu.selectedIndex != -1) { menu.options[menu.selectedIndex].selected = true; } //create the wrapper, and insert the hidden select menu let menuWrapper = document.createElement('div'); menuWrapper.className = selectMenuSelector; menu.parentNode.insertBefore(menuWrapper, menu); menuWrapper.appendChild(menu); //determine if an icon is specified let iconName = menu.getAttribute('icon'); //create the button + nested elements let button = document.createElement('button') let icon; let buttonLabel = document.createElement('span') let buttonCaret = document.createElement('span') if (iconName) { icon = document.createElement('span') icon.className = 'icon ' + iconName; } //add classes button.className = selectMenuSelector + '__button'; buttonLabel.className = selectMenuSelector + '__label'; buttonCaret.className = selectMenuSelector + '__caret'; //add content if (menu.selectedIndex != -1) { buttonLabel.textContent = menu.options[menu.selectedIndex].text; if (menu.options[menu.selectedIndex].value === '') { buttonLabel.classList.add(selectMenuSelector + '__label--placeholder'); } } else { buttonLabel.textContent = 'No items to display'; buttonLabel.classList.add(selectMenuSelector + '__label--placeholder'); } //create the menu container optionList = document.createElement('ul'); optionList.className = selectMenuSelector + '__menu'; //add elements to dom menuWrapper.appendChild(button); menuWrapper.appendChild(optionList); if (icon) { button.appendChild(icon); } button.appendChild(buttonLabel); button.appendChild(buttonCaret); //add event listener button.addEventListener('click', displayMenu, false); } //create a list item const createMenuItem = function(menuItem) { /* only create an item if there is a value this will ignore the first menu item (if included) as a placeholder */ if (menuItem.hasAttribute('value') && menuItem.value != '') { //create list item elements let item = document.createElement('li'); let itemIcon = document.createElement('span'); let itemLabel= document.createElement('span') //set classnames item.className = selectMenuSelector + '__item'; itemIcon.className = selectMenuSelector + '__item-icon'; itemLabel.className = selectMenuSelector + '__item-label'; //add elements to dom item.appendChild(itemIcon); item.appendChild(itemLabel); optionList.appendChild(item); //configure attributes item.setAttribute('data-value', menuItem.value); itemLabel.textContent = menuItem.text; item.setAttribute('position', itemHeight); /* after the item is created we pass this element to this function this function calculates the height of the item and increases value of the item height var */ addItemHeight(item); //if item is selected, add active class if (menuItem.index === selectedItem) { item.classList.add(selectMenuSelector + '__item--selected'); let menuPosition = -Math.abs(parseInt(item.getAttribute('position'))); optionList.style.top = menuPosition + 'px'; } //event listener item.addEventListener('click', displayMenu, false); } } //function to display the menu when clicked var displayMenu = function(event) { /*the event is any click registered inside the element and then determine if the button or menu item is clicked */ if (this.tagName == 'BUTTON') { //get the menu element so we can see if there are options to display let selectMenu = this.parentNode.querySelector('select'); if (selectMenu.children.length > 0) { //add active class to button (is this needed?) this.classList.toggle(selectMenuSelector + '__button--active'); //toggle the menu let menu = this.parentNode.querySelector('UL'); menu.classList.toggle(selectMenuSelector + '__menu--active'); //update position of menu resizeAndPosition(menu); } this.blur(); } else if (this.tagName === 'LI') { //define the menu let menu = this.parentNode.parentNode.querySelector('UL'); //remove active classses from all menus let menuItems = Array.from(menu.getElementsByTagName('LI')); menuItems.forEach(item => { item.classList.remove(selectMenuSelector + '__item--selected'); }); //select item this.classList.add(selectMenuSelector + '__item--selected'); //update the value of the select menu let select = menu.parentNode.querySelector('SELECT'); let selectedValue = this.getAttribute('data-value'); let options = select.querySelectorAll('option'); //remove selected option for all elements options.forEach(option => { if (option.value === selectedValue) { option.setAttribute('selected','selected'); } else { option.removeAttribute('selected'); } }); select.value = selectedValue; //dispatch change event let event = new Event('change'); select.dispatchEvent(event); //update the button on the dropdown let button = this.parentNode.parentNode.querySelector('BUTTON'); let buttonLabel = button.querySelector('.' + selectMenuSelector + '__label'); buttonLabel.textContent = this.textContent; buttonLabel.classList.remove(selectMenuSelector + '__label--placeholder'); button.classList.toggle(selectMenuSelector + '__button--active'); //toggle the dropdown visibility menu.classList.toggle(selectMenuSelector + '__menu--active'); //update the position of the drop down after hidden let menuPosition = -Math.abs(parseInt(this.getAttribute('position'))); menu.style.top = menuPosition + 'px'; //update position of menu resizeAndPosition(menu); } } // event handlers //stop event propagation var stopProp = function(event) { event.stopPropagation(); } //track clicks outside the menu var isOutside = function(event) { let selectMenus = document.querySelectorAll('select.' + selectMenuSelector); selectMenus.forEach(select => { let menuWrapper = select.parentNode; let menu = menuWrapper.querySelector('UL'); let button = menuWrapper.querySelector('BUTTON'); if (menu.classList.contains(selectMenuSelector + '__menu--active')) { let clickInside = menuWrapper.contains(event.target); if (!clickInside) { menu.classList.remove(selectMenuSelector + '__menu--active'); button.classList.remove(selectMenuSelector + '__button--active'); } } }); } // this function ensures that the select menu // fits inside the plugin viewport // if its too big, it will resize it and enable a scrollbar // if its off screen it will shift the position const resizeAndPosition = function(menu) { //set the max height of the menu based on plugin/iframe window let maxMenuHeight = window.innerHeight - 16; let menuHeight = menu.offsetHeight; let menuResized = false; let menuButton = menu.parentNode.querySelector('BUTTON'); if (menuHeight > maxMenuHeight) { menu.style.height = maxMenuHeight + 'px'; menuResized = true; } //lets adjust the position of the menu if its cut off from viewport let bounding = menu.getBoundingClientRect(); let parentBounding = menuButton.getBoundingClientRect(); if (bounding.top < 0) { menu.style.top = -Math.abs(parentBounding.top - 8) + 'px'; } if (bounding.bottom > (window.innerHeight || document.documentElement.clientHeight)) { let minTop = -Math.abs(parentBounding.top - (window.innerHeight - menuHeight - 8)); let newTop = -Math.abs(bounding.bottom - window.innerHeight + 16); if (menuResized) { menu.style.top = -Math.abs(parentBounding.top - 8) + 'px'; } else if (newTop > minTop) { menu.style.top = minTop + 'px'; } else { menu.style.top = newTop + 'px'; } } } //helper functions //increment itemHeight function addItemHeight(element) { //get key dimensions to calculate height let dimensions = [ parseInt(window.getComputedStyle(element, null).getPropertyValue('margin-top')), parseInt(window.getComputedStyle(element, null).getPropertyValue('margin-bottom')), parseInt(window.getComputedStyle(element, null).getPropertyValue('padding-top')), parseInt(window.getComputedStyle(element, null).getPropertyValue('padding-bottom')), parseInt(window.getComputedStyle(element, null).getPropertyValue('height')) ]; itemHeight += arraySum(dimensions); } //helper function to return sum of array function arraySum(data) { return data.reduce(function(a,b){ return a + b }, 0); } // PUBLIC window.selectMenu = { init: function() { //destroy first if already initialized if (init == true) { this.destroy(); } //initialize all menus elements = document.querySelectorAll('.' + selectMenuSelector); if (elements) { //create the menu(s) createMenus(); //click handler for clicks outside of menu document.addEventListener('click', isOutside, false); //set init to true now that menu has been initialized init = true; } }, destroy: function() { if (elements) { //remove all the generated DOM elements elements.forEach((menu) => { let parent = menu.parentNode; parent.querySelector('BUTTON').remove(); parent.querySelector('UL').remove(); parent.outerHTML = parent.innerHTML; }); //remove event handler on each element document.removeEventListener('click', isOutside, false); //set init to false now that menu has been destroyed init = false; } } } })();