Skip to content

Instantly share code, notes, and snippets.

@mattduffield
Forked from jdanyow/app.html
Last active September 28, 2017 05:53
Show Gist options
  • Select an option

  • Save mattduffield/51180ab8fe05c90572a3c9fe0220cdda to your computer and use it in GitHub Desktop.

Select an option

Save mattduffield/51180ab8fe05c90572a3c9fe0220cdda to your computer and use it in GitHub Desktop.
Aurelia Gist - Tree
<template>
<require from="./tree"></require>
<require from="./context-menu"></require>
<h1>${message}</h1>
<tree items.bind="items"></tree>
<context-menu></context-menu>
</template>
export class App {
message = 'Hello World!';
items = [
{
"name": "app-contacts",
"container": "app-contacts",
"folderName": "app-contacts",
"fileName": "app-contacts",
"isProject": true,
"isFolder": true,
"isOpen": true,
"items": [
{
"name": "src",
"isNew": false,
"container": "app-contacts",
"folderName": "",
"isFolder": true,
"items": [
{
"name": "app.html",
"ext": "html",
"container": "app-contacts",
"folderName": "app-contacts",
"fileName": "app.html",
"items": []
},
{
"name": "app.js",
"ext": "js",
"container": "app-contacts",
"folderName": "app-contacts",
"fileName": "app.js",
"items": []
}
],
"isOpen": true,
"ext": "src"
},
{
"name": "index.html",
"ext": "html",
"container": "app-contacts",
"folderName": "app-contacts",
"fileName": "index.html",
"isOpen": true,
"items": []
}
]
},
{
"name": "contact-manager",
"container": "contact-manager",
"folderName": "contact-manager",
"fileName": "contact-manager",
"isProject": true,
"isFolder": true,
"isOpen": true,
"items": [
{
"name": "src",
"container": "contact-manager",
"folderName": "contact-manager",
"isFolder": true,
"items": [
{
"name": "resources",
"isNew": false,
"container": "contact-manager",
"folderName": "",
"isFolder": true,
"items": [
{
"name": "elements",
"isNew": false,
"container": "contact-manager",
"folderName": "",
"isFolder": true,
"items": [
{
"name": "my-table",
"isNew": false,
"container": "contact-manager",
"folderName": "",
"isFolder": true,
"items": [
{
"name": "my-table.html",
"isNew": false,
"container": "contact-manager",
"folderName": "contact-manager",
"fileName": "",
"items": [],
"ext": "html"
},
{
"name": "my-table.js",
"isNew": false,
"container": "contact-manager",
"folderName": "contact-manager",
"fileName": "",
"items": [],
"ext": "js"
}
],
"isOpen": true,
"ext": "my-table"
}
],
"isOpen": true,
"ext": "elements"
}
],
"isOpen": true,
"ext": "resources"
},
{
"name": "app.html",
"ext": "html",
"container": "contact-manager",
"folderName": "contact-manager",
"fileName": "app.html",
"items": []
},
{
"name": "app.js",
"ext": "js",
"container": "contact-manager",
"folderName": "contact-manager",
"fileName": "app.js",
"items": []
}
],
"isOpen": true
},
{
"name": "index.html",
"ext": "html",
"container": "contact-manager",
"folderName": "contact-manager",
"fileName": "index.html",
"isOpen": true,
"items": []
},
{
"name": "README.md",
"ext": "md",
"container": "contact-manager",
"folderName": "contact-manager",
"fileName": "README.md",
"isOpen": true,
"items": []
}
]
}
];
}
export class ApplicationService {
user = {
sourceControl: {}
};
contextMenu = [];
initCurrentRecord(record) {
this.currentRecord = record;
}
}
.context-menu {
display: none;
}
.context-menu-show {
position: absolute;
display: block;
}
.context-menu > li > a {
cursor: pointer;
}
<template>
<require from="./context-menu.css"></require>
<ul class="context-menu dropdown-menu"
role="menu"
css="left: ${left}px; top: ${top}px;">
<li repeat.for="item of appService.contextMenu"
if.bind="item.showAction(appService) & signal:'name-signal'"
class="${item.isDivider ? 'divider' : ''}">
<a if.bind="!item.isDivider" tabindex="-1"
click.delegate="processContextMenu(item.action, item.actionArgs, item.actionFn)">
<i class="fa fa-${item.icon}"></i>
${item.title}
</a>
</li>
</ul>
</template>
import {TaskQueue, customElement, bindable} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import {ApplicationService} from './application-service';
import {UtilService} from './util-service';
@customElement('context-menu')
export class ContextMenu {
static inject = [Element, TaskQueue, EventAggregator, ApplicationService, UtilService];
@bindable left;
@bindable top;
currentMenu;
copy;
constructor(element, taskQueue, messageBus, appService, utilService) {
this.element = element;
this.taskQueue = taskQueue;
this.messageBus = messageBus;
this.appService = appService;
this.utilService = utilService;
this.messageBus.subscribe('tree-node:contextmenu', (payload) => {
this.onContextMenu(payload.e, payload.item, payload.parent, payload.index);
});
}
attached() {
// We only want to add this once.
if (!document.body.getAttribute('context-menu-init')) {
document.addEventListener('click', this.handleClick.bind(this), true);
// document.addEventListener('contextmenu', this.handleContextMenu.bind(this), true);
// this ensures we only do this once.
document.body.setAttribute('context-menu-init', true);
}
}
detached() {
document.removeEventListener('click', this.handleClick.bind(this), true);
// document.removeEventListener('contextmenu', this.handleContextMenu.bind(this), true);
}
handleClick(e) {
if (this.currentMenu) {
this.currentMenu.classList.remove('context-menu-show');
}
return true;
}
onContextMenu(e, item, parent, index) {
let content = item.name;
let container = item.container;
// console.log('item', item, 'container', item.container, 'content', item.name);
let fileName = item.fileName;
this.appService.contextMenu = [];
this.appService.contextMenu = [
{
icon: "map-signs",
title: "Configure Router",
actionFn: () => this.appService.configureRouter(container, fileName),
showAction: () => !item.isReadonly && item.ext === 'js'
},
{
icon: "code-fork",
title: "Add External Resource",
actionFn: () => this.appService.manageExternalResources(container, fileName),
showAction: () => !item.isReadonly && item.name === 'index.html'
},
{
isDivider: true,
showAction: () => (!item.isReadonly && item.name === 'index.html') ||
(!item.isReadonly && item.ext === 'js')
},
{
icon: "clone",
title: "Clone Project",
actionFn: () => {
// this.appService.addCloneProject(container);
this.messageBus.publish('clone-project', container)
},
showAction: () => item.isProject || (item.isProject && item.isReadonly)
},
{
icon: "trash",
title: "Delete Project",
actionFn: () => this.appService.removeProject(container),
showAction: () => item.isProject
},
{
icon: "files-o",
title: "Copy Files",
actionFn: () => this.appService.copyFiles(container),
showAction: () => item.isProject
},
{
isDivider: true,
showAction: () => item.isProject
},
{
icon: "puzzle-piece",
title: "Add View/ViewModel Files",
actionFn: () => this.appService.addViewViewModelFiles(container),
showAction: () => item.isProject
},
{
icon: "link",
title: "Manage Linked Files",
actionFn: () => this.appService.addLinkedFile(container),
showAction: () => item.isProject
},
{
isDivider: true,
showAction: () => item.isProject
},
{
icon: "download",
title: "Export",
actionFn: () => {
// this.appService.downloadProject(container);
this.messageBus.publish('download-project', container)
},
showAction: () => item.isProject || (item.isProject && item.isReadonly)
},
{
icon: "code-fork",
title: "Commit",
actionFn: () => this.appService.addNewCommit(container),
showAction: () => item.isProject && this.appService.user.sourceControl
},
{
isDivider: true,
showAction: () => item.isProject || (item.isProject && item.isReadonly)
},
{
icon: "pencil",
title: "Rename",
actionFn: () => {
item.isNew = true;
setTimeout(() => {
let input = document.querySelector('.new-item');
if (input) {
input.focus();
input.select();
}
}, 50);
},
showAction: () => true
},
{
icon: "trash",
title: "Delete",
actionFn: () => {
parent.items.splice(index, 1);
},
showAction: () => true
},
{
icon: "files-o",
title: "Copy",
actionFn: () => {
this.copy = JSON.parse(JSON.stringify(item));
},
showAction: () => !item.isProject
},
{
icon: "clipboard",
title: "Paste",
actionFn: () => {
// Need to update properties on the target.
let func = (c) => {
// chart-sample/src~bar-chart.js
c.container = item.container;
c.folderName = item.folderName;
c.href = c.href.replace(/c.container/,item.container);
// c.href = `/${c.folderName}/${c.name}`;
};
this.utilService.recurseItems([this.copy], func);
item.items.push(this.copy);
this.taskQueue.queueMicroTask(() => {
item.items.sort(this.utilService.nameCompare);
});
// this.messageBus.publish('tree-node:paste', {item:this.copy, parent: parent, index: index});
},
showAction: () => this.copy
}
];
this.showContextMenu(e);
return false;
}
showContextMenu(e) {
if (this.appService.contextMenu.length === 0) return;
this.currentMenu = this.element.querySelector('.context-menu');
if (this.currentMenu) {
this.left = e.clientX;
this.top = e.clientY;
setTimeout(() => {
this.currentMenu.classList.add('context-menu-show');
}, 250);
// console.log('target', e.target, 'left', this.left, 'top', this.top);
}
}
processContextMenu(action, actionArgs, actionFn) {
// console.log('context-menu:processContextMenu', action, actionArgs);
if (actionFn) {
actionFn();
} else if (actionArgs) {
let args = JSON.parse(actionArgs);
// console.log('context-menu:processContextMenu', action, ...args);
this.appService[action](...args);
} else {
this.appService[action]();
}
}
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body aurelia-app>
<h1>Loading...</h1>
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/config.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script>
<script>
require(['aurelia-bootstrapper']);
</script>
</body>
</html>
<template>
<ul>
<li repeat.for="item of items"
class="filterable ${item.isActive ? 'is-active' : ''}"
data-items-length="${item.items.length}"
data-tag="${item.name}"
data-tags="${item.tags.join(' ')}"
click.delegate="toggleOpen($event, item)"
if.bind="!item.isConfigurationFile">
<label class="item" for="${item.id}">
<div if.bind="item.items.length > 0 || item.isFolder"
class="${item.isProject ? 'is-project' : ''} ${item.isReadonly ? 'is-readonly' : ''} ${item.isFolder ? 'is-folder' : ''}"
data-container="${item.container}"
data-file-name="${item.fileName}"
contextmenu.trigger="onContextMenu($event, item, $parent, $index)"
click.delegate="setCurrentView($event, item)">
<div class="no-events" if.bind="item.isNew">
<input class="new-item form-control"
blur.trigger="onBlur($event, item, $parent, $index)"
keydown.trigger="onKeydown($event, item, $parent, $index)"
value.bind="item.name" />
</div>
<div class="no-eventsx" if.bind="!item.isNew">
<i class="fa fa-${item.isProject ? item.isOpen ? 'minus-square' : 'plus-square' : item.isOpen ? 'folder-open' : 'folder'} fa-fw no-events"></i>
<span class="no-events">${item.name}</span>
<span class="new-folder" click.delegate="newFolder($event, item)">
<i class="fa fa-folder-o"></i>
</span>
<span class="new-file" click.delegate="newFile($event, item)">
<i class="fa fa-file-o"></i>
</span>
</div>
</div>
<div if.bind="item.items.length == 0 && !item.isFolder">
<div if.bind="item.isNew">
<input class="new-item form-control"
blur.trigger="onBlur($event, item, $parent, $index)"
keydown.trigger="onKeydown($event, item, $parent, $index)"
value.bind="item.name" />
</div>
<div if.bind="!item.isNew">
<i class="fa fa-${mapMimeType(item) & signal:'name-signal'} fa-fw no-events"></i>
<a href.bind="item.href"
class="${item.isReadonly ? 'is-readonly' : ''}"
data-container="${item.container}"
data-file-name="${item.fileName}"
contextmenu.trigger="onContextMenu($event, item, $parent, $index)"
click.delegate="setCurrentView($event, item)">
<span class="no-events">${item.name}</span>
</a>
<input if.bind="canInteract" type="checkbox"
class="tree-selector"
checked.bind="item.isSelected"
change.delegate="toggleSelection(item)">
<i if.bind="item.isLinkedFile"
class="fa fa-link is-linked-file margin-right-10"
title="Linked File"></i>
</div>
</div>
<div class="overlay"></div>
</label>
<input if.bind="item.items.length > 0 || item.isFolder"
class="tree-folder"
type="checkbox"
id="${item.id}"
checked.bind="item.isOpen">
<tree-node items.bind="item.items"
can-interact.bind="canInteract"
can-set-current-view.bind="canSetCurrentView"
toggle-selection-topic.bind="toggleSelectionTopic">
</tree-node>
</li>
</ul>
</template>
import {TaskQueue, bindable} from 'aurelia-framework';
import {BindingSignaler} from 'aurelia-templating-resources';
import {EventAggregator} from 'aurelia-event-aggregator';
import {ApplicationService} from './application-service';
import {UtilService} from './util-service';
export class TreeNode {
static inject = [TaskQueue, BindingSignaler, EventAggregator, ApplicationService, UtilService];
@bindable items = [];
@bindable folderName = '';
@bindable filterable = '';
@bindable canInteract = true;
@bindable canSetCurrentView = true;
@bindable toggleSelectionTopic = 'tree-node:toggle-selection';
constructor(taskQueue, signaler, messageBus, appService, utilService) {
this.taskQueue = taskQueue;
this.signaler = signaler;
this.messageBus = messageBus;
this.appService = appService;
this.utilService = utilService;
this.messageBus.subscribe('tree-node:paste', (payload) => {
console.log('paste', payload);
// handle paste here...
payload.parent.items.push(payload.item);
this.taskQueue.queueMicroTask(() => {
payload.parent.items.sort(this.compare);
});
});
}
onBlur(e, item, parent, index) {
// console.log('onBlur', e, item, parent, index);
if (!item.name) {
parent.items.splice(index, 1);
} else {
this.createItem(item, parent);
}
}
onKeydown(e, item, parent, index) {
// console.log('keydown', e, item, index);
if (e.keyCode === 27 /* ESCAPE */) {
e.preventDefault(); // Ensure it is only this code that runs
e.target.blur();
} else if (e.keyCode === 13 /* ENTER */) {
e.preventDefault(); // Ensure it is only this code that runs
e.target.blur();
} else {
return true;
}
return true;
}
createItem(item, parent) {
console.log('item', item, 'parent', parent);
item.isNew = false;
let origFolder = '';
if (item.isFolder) {
let folders = item.folderName.split('/');
if (folders.length > 1) {
origFolder = folders.pop();
item.folderName = `${folders.join('/')}/${item.name}`;
} else {
item.folderName = `${item.container}/${item.name}`;
}
// Need to build an array from the recursion for all the children
// to be copied to the new location using copyBlob...
let func = (c) => {
c.folderName = c.folderName.replace(origFolder, item.name);
if (!c.isFolder) {
c.href = c.href.replace(origFolder, item.name);
c.fileName = c.fileName.replace(origFolder, item.name);
}
};
this.utilService.recurseItems(item.items, func);
} else {
let ext = item.name.split('.').pop();
item.ext = ext;
item.fileName = `${item.folderName}/${item.name}`;
let folders = item.folderName.split('/');
if (folders.length > 0) {
item.href = `/${item.folderName}~${item.name}`;
} else {
item.href = `/${item.folderName}/${item.name}`;
}
}
this.taskQueue.queueMicroTask(() => {
parent.items.sort(this.compare);
});
// this.signaler.signal('name-signal');
}
compare(a, b) {
let nameA = a.name.toLowerCase();
let nameB = b.name.toLowerCase();
if (a.isFolder) {
nameA = a.name.toUpperCase();
}
if (b.isFolder) {
nameB = b.name.toUpperCase();
}
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
}
newFolder(e, item) {
e.preventDefault();
e.stopPropagation();
item.isOpen = true;
item.items.push({
name: '',
isNew: true,
container: `${item.container}`,
folderName: `${item.folderName}`,
isFolder: true,
items: []
});
setTimeout(() => {
let input = document.querySelector('.new-item');
if (input) {
input.focus();
input.select();
}
}, 50);
}
newFile(e, item) {
e.preventDefault();
e.stopPropagation();
item.isOpen = true;
item.items.push({
name: '',
isNew: true,
container: `${item.container}`,
folderName: `${item.folderName}`,
fileName: '',
items: []
});
setTimeout(() => {
let input = document.querySelector('.new-item');
if (input) {
input.focus();
input.select();
}
}, 50);
}
paste(item) {
}
toggleOpen(e, item) {
e.preventDefault();
e.stopPropagation();
// console.log('item.name', item.name);
if (item.isFolder) {
item.isOpen = !item.isOpen;
return true;
} else {
return false;
}
}
onContextMenu(e, item, parent, index) {
e.preventDefault();
// console.log('tree-node:onContextMenu', item);
if (item.isFolder || item.isProject) {
let container = item.container;
let folder = item.folderName;
let recordName = `${container}`;
if (folder && container !== folder) {
recordName = `${container} > ${folder.replace(/\//g, ' > ')}`;
}
let record = { name: `${recordName}`, container: container, folder: folder };
this.appService.initCurrentRecord(record);
}
let payload = {
e: e,
item: item,
parent: parent,
index: index
};
this.messageBus.publish('tree-node:contextmenu', payload);
return true;
}
setCurrentView(e, item) {
// console.log('tree-node:setCurrentView', item);
if (this.canInteract && (item.isFolder || item.isProject)) {
let container = item.container;
let folder = item.folderName;
let recordName = `${container}`;
if (folder && container !== folder) {
recordName = `${container} > ${folder.replace(/\//g, ' > ')}`;
}
let record = { name: `${recordName}`, container: container, folder: folder };
this.appService.initCurrentRecord(record);
// console.log('container:', container, 'folder:', folder);
}
if (this.canSetCurrentView) {
let payload = {
item: item
};
// console.log('tree-node:setCurrentView - item', item);
// this.messageBus.publish('tree-node:set-active', payload);
// console.log('...publishing - tree-node:set-active');
this.messageBus.publish('tree-node:set-active', item);
return true;
} else if (item.items.length > 0) {
// console.log('tree-node:setCurrentView - items.length > 0', item);
return true;
}
}
toggleSelection(item) {
// console.log('tree-node:toggleSelection', item);
let payload = {
item: item
};
this.messageBus.publish(this.toggleSelectionTopic, payload);
return true;
}
mapMimeType(item) {
let ext = item.ext || '';
ext = ext.toLowerCase();
let map = {
css: 'css3',
js: 'code',
json: 'code',
html: 'html5'
}
let result = map[ext];
if (result) {
return result;
} else if (this.utilService.isTextMimeTypeExt(ext)) {
result = 'file-text-o';
} else if (this.utilService.isImgMimeTypeExt(ext)) {
result = 'file-image-o';
} else if (this.utilService.isFontMimeTypeExt(ext)) {
result = 'font';
} else {
result = 'file-o';
}
return result;
}
}
:root {
/*--tree-bg-color: transparent;*/
/*--tree-color: lightgray;*/
--tree-bg-color: transparent;
--tree-color: black;
--tree-height-offset: 110px;
}
.no-events {
user-select: none;
pointer-events: none;
}
.tree {
overflow: hidden;
/*width: 299px;*/
}
.tree .tree-search {
margin-bottom: 5px;
}
.tree .tree-container {
position: relative;
height: calc(100vh - var(--tree-height-offset));
background-color: var(--tree-bg-color);
border-radius: 5px;
padding: 5px;
overflow-x: hidden;
overflow-y: auto;
}
.tree ul {
margin: 0;
padding: 0;
list-style-type: none;
}
.tree ul li {
padding-left: 16px;
user-select: none;
}
.tree > .tree-container > tree-node > ul > li {
padding-left: 0;
}
.tree .new-folder {
visibility: hidden;
float: right;
}
.tree .new-file {
visibility: hidden;
float: right;
margin-right: 10px;
}
.tree .is-folder:hover .new-folder,
.tree .is-folder:hover .new-file {
visibility: visible;
}
.tree .is-folder.folder-hover .new-folder,
.tree .is-folder.folder-hover .new-file {
visibility: visible;
}
.tree li .item {
color: var(--tree-color);
position: relative;
width: 100%;
}
.tree li .item .overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #C5E7E6;
background-color: rgba(0,0,0,.3);
display: none;
left: -100px;
width: 1000px;
}
.tree li .item:hover .overlay {
display: block;
pointer-events: none;
}
.tree li .item a {
color: var(--tree-color);
text-decoration: none;
}
.tree li.is-active > .item > .overlay {
display: block;
pointer-events: none;
background-color: rgba(0,0,0,.3);
}
.tree .is-linked-file {
float: right;
color: rgba(99,204,245,.5);
}
.tree input[type="checkbox"].tree-selector {
float: right;
}
.tree input[type="checkbox"].tree-folder {
position: absolute;
left: -9999px;
}
.tree input[type="checkbox"].tree-folder ~ tree-node > ul {
height: 0;
transform: scaleY(0);
}
.tree input[type="checkbox"].tree-folder:checked ~ tree-node > ul {
height: 100%;
transform-origin: top;
transition: transform .2 ease-out;
transform: scaleY(1);
}
<template>
<require from="./tree.css"></require>
<require from="./tree-node"></require>
<div id="treeFileDrop" class="tree">
<div class="tree-search">
<div class="input-group">
<input class="form-control"
value.bind="searchFilter & debounce:800"
placeholder="Search Here...">
<span class="input-group-btn">
<button class="btn btn-default" type="button"
click.delegate="clearSearchFilter()">
<i class="fa fa-times no-events"></i>
</button>
<button class="btn btn-primary" type="button"
click.delegate="dumpJSON()">
<i class="fa fa-code no-events"></i>
</button>
</span>
</div>
</div>
<div class="tree-container">
<tree-node items.bind="items"
can-interact.bind="canInteract"
can-set-current-view.bind="canSetCurrentView"
toggle-selection-topic.bind="toggleSelectionTopic">
</tree-node>
</div>
</div>
</template>
import {bindable} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
// import {ApplicationService} from '../../../services/application-service';
export class Tree {
// static inject = [EventAggregator, ApplicationService];
static inject = [EventAggregator];
@bindable searchFilter = '';
@bindable items = [];
// @bindable filterable = '.filterable';
@bindable filterSelector = '.filterable';
@bindable canInteract = true;
@bindable canSetCurrentView = true;
@bindable toggleSelectionTopic = 'tree-node:toggle-selection';
// constructor(messageBus, appService) {
constructor(messageBus) {
this.messageBus = messageBus;
// this.appService = appService;
this.openPathSub = this.messageBus.subscribe('tree:open-path', this.openPath.bind(this));
}
attached() {
let tree = document.querySelector('.tree');
// this.searchFilter = this.appService.searchFilter;
if (this.searchFilter) {
// console.log('tree:attached - searchFilter', this.searchFilter);
this.performFilter(this.searchFilter);
}
// tree.addEventListener('mouseover2', (e) => {
// let target = e.target;
// let related = e.relatedTarget;
// let delegationSelector = '.is-folder';
// let match;
// // search for a parent node matching the delegation selector
// while (target && target != document && !( match = target.matches(delegationSelector))) {
// target = target.parentNode;
// }
// // exit if no matching node has been found
// if (!match) { return; }
// // loop through the parent of the related target to make sure that it's not a child of the target
// while (related && related != target && related != document) {
// related = related.parentNode;
// }
// // exit if this is the case
// if (related == target) { return; }
// if (target && target.classList && target.classList.contains('is-folder')) {
// target.classList.add('folder-hover');
// }
// }, false);
// tree.addEventListener('mouseout2', (e) => {
// let target = e.target;
// let related = e.relatedTarget;
// let delegationSelector = '.folder-hover';
// let match;
// // search for a parent node matching the delegation selector
// while (target && target != document && !( match = target.matches(delegationSelector))) {
// target = target.parentNode;
// }
// // exit if no matching node has been found
// if (!match) { return; }
// // loop through the parent of the related target to make sure that it's not a child of the target
// while (related && related != target && related != document) {
// related = related.parentNode;
// }
// // exit if this is the case
// if (related == target) { return; }
// if (target && target.classList) {
// target.classList.remove('folder-hover');
// }
// }, false);
}
detached() {
this.openPathSub.dispose();
}
matches( elem, selector ){
// the matchesSelector is prefixed in most (if not all) browsers
return elem.matchesSelector( selector );
}
openPath(payload) {
let nameArray = payload.path.split('/');
// Remove the last part as it is the actual file name.
let fileName = nameArray.pop();
let pathItems = this.items;
nameArray.forEach(n => {
let folder = pathItems.find(p => p.name === n);
if (folder) {
folder.isOpen = true;
pathItems = folder.items;
}
});
let file = pathItems.find(p => p.name === fileName);
if (file) {
// console.log('...publishging - tree - tree-node:set-active');
this.messageBus.publish('tree-node:set-active', file);
}
}
performFilter(filter) {
// console.log('shell:performFilter', filter, selector);
let selector = this.filterSelector;
let cards = document.querySelectorAll(selector);
// Reset filter
Array.from(cards).forEach((item, index) => {
item.classList.remove('hidden');
});
if (filter) {
// Apply filter
Array.from(cards).forEach((item, index) => {
let itemsLen = item.getAttribute('data-items-length');
let tags = item.getAttribute('data-tags').split(' ');
let attr = item.getAttribute('data-tag');
let tag = attr.toLowerCase().includes(filter.toLowerCase());
let found = tags.find(t => t.toLowerCase() == filter.toLowerCase());
if (!tag && !found && itemsLen == 0) {
item.classList.add('hidden');
}
});
}
}
clearSearchFilter(filter) {
this.searchFilter = "";
}
searchFilterChanged(newValue, oldValue) {
// console.log('searchFilterChanged', newValue);
// this.appService.searchFilter = newValue;
this.performFilter(newValue);
}
dumpJSON() {
console.log(JSON.stringify(this.items, null, 2));
}
}
export class UtilService {
constructor() {
}
/**
* This takes an HTMLElement and tries to grab the view-model off of it.
* It will either return the view-model or null.
*/
getViewModel(element) {
if (element.au && element.au.controller &&
element.au.controller.viewModel) {
return element.au.controller.viewModel;
}
return null;
}
/**
* This functions formats HTML elements into a proper
* hierarchy so that the templates is nice and clean.
* This code was taken from the following url:
* https://jsfiddle.net/buksy/rxucg1gd/
*/
formatHTML(code, stripWhiteSpaces, stripEmptyLines) {
// Parameters:
// code - (string) code you wish to format
// stripWhiteSpaces - (boolean) do you wish to remove multiple whitespaces coming after each other?
// stripEmptyLines - (boolean) do you wish to remove empty lines?
let whitespace = ' '.repeat(2); // Default indenting 2 whitespaces
let currentIndent = 0;
let char = null;
let nextChar = null;
let startTag = false;
let currentTag = '';
let tag = '';
// https://www.w3.org/TR/html5/syntax.html#void-elements
let selfClosing = [
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"keygen",
"link",
"meta",
"param",
"source",
"track",
"wbr"
];
let result = '';
for (var pos=0; pos <= code.length; pos++) {
char = code.substr(pos, 1);
nextChar = code.substr(pos+1, 1);
if (char === ' ' || char === '>') {
startTag = false;
if (tag) {
currentTag = tag;
}
tag = '';
// console.log(currentTag);
}
if (startTag) {
tag += char;
}
// if opening tag, add newline character and indention
if (char === '<' && nextChar !== '/') {
startTag = true;
result += '\n' + whitespace.repeat(currentIndent);
currentIndent++;
}
// if closing tag, add newline and indention
else if(char === '-' && nextChar === '>') {
// if there're more closing tags than opening
if(--currentIndent < 0) currentIndent = 0;
}
else if(char === '<' && nextChar === '/') {
// if there're more closing tags than opening
if(--currentIndent < 0) currentIndent = 0;
result += '\n' + whitespace.repeat(currentIndent);
}
else if (char === '>' && (nextChar === ' ' || nextChar === '<' || nextChar === '\n')) {
if (selfClosing.includes(currentTag)) {
// if there're more closing tags than opening
if(--currentIndent < 0) currentIndent = 0;
currentTag = '';
result += '\n' + whitespace.repeat(currentIndent);
}
}
// remove multiple whitespaces
else if(stripWhiteSpaces === true && char === ' ' && nextChar === ' ') {
char = '';
}
// remove empty lines
else if(stripEmptyLines === true && char === '\n' ) {
//debugger;
if(code.substr(pos, code.substr(pos).indexOf("<")).trim() === '' ) char = '';
}
result += char;
}
return result.trim();
}
/**
* This function takes in a string and removes all the designer attributes
* that are using during the creation of a screen.
*/
stripDesignerAttributes(template) {
template = template
.replace(/drag-container/g, '')
.replace(/drag-item/g, '')
.replace(/drag-div/g, '')
.replace(/drag-row/g, '')
.replace(/drag-col/g, '')
.replace(/drag-form-group/g, '')
.replace(/drag-form/g, '')
.replace(/drag-label/g, '')
.replace(/drag-input/g, '')
.replace(/drag-button/g, '');
// .replace(/draggable\=\"true\"/g, '');
return template;
}
/**
* This function takes in a string content and strips off
* the template outer tag and replaces it with one that
* the browser will render.
*/
stripForDesignerTab(content) {
content = content
// .replace(/\<template\>/, '<remove-template>')
.replace(/\<template(.*)\>/, '<remove-template$1>')
.replace(/\<\/template\>/, '</remove-template>');
return content;
}
/**
* This function takes in a string content and strips off
* the all designer specific items as well as removes the
* remove-template wrapper.
*/
stripForHtmlTab(content) {
content = content
.replace(/ drag-container/g, '')
.replace(/drag-container /g, '')
.replace(/drag-container/g, '')
.replace(/ drag-item/g, '')
.replace(/drag-item /g, '')
.replace(/drag-item/g, '')
.replace(/\<remove\-template(.*)\>/, '<template$1>')
// .replace(/\<remove\-template\>/, '<template>')
.replace(/\<\/remove\-template\>/, '</template>')
.replace(/ class="" /g, '')
.replace(/ class=""/g, '')
.replace(/class="" /g, '')
.replace(/class=""/g, '');
return content;
}
/**
* This function takes in a JSON object and converts it to a string.
* It then strips off all new lines, tabs, and spaces.
*/
stringifyAndStrip(record) {
let content = JSON.stringify(record, null, 2)
.replace(/drag-container/g, '')
.replace(/drag-item/g, '')
.replace(/\<remove\-template(.*)\>/, '<template$1>')
.replace(/\<\/remove\-template\>/, '</template>')
.replace(/\n/g, '')
.replace(/\\n/g, '')
.replace(/\t/g, '')
.replace(/\\t/g, '')
.replace(/ /g, '')
.replace(/class=\\"\\"/g, '');
return content;
}
/**
* This function provides the ability to stringify
* an object that has functions. It was taken from
* the following url:
* https://gist.github.com/cowboy/3749767
*/
stringify(obj, prop) {
let placeholder = '____PLACEHOLDER____';
let fns = [];
let json = JSON.stringify(obj, function(key, value) {
if (typeof value === 'function') {
fns.push(value);
return placeholder;
}
return value;
}, 2);
json = json.replace(new RegExp('"' + placeholder + '"', 'g'), function(_) {
return fns.shift();
});
return json;
// return 'this["' + prop + '"] = ' + json + ';';
}
camelCaseToProperCase(input) {
return input.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
}
fileCaseToProperCase(input) {
let words = input.split('-');
words = words.map(w => w.replace(/^./, (str) => str.toUpperCase()));
return words.join('');
}
camelCaseToAttribute(input) {
let fmt = input.replace(/([A-Z])/g, '-$1');
return fmt.toLowerCase();
}
preSpaceCleanup(template) {
return template.replace(/(\s{2,})/g, ' ');
}
postSpaceCleanup(template) {
let exp = /(class=")([a-zA-Z0-9\-]+)(\s+)(")/g;
template = template.replace(exp, '$1$2$4');
let exp2 = /([\w-\.]+=")([\w\.\(\)]+)(\s+)(")/g;
template = template.replace(exp2, '$1$2$4');
return template;
}
createChild(template) {
var child = document.createElement('div');
child.innerHTML = template.trim();
child = child.firstChild;
return child;
}
appendChild(target, child) {
target.appendChild(child);
}
createAndAppendChild(template, target) {
let child = this.createChild(template);
this.appendChild(target, child);
return child;
}
parseFunction(input) {
// console.log('util-service:parseFunction - input', input);
let funcReg = /function *\(([^()]*)\)\s*{([\s\S]*)}/gmi;
let match = funcReg.exec(input);
// console.log('input match', match);
if(match) {
let result = new Function(match[1].split(','), match[2]);
// console.log('result', result);
return result;
}
return null;
}
compileScript(script) {
let result = {};
let cls = script;
let injectReg = /\@inject\(([\w\,\s]*)\)/gmi;
let injectMatch = injectReg.exec(cls);
if (injectMatch) {
let inject = injectMatch[1];
cls = cls.replace(`@inject(${inject})`, '');
result.inject = inject;
}
result.fn = eval(`(${cls})`);
return result;
}
// compileScriptUsingFunction(script) {
// let result = {};
// let cls = script;
// let injectReg = /\@inject\(([\w\,\s]*)\)/gmi;
// let injectMatch = injectReg.exec(cls);
// if (injectMatch) {
// let inject = injectMatch[1];
// cls = cls.replace(`@inject(${inject})`, '');
// result.inject = inject;
// }
// let nameReg = /class ([\w]*) {/gmi;
// let nameMatch = nameReg.exec(cls);
// if (nameMatch) {
// let name = nameMatch[1];
// cls = `
// ${cls.trim()}
// return {
// ${name}: ${name}
// };
// `;
// let func = new Function(cls);
// let fn = func();
// result.fn = fn[name];
// return result;
// }
// return null;
// }
classBuilder(container, fn, inject) {
let injectors = [];
inject.split(',').forEach((item) => {
let di = container.get(item.trim());
// console.log(item.trim(), di);
injectors.push(di);
});
// console.log('util-service:classBuilder', injectors, container);
let instance = new fn(...injectors);
return instance;
}
/**
* The following method receives a JSON object representing
* metadata markup for a template. It then converts the object
* to a string and looks for an matches to build up a params
* array. Once it has finished iterating over all the matches,
* it then builds up the args array.
* Currently, this implementation only supports top-level
* references, e.g. 'this.modal'
*/
processTemplate(meta, context) {
// console.log('processTemplate', meta, context);
let counter = 0;
context = context || this;
let markup = JSON.stringify(meta);
let fields = markup.match(/\${(.+?)}/g) || [];
let args = [];
let params = [];
// console.log('processTemplate - fields', fields);
fields.forEach((field,index,list) => {
let inner = field
.replace(/\${/g, '')
.replace(/}/g, '');
let dot = inner.split('.');
let objVal = {};
dot.forEach((c) => {
if (c == 'this'){
objVal = context;
} else {
objVal = objVal[c];
}
});
let f = `field${counter}`;
counter++;
if (!params.includes(f)) {
params.push(f);
}
args.push(objVal);
markup = markup.replace(field, '${' + f + '}');
});
return this.render(markup, params, args);
}
processTemplate2(meta, context) {
// console.log('processTemplate', meta, context);
let counter = 0;
context = context || this;
let markup = JSON.stringify(meta);
let fields = markup.match(/\${(.+?)}/g) || [];
let args = [];
let params = [];
// console.log('processTemplate - fields', fields);
fields.forEach((field,index,list) => {
let inner = field
.replace(/\${/g, '')
.replace(/}/g, '');
let dot = inner.split('.');
let objVal = context;
dot.forEach((c) => {
objVal = objVal[c];
});
let f = `field${counter}`;
counter++;
if (!params.includes(f)) {
params.push(f);
}
args.push(objVal);
markup = markup.replace(field, '${' + f + '}');
});
return this.render(markup, params, args);
}
/**
* This method takes a string template, params, and args.
* It then calls the assemble function and then parses
* the results and returns back a JSON object.
*/
render(template, params, args) {
try {
let fn = this.assemble(template, params);
// console.log('render - assemble', fn);
template = fn.apply(null, args);
// console.log('render - apply', template);
template = JSON.parse(template);
}
catch(e) {
console.log('Render error:', e);
}
return template;
}
/**
* The following method receives a string representing
* markup for a template. It then converts the object
* to a string and looks for an matches to build up a params
* array. Once it has finished iterating over all the matches,
* it then builds up the args array.
* Currently, this implementation only supports top-level
* references, e.g. 'this.modal'
*/
processHtmlTemplate(meta, context) {
// console.log('processTemplate', meta, context);
let counter = 0;
context = context || this;
let markup = JSON.stringify(meta);
let fields = markup.match(/\${(.+?)}/g) || [];
let args = [];
let params = [];
// console.log('processTemplate - fields', fields);
fields.forEach((field,index,list) => {
let inner = field
.replace(/\${/g, '')
.replace(/}/g, '');
let dot = inner.split('.');
let objVal = context;
dot.forEach((c) => {
objVal = objVal[c];
});
let f = `field${counter}`;
counter++;
if (!params.includes(f)) {
params.push(f);
}
args.push(objVal);
markup = markup.replace(field, '${' + f + '}');
});
// console.log('processTemplate - render', markup);
// console.log('processTemplate - params', params);
// console.log('processTemplate - args', args);
return this.renderHtml(markup, params, args);
}
/**
* This method takes a string template, params, and args.
* It then calls the assemble function and then parses
* the results and returns back a string object.
*/
renderHtml(template, params, args) {
try {
let fn = this.assemble(template, params);
// console.log('render - assemble', fn);
template = fn.apply(null, args);
// console.log('render - apply', template);
}
catch(e) {
console.log('Render error:', e);
}
return template;
}
/**
* This method uses the 'new Function' paradigm to construct
* a new object as well as enforce string interpolation.
* This is necessary as we want to support dynamic templates.
*/
assemble(template, params) {
return new Function(params, "return `" + template +"`;");
}
/**
* This method determines if two objects are equal.
*/
areEqual(obj1, obj2) {
return Object.keys(obj1).every((key) => obj2.hasOwnProperty(key) && (obj1[key] === obj2[key]));
}
/**
* This method determines if two objects are equal by comparing
* two strings.
*/
areEqual2(obj1, obj2) {
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
/**
* This function tries to get the query parameter with the key
* corresponding the name argument and return the value.
*/
getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
let regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
let results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
/**
* This function returns a unique id.
* It was found: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
*/
guid() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
/**
* This function takes in a character and repeats it
* by the variable times.
*/
repeatString(char, times) {
return char.repeat(times);
}
/**
* This function takes in a list, function, and a property.
* It iterates over the list, executing the function passed
* and then recursively walks over all the children.
* It returns a new list of only the items that passed the
* function.
*/
recurseFilterItems(list = [], func, property = 'items') {
let filter = [];
// console.log('filter', filter);
list.forEach(c => {
if (func(c)) {
filter.push(c);
}
let child = this.recurseFilterItems(c[property], func, property);
if (child.length) {
filter = [...filter, ...child];
}
});
return filter;
}
/**
* This function takes in a list, function, and a property.
* It iterates over the list, executing the function passed
* and then recursively walks over all the children. Basically,
* it is the map function but recursive.
*/
recurseItems(list, func, property = 'items') {
if (list) {
list.forEach(c => {
func(c);
this.recurseItems(c[property], func, property);
});
}
}
/**
* The following takes in a file name and returns whether or
* not it is a Text mime-type.
*/
isTextMimeType(fileName = '') {
let types = [
'.css',
'.csv',
'.htm',
'.html',
'.js',
'.json',
'.md',
'.rtf',
'.sh',
'.svg',
'.ts',
'.txt',
'.xml'
];
let filter = types.filter(f => fileName.toLowerCase().endsWith(f));
if (filter && filter.length > 0) {
return true;
} else {
return false;
}
}
/**
* The following takes in an extension and returns whether or
* not it is a Font mime-type.
*/
isFontMimeTypeExt(ext = '') {
let types = [
'otf',
'eot',
'ttf',
'woff',
'woff2'
];
let filter = types.filter(f => ext.toLowerCase() === f);
if (filter && filter.length > 0) {
return true;
} else {
return false;
}
}
/**
* The following takes in an extension and returns whether or
* not it is an IMG mime-type.
*/
isImgMimeTypeExt(ext = '') {
let types = [
'ico',
'jpeg',
'jpg',
'gif',
'png',
'bmp'
];
let filter = types.filter(f => ext.toLowerCase() === f);
if (filter && filter.length > 0) {
return true;
} else {
return false;
}
}
/**
* The following takes in an extension and returns whether or
* not it is a Text mime-type.
*/
isTextMimeTypeExt(ext = '') {
let types = [
'css',
'csv',
'htm',
'html',
'js',
'json',
'md',
'rtf',
'sh',
'svg',
'ts',
'txt',
'xml'
];
let filter = types.filter(f => ext.toLowerCase() === f);
if (filter && filter.length > 0) {
return true;
} else {
return false;
}
}
/**
* This function tries to determine if an element is visible or not.
*/
isVisible(element) {
return !!( element.offsetWidth || element.offsetHeight || element.getClientRects().length );
}
/**
* This function tries to determine if an element is visible or not.
*/
isVisibleBySelector(selector) {
let element = document.querySelector(selector);
return !!( element.offsetWidth || element.offsetHeight || element.getClientRects().length );
}
/**
* The following uses (Tail)Recursion in ES6.
* https://hackernoon.com/recursion-in-javascript-with-es6-destructuring-and-rest-spread-4b22ae5998fa
*/
map([ head, ...tail ], fn) {
if (head === undefined && !tail.length) return [];
return tail.length ? [ fn(head), ...(this.map(tail, fn)) ] : [ fn(head) ];
}
filter([ head, ...tail ], fn) {
// console.log('head', head.name);
const newHead = fn(head) ? [ head ] : [];
return tail.length ? [ ...newHead, ...(this.filter(tail, fn)) ] : newHead;
}
join([ head, ...tail ], separator = ',') {
if (head === undefined && !tail.length) return '';
return tail.length ? head + separator + this.join(tail, separator) : head;
}
/**
* This function performs a string comparison for sorting arrays.
* If the object has a property isFolder, it uses upper case; otherwise,
* it uses lower case for the comparison. This ensures that folders are
* always at the beggining of the array.
*/
nameCompare(a, b) {
let nameA = a.name.toLowerCase();
let nameB = b.name.toLowerCase();
if (a.isFolder) {
nameA = a.name.toUpperCase();
}
if (b.isFolder) {
nameB = b.name.toUpperCase();
}
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment