-
-
Save mattduffield/51180ab8fe05c90572a3c9fe0220cdda to your computer and use it in GitHub Desktop.
Aurelia Gist - Tree
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <template> | |
| <require from="./tree"></require> | |
| <require from="./context-menu"></require> | |
| <h1>${message}</h1> | |
| <tree items.bind="items"></tree> | |
| <context-menu></context-menu> | |
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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": [] | |
| } | |
| ] | |
| } | |
| ]; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export class ApplicationService { | |
| user = { | |
| sourceControl: {} | |
| }; | |
| contextMenu = []; | |
| initCurrentRecord(record) { | |
| this.currentRecord = record; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| :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); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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