HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URLs added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by esm.sh, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ESM usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<div ng-app="myApp" ng-controller="myCtrl as vm">
<div style="padding: 20px;">
<nested-select options="vm.populations" label="Simple Nested Select"
show-selection></nested-select>
</div>
<div style="padding: 20px;">
<nested-select options="vm.populations" placeholder="Search items" label="With Search"
show-selection></nested-select>
</div>
<div style="padding: 20px;">
<nested-select options="vm.populations" label="Collapsibe"
collapsible show-selection></nested-select>
</div>
<div style="padding: 20px;">
<nested-select options="vm.populations" placeholder="Search items" label="Collapsible with Search"
collapsible show-selection></nested-select>
</div>
<div style="padding: 20px;">
<nested-select options="vm.populations" label="Multiple Select"
multiple collapsible show-selection allow-select-all></nested-select>
</div>
<div style="padding: 20px;">
<nested-select options="vm.populations" placeholder="Search items" label="Multiple with Search"
multiple collapsible show-selection allow-select-all></nested-select>
</div>
<div style="padding: 20px;">
<nested-select options="vm.populations" placeholder="Search items" label="Permanently Open"
multiple collapsible fixed-menu allow-select-all></nested-select>
</div>
</div>
html {
font-size: 62.5%;
line-height: 1.4;
}
body {
color: rgba(0,0,0,0.87);
font-family: 'Roboto', sans-serif;
font-size: 1.6rem;
}
nested-select {
display: block;
width: 300px;
}
/*
* Nested selects include an input container unless they're
* permanently open.
*/
nested-select md-input-container {
width: 100%;
}
/*
* Most of the styling for the contents of the input
* container come from Angular Material, but we do
* need to override a few styles.
*/
nested-select-selection.md-select-value {
border-bottom-color: rgba(0, 0, 0, 0.12);
cursor: pointer;
}
nested-select-selection.md-select-value:focus,
nested-select-selection.md-select-value.focused {
border-bottom-color: #3F51B5;
border-bottom-width: 2px;
outline: none;
padding-bottom: 0;
}
/*
* The select options themselves are enclosed in a menu
* container. This container may be permanently open or
* it may open/close based on a trigger. By default, the
* menu is triggerable, so set it to display: none.
* Also give it an absolute position and a z-index so
* it pops over the main page content.
*/
nested-select-menu {
display: none;
position: absolute;
transform: translateY(-16px);
z-index: 100;
}
/*
* The menu container has a tabindex so that clicking
* anywhere within it keeps the focus within the nested
* select. (Otherwise, focus may revert to the body
* element.) There's no need to visually distinguish
* this focus, though, so disable the normal outline.
*/
nested-select-menu:focus {
outline: none;
}
/*
* Adjust the menu if it is permanently fixed open.
*/
nested-select[fixed-menu] nested-select-menu {
display: block;
position: static;
z-index: inherit;
}
/*
* When the meny is visible, show it.
*/
nested-select-menu.visible {
display: block;
}
/*
* If the select only supports single values, there's
* no need to show the accumulated values in the
* input container, so shift the open menu up so that
* it's placed "on top of" that container.
*/
nested-select:not([multiple]) nested-select-menu.visible {
transform: translate(2px,-48px);
}
/*
* If search/filter is available, the menu container
* includes a header with the search field.
*/
nested-select-header {
background-color: rgb(250, 250, 250);
border-bottom: 1px solid rgba(0,0,0,0.12);
padding-left: 10.667px;
height: 48px;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
width: auto;
}
nested-select-header input[type=search] {
background-color: transparent;
border: none;
outline: none;
height: 100%;
width: 100%;
padding: 0;
}
/*
* Beneath the header is the actual menu content.
*/
nested-select md-content {
overflow-x: hidden;
padding: 16px 0 0 10.667px;
}
/*
* The main content is a series of selectable
* options. An option includes the text and
* related styling cues (icons).
*/
nested-select-option {
display: block;
width: 100%;
}
nested-select-text {
display: flex;
height: 40px;
width: 100%;
}
/*
* Each option may have a series of icons "in front
* of it". There are two types of icons. If the
* select is collapsible, then each option that has
* children will include a button to collapse or
* expand the display of those children. And if
* the component only allows single selections, then
* additional icons add visual guidance to the
* heirarchy. (For multiple selects, the checkboxes
* provide that visual guidance.)
*/
/*
* Styles for the "guide" icons.
*/
nested-select-guide {
left: 8px;
position: relative;
top: -8px;
}
/*
* Styles for the collapse/expand buttons
*/
.nested-select-collapse.nested-select-collapse {
flex-shrink: 0;
left: 8px;
margin: 0px;
position: relative;
top: -8px;
width: 40px;
}
.nested-select-collapse ng-md-icon {
color: rgba(0,0,0,0.38);
}
.nested-select-collapse:focus ng-md-icon,
.nested-select-collapse:hover ng-md-icon {
color: rgba(0,0,0,0.87);
}
.nested-select-collapse svg {
transition: transform 200ms linear 0s;
}
.nested-select-collapse.collapsed svg {
transform: rotate(-90deg);
}
/*
* Individual options are implemented as Angular
* Material checkboxes, and most of their styles
* come from that library. We do need to make a
* few adjustments, though.
*/
nested-select md-checkbox:last-of-type {
display: block;
margin-left: 16px;
width: 100%;
}
nested-select md-checkbox > .md-label {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: calc(100% - 16px);
}
nested-select[multiple] md-checkbox > .md-label {
width: calc(100% - 40px - 16px);
}
/*
* If the component is using single select, don't
* display the actual checkboxes (and don't shift
* the label over to leave room for them).
* Also display any checked text using the primaru
* color.
*/
nested-select:not([multiple]) md-checkbox > .md-container {
display: none;
}
nested-select:not([multiple]) md-checkbox > .md-label {
margin-left: 0;
}
nested-select:not([multiple]) md-checkbox.md-checked {
color: #3F51B5;
}
/*
* Since the md-checkbox elements might not
* span the entire width of the menu, we use
* a pseudo-element to render the focus, hover,
* and selected styles. We can extend it the
* full width using a bit of vw trickery.
*/
nested-select md-checkbox:focus::before,
nested-select md-checkbox:hover::before,
nested-select:not([multiple]) md-checkbox.md-checked::before {
content: "";
position: absolute;
height: 40px;
width: 200vw;
background-color: rgb(238, 238, 238);
transform: translate(-100vw,-8px);
}
/*
* Keep the icons visible even when the option is
* focus/hover/checked by raising their z-index.
*/
nested-select button,
nested-select-guide {
z-index: 101;
}
nested-select[fixed-menu] button,
nested-select[fixed-menu] nested-select-guide {
z-index: 1;
}
/*
* Handle collapse/expand via CSS. Someday
* this might be animated.
*/
nested-select-options.collapsed {
display: none;
}
angular.module('myApp', ['ngSanitize','ngMaterial', 'ngMdIcons'])
.controller('myCtrl', [function myCtrl () {
var vm = this;
// static collection for test/demo
vm.populations = [
{ text: "Item 1" },
{ text: "Item 2",
children: [
{ text: "Item 2.1" }
]
},
{ text: "Item 3",
children: [
{ text: "Item 3.1",
children: [
{ text: "Item 3.1.1",
children: [
{ text: "Yet Another Item 3.1.1.1" }
]
},
{ text: "Item 3.1.2" }
]
},
{ text: "Item 3.2",
children: [
{ text: "Item 3.2.1" },
{ text: "Item 3.2.2" }
]
},
{ text: "Item 3.3" }
]
}
];
}])
.config(function (ngMdIconServiceProvider) {
ngMdIconServiceProvider.addShapes({
'icon-nested-select-vbar': '<path d="M 19.5 0h1v40h-1z"/>',
'icon-nested-select-el': '<path d="M 19.5 0h1v19.5h19.5v1h-20.5z"/>',
'icon-nested-select-midbar': '<path d="M 19.5 0h1v19.5h19.5v1h-19.5v39h-1z"/>',
'icon-nested-select-space': '<g/>',
}).addViewBoxes({
'icon-nested-select-vbar': '0 0 40 40',
'icon-nested-select-el': '0 0 40 40',
'icon-nested-select-midbar': '0 0 40 40',
'icon-nested-select-space': '0 0 40 40'
});
})
.component('nestedSelect', {
bindings: {
options: '<',
label: '@',
onChange: '&'
},
template: `
<md-input-container ng-class="{'md-input-has-value': ($ctrl.hasValue() || $ctrl.fixedMenu), 'md-input-focused': $ctrl.hasFocus()}">
<label>{{$ctrl.label}}</label>
<nested-select-selection ng-if="$ctrl.showSelection"
ng-click="$ctrl.onClickSelection()" ng-class="{'menu-open': $ctrl.showMenu, focused: $ctrl.hasFocus()}"
class="md-select-value" tabindex="0">
<span ng-bind="$ctrl.getSelectionText()" >
</span>
<span class="md-select-icon"></span>
</nested-select-selection>
</md-input-container>
<nested-select-menu ng-class="{visible: $ctrl.showMenu}" md-whiteframe="2" tabindex="-1">
<nested-select-header ng-if="$ctrl.placeholder" >
<input type="search" placeholder="{{$ctrl.placeholder}}"
ng-model="$ctrl.filter" ng-change="$ctrl.onChangeFilter()" />
<md-button class="md-icon-button" ng-if="$ctrl.multiple && $ctrl.allowSelectAll"
ng-click="$ctrl.onClickAll()">
<md-tooltip>Select All</md-tooltip>
<ng-md-icon icon="playlist_add_check" style="fill: currentColor"></ng-md-icon>
</md-button>
</nested-select-header>
<md-content ng-class="{filtering: $ctrl.filter.length}">
<nested-select-options options="$ctrl.options" collapsible="$ctrl.collapsible" multiple="$ctrl.multiple" depth="0" on-change="$ctrl.onOptionChange($event)">
</md-content>
</nested-select-menu>
`,
controller: function nestedSelect ($scope, $timeout, $element, $attrs, $mdConstant) {
var $ctrl = this;
// Lifecycle event handlers
$ctrl.$onInit = function () {
$ctrl.filter = "";
$ctrl.placeholder = $attrs.placeholder;
$ctrl.multiple = !!$attrs.$attr.multiple;
$ctrl.collapsible = !!$attrs.$attr.collapsible;
$ctrl.showSelection = !!$attrs.$attr.showSelection;
$ctrl.allowSelectAll = !!$attrs.$attr.allowSelectAll;
$ctrl.fixedMenu = !!$attrs.$attr.fixedMenu;
$element.on("focusout", $ctrl.onFocusout);
$element.on("keyup", $ctrl.onKeyup);
};
$ctrl.$onChanges = function (changes) {
if (changes.options) {
$ctrl.options = angular.copy($ctrl.options);
} else {
$ctrl.options = $ctrl.options || [];
}
$ctrl.updateHighlightedOptions();
};
$ctrl.$postLink = function () {
var menu = $element.find('nested-select-menu');
if (menu) {
menu.css('width', $element[0].clientWidth + 'px');
}
};
// UI event handlers
$ctrl.onFocusout = function (event) {
// Because the component includes multiple focusable
// elements, this handler can be triggered when the
// user moves from one element to another within
// the main container. That's not really a focusout
// event for the whole component, so we want to
// ignore those interactions.
if ($element[0].contains(event.relatedTarget)) {
return;
}
$ctrl.closeMenu();
// Angular doesn't automatically run a digest if
// the event's target is outside of the component
// element. This occurs, for example, if the
// user clicks outside the element.
$scope.$apply();
};
$ctrl.onKeyup = function (event) {
switch(event.keyCode) {
case $mdConstant.KEY_CODE.ESCAPE:
$ctrl.closeMenu();
document.activeElement.blur();
$scope.$apply();
break;
case $mdConstant.KEY_CODE.RIGHT_ARROW:
case $mdConstant.KEY_CODE.DOWN_ARROW:
$ctrl.navigateForward();
break;
case $mdConstant.KEY_CODE.LEFT_ARROW:
case $mdConstant.KEY_CODE.UP_ARROW:
$ctrl.navigateBackward();
break;
}
};
$ctrl.onClickSelection = function () {
$ctrl.toggleMenu();
};
$ctrl.onChangeFilter = function () {
$ctrl.updateHighlights();
$ctrl.updateHighlightedOptions();
};
$ctrl.onClickAll = function () {
// Don't emit change events for
// each change but rather keep track
// of all changes and emit a single event
// after all options have been processed.
var hasChanged = false;
// If all the visible options are already
// selected, this event de-selects them.
if ($ctrl.isSelectionComplete()) {
$ctrl.applyFnToOptions(function deselectOption (opt) {
if (!opt.suppressed && !opt.muted) {
$ctrl.deselectOption(opt, true);
hasChanged = true;
}
}, $ctrl.options, true);
} else {
$ctrl.applyFnToOptions(function selectOption (opt) {
if (!opt.suppressed && !opt.muted) {
$ctrl.selectOption(opt, true);
hasChanged = true;
}
}, $ctrl.options, true);
}
// If any options were updated, inform
// the parent controller that the
// selection set has changed.
if (hasChanged) {
$ctrl.emitChange();
}
};
// Child component event handlers
$ctrl.onOptionChange = function (event) {
if (!$ctrl.multiple) {
if (event.option.selected) {
$ctrl.applyFnToOptions(function (opt) {
if (opt !== event.option) {
$ctrl.deselectOption(opt, true);
}
});
var selection = $element.find('nested-select-selection');
if (selection.length) {
$ctrl.setFocus(selection[0]);
}
} else {
$ctrl.clearFocus();
}
$ctrl.closeMenu();
}
$ctrl.emitChange();
};
// Emit events to parent components
$ctrl.emitChange = function () {
$ctrl.onChange({$event: {selectedValues: $ctrl.getSelectionValues()}});
};
// Utility functions
$ctrl.applyFnToOptions = function (fn, opts, excludeCollapsed) {
var opts = opts || $ctrl.options;
opts.forEach(function applyFnToOption (opt) {
fn(opt);
if (opt.children && (!opt.collapsed || !excludeCollapsed)) {
$ctrl.applyFnToOptions(fn, opt.children, excludeCollapsed);
}
});
};
$ctrl.isSelectionComplete = function () {
var all = true;
$ctrl.applyFnToOptions(function checkOptionSelectedState (opt) {
all = all && (opt.selected || opt.suppressed || opt.muted);
}, $ctrl.options, true);
return all;
};
$ctrl.getSelectionText = function () {
var selection = [];
$ctrl.applyFnToOptions(function accumulateSelectedText (opt) {
if (opt.selected) {
selection.push(opt.text);
}
});
return selection.join(", ");
};
$ctrl.getSelectionValues = function () {
var selection = [];
$ctrl.applyFnToOptions(function accumulateSelectedValues (opt) {
if (opt.selected) {
selection.push(opt.value);
}
});
return selection;
};
$ctrl.hasValue = function () {
var hasValue = false;
$ctrl.applyFnToOptions(function accumulateSelectedValues (opt) {
if (opt.selected) {
hasValue = true;
}
});
return hasValue;
};
$ctrl.hasFocus = function () {
return $element[0].contains(document.activeElement);
};
$ctrl.setFocus = function (element, delay) {
delay = delay || 0;
// Because the controller is already watching for
// focus events on the entire component, make sure
// that Angular doesn't run the digest loop here.
// Otherwise Angular throws an already in progress
// error.
// See https://docs.angularjs.org/error/$rootScope/inprog
$timeout(function () {
element.focus();
}, delay, false);
};
$ctrl.clearFocus = function () {
// Because the controller is already watching for
// focusout events on the entire component, make sure
// that Angular doesn't run the digest loop here.
// Otherwise Angular throws an already in progress
// error.
// See https://docs.angularjs.org/error/$rootScope/inprog
$timeout(function () {
document.activeElement.blur();
}, 0, false);
};
$ctrl.getFocusableNodes = function () {
var nodes = Array.prototype.slice.call($element[0].querySelectorAll([
"nested-select-selection",
"input",
"button",
"md-checkbox"
].join(",")));
var hidden = new Set($element[0].querySelectorAll([
".filtering button",
"md-checkbox[disabled]",
"nested-select-options.collapsed button",
"nested-select-options.collapsed md-checkbox"
].join(",")));
return nodes.filter(function (node) {
return !hidden.has(node);
});
};
$ctrl.getFocusedIndex = function (nodes) {
var curIdx = -1;
nodes.some(function (node, idx) {
if (node === document.activeElement) {
curIdx = idx;
return true;
}
});
return curIdx;
};
// Component logic
$ctrl.toggleMenu = function () {
if ($ctrl.showMenu) {
$ctrl.closeMenu();
} else {
$ctrl.openMenu();
}
};
$ctrl.openMenu = function () {
$ctrl.showMenu = true;
var search = $element.find('input');
if (search.length) {
$ctrl.setFocus(search[0], 400);
}
};
$ctrl.closeMenu = function () {
$ctrl.showMenu = $ctrl.fixedMenu;
};
$ctrl.updateHighlights = function () {
var filter = $ctrl.filter;
if (filter) {
// Convert the text input from the search element
// into a regular expression to support features
// such as case-insensitivity and match highlighting.
// Since the ultimate result is a regular expression,
// also give users the ability to enter their own
// regular expressions directly. User-entered regular
// expressions start and end with a `/` and have at
// least one character between them.
if (filter.match(/^\/.+\/$/)) {
// Already a regular expression, so just
// remove the delimiters.
filter = filter.slice(1, -1);
} else {
// Not a regular expression, so escape any
// characters that a regular expression would
// view as control characters.
filter = filter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
// Add grouping parentheses around the search text
// so that matching text can be highlighted.
$ctrl.highlights = new RegExp("(" + filter + ")", "gi");
} else {
$ctrl.highlights = false;
}
};
$ctrl.applyHighlightsToOptions = function (opts) {
// Recurse through the options tree and update
// each option's status with respect to highlighting.
// An option is fully suppressed if it is not
// itself highlighted and has no children that
// are highlighted. If an option is not itself
// highlighted but has highlighted children, it
// is not suppressed but muted.
// Returns a boolean indicating whether or
// not any of the options at the current level
// are highlighted or have highlighted children.
return opts.reduce(function accumulateHighlights(anyHighlights, opt) {
// Does the current option have any highlighted children?
var childHighlights = opt.children && $ctrl.applyHighlightsToOptions(opt.children);
// Should the current option itself be highlighted?
var isHighlighted = $ctrl.highlights.test(opt.text);
// Since regex is global, be sure to reset after test()
$ctrl.highlights.lastIndex = 0;
// If highlighting is needed, do it; otherwise just
// use the plain text.
if (isHighlighted) {
opt.highlightedText = opt.text.replace($ctrl.highlights, "<b>$1</b>");
} else {
opt.highlightedText = opt.text;
}
// Update the option status.
opt.suppressed = !isHighlighted && !childHighlights;
opt.muted = !isHighlighted && childHighlights;
// Update accumulating value by logically ORing
// the highlight status of the current option and
// its children
return anyHighlights || isHighlighted || childHighlights;
}, false);
};
$ctrl.updateHighlightedOptions = function () {
if ($ctrl.highlights) {
// If there are highlights to apply, do so
$ctrl.applyHighlightsToOptions($ctrl.options);
} else {
// No highlights to apply so just reset
// the option status
$ctrl.applyFnToOptions(function (opt) {
opt.highlightedText = opt.text;
opt.suppressed = false;
opt.muted = false;
});
}
};
$ctrl.selectOption = function (opt, suppressChange) {
if (!opt.selected) {
if (!$ctrl.multiple) {
$ctrl.applyFnToOptions(function (opt) {
$ctrl.deselectOption(opt, true);
});
$ctrl.closeMenu();
}
if (!suppressChange) {
$ctrl.emitChange();
}
opt.selected = true;
}
};
$ctrl.deselectOption = function (opt, suppressChange) {
var changed = opt.selected;
opt.selected = false;
if (changed && !suppressChange) {
$ctrl.emitChange();
}
};
$ctrl.navigateForward = function () {
var nodes = $ctrl.getFocusableNodes();
var curIdx = $ctrl.getFocusedIndex(nodes);
if (curIdx !== -1 && curIdx < nodes.length - 1) {
$ctrl.setFocus(nodes[curIdx + 1]);
}
};
$ctrl.navigateBackward = function () {
var nodes = $ctrl.getFocusableNodes();
var curIdx = $ctrl.getFocusedIndex(nodes);
if (curIdx > 0) {
$ctrl.setFocus(nodes[curIdx - 1]);
}
};
}
})
.component('nestedSelectOptions', {
bindings: {
options: '<',
collapsible: '<',
multiple: '<',
depth: '<',
lastSibling: '<',
onChange: '&'
},
template: `
<nested-select-option ng-repeat="option in $ctrl.options">
<nested-select-text ng-if="!option.suppressed">
<nested-select-guide ng-if="$ctrl.showCollapseSpacer(option)" >
<ng-md-icon class="nested-select-icon" icon="icon-nested-select-space" size="40" style="fill: rgba(0,0,0,0.12)"></ng-md-icon>
</nested-select-guide>
<nested-select-guide ng-repeat="guide in $ctrl.countPrefixGuides(option) track by $index">
<ng-md-icon class="nested-select-icon" icon="{{$ctrl.getPrefixGuideIcon(option, $index)}}" size="40" style="fill: rgba(0,0,0,0.12)"></ng-md-icon>
</nested-select-guide>
<nested-select-guide ng-if="$ctrl.showGuide(option)" >
<ng-md-icon class="nested-select-icon" icon="{{$ctrl.getGuideIcon(option)}}" size="40" style="fill: rgba(0,0,0,0.12)"></ng-md-icon>
</nested-select-guide>
<md-button class="md-icon-button nested-select-collapse" ng-if="$ctrl.showCollapse(option)"
aria-label="collapse/expand" ng-click="$ctrl.onCollapseClick(option)" ng-class="{collapsed: option.collapsed}">
<ng-md-icon icon="keyboard_arrow_down" style="fill: currentColor"></ng-md-icon>
</md-button>
<md-checkbox ng-if="!option.suppressed" ng-model="option.selected" ng-disabled="option.muted" ng-change="$ctrl.onCheckboxChange(option)" aria-label="{{option.text}}">
<!-- <md-tooltip>{{option.text}}</md-tooltip> -->
<span ng-bind-html="option.highlightedText"></span>
</md-checkbox>
</nested-select-text>
<nested-select-options ng-if="option.children && option.children.length" options="option.children"
collapsible="$ctrl.collapsible" multiple="$ctrl.multiple" depth="$ctrl.depth + 1" last-sibling="$ctrl.lastSibling + (+$last)"
ng-class="{collapsed: option.collapsed}" on-change="$ctrl.onChildChange($event)">
</nested-select-options>
</nested-select-option>
`,
controller: function nestedSelectOptions ($element, $attrs) {
var $ctrl = this;
$ctrl.$onInit = function () {
$ctrl.collapsed = false;
$ctrl.lastSibling = $ctrl.lastSibling || "";
}
$ctrl.$onChanges = function (changes) {
if (changes.options && !changes.options.isFirstChange()) {
$ctrl.options = angular.copy($ctrl.options);
}
};
// Utilities for adding visual guides and buttons
// Should a spacer be added to account for collapse/expand button?
$ctrl.showCollapseSpacer = function (opt) {
return $ctrl.collapsible && $ctrl.depth;
};
// How many guides to insert in front of a text label?
$ctrl.countPrefixGuides = function () {
// retuns an empty array with the appropriate number of
// elements so that ng-repeat can iterate over it
return $ctrl.depth > 0 ? new Array($ctrl.depth - 1) : [];
};
// What icon should be used at the current guide depth?
$ctrl.getPrefixGuideIcon = function (opt, depthIdx) {
if ($ctrl.multiple || $ctrl.lastSibling[depthIdx + 1] === '1') {
return "icon-nested-select-space";
} else {
return "icon-nested-select-vbar";
}
};
// Should a guide be added for this option?
$ctrl.showGuide = function (opt) {
return !$ctrl.showCollapse(opt) && (!$ctrl.multiple && ($ctrl.depth > 0) || $ctrl.collapsible);
};
// What icon should be used for the current option's guide?
$ctrl.getGuideIcon = function (opt) {
if ($ctrl.collapsible && (!opt.children || opt.children.length === 0) && ($ctrl.depth === 0 || $ctrl.multiple)) {
return "icon-nested-select-space";
} else if ($ctrl.options[$ctrl.options.length - 1] === opt) {
return "icon-nested-select-el";
} else {
return "icon-nested-select-midbar";
}
};
// Should the collapse/expand button be included?
$ctrl.showCollapse = function (opt) {
return $ctrl.collapsible && opt.children && (opt.children.length > 0);
};
$ctrl.onCheckboxChange = function (opt) {
$ctrl.onChange({$event: {option: opt}});
};
$ctrl.onCollapseClick = function (opt) {
opt.collapsed = !opt.collapsed;
};
$ctrl.onChildChange = function (event) {
$ctrl.onChange({$event: {option: event.option}});
};
}
});
Also see: Tab Triggers