CanJS File Navigator (Simple) [CanJS 4]

Finished version of the CanJS File Navigator Guide (Simple)

by donejs

HTML

<script src="https://unpkg.com/can@4/dist/global/can.all.js"></script>
<script type="text/stache" id="entities-template">
  <span on:click="toggleOpen()">{{name}}</span>
  {{#if(isOpen)}}
    <ul>
      {{#each(./children)}}
        <li class="{{type}} {{#if(hasChildren)}}hasChildren{{/if}}">
          {{#eq(type, 'file')}}
            πŸ“ <span>{{name}}</span>
          {{else}}
            πŸ“ {{>entities}}
          {{/eq}}
        </li>
      {{/each}}
    </ul>
  {{/if}}
</script>

<!--
  <span>root</span>
  <ul>
    <li class='file'>πŸ“ <span>cats.png</span></li>
    <li class='file'>πŸ“ <span>dogs.jpg</span></li>
    <li class='folder'>πŸ“ <span>empty folder</span></li>
    <li class='folder hasChildren'>πŸ“ <span>folder with files</span></li>
    <li class='folder hasChildren'>
      πŸ“ <span>folder loading files </span>
      <div class="loading">Loading</div>
    </li>
    <li class='folder hasChildren'>
      πŸ“ <span>opened folder</span>
      <ul>
        <li class='file'>πŸ“ <span>file1.png</span></li>
        <li class='file'>πŸ“ <span>file2.jpg</span></li>
      </ul>
    </li>
  </ul>
-->

<script src="https://unpkg.com/can@4/dist/global/can.all.js"></script>

<script type="text/javascript">
let entityId = 1;

const makeEntities = function(parentId, depth){
  if (depth > 5) {
    return [];
  }

  const entitiesCount = can.fixture.rand(6);

  const entities = [];

  for (let i = 0 ; i < entitiesCount; i++) {

    // The id for this entity
    const id = ""+(entityId++);

    // If the entity is a folder or file
    const isFolder = Math.random() > 0.3;

    // The children for this folder.
    const children = isFolder ? makeEntities(id, depth+1) : [];

    const entity = {
      id: id,
      name: (isFolder ? "Folder" : "File")+" "+id,
      parentId: parentId,
      type: (isFolder ? "folder" : "file"),
      hasChildren: children.length ? true : false
    };
    entities.push(entity);
    if (isFolder) {
      entity.children = children;
    }

  }
  return...

CSS

body {
  padding: 10px;
}

@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:400,700');

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: "Roboto Mono", "Lucida Console", Monaco, monospace;
  font-size: 14px;
  color: #000;
}

ul {
  list-style: none;
}

li {
  padding-left: 20px;
  position: relative;
  line-height: 2em;
}

li:before{
  position: absolute;
  left: 5px;
  top: -2px;
  content: '';
  display: block;
  height: 1em;
  border-bottom: 1px solid #bbb;
  width: 10px;
}

li:after {
  position: absolute;
  left: 5px;
  bottom: 0;
  content: '';
  display: block;
  border-left: 1px solid #bbb;
  height: 100%;
}

.loading {
  color: black;
  padding-left: 20px;
  padding-top: 10px;
  padding-bottom: 10px;
}

.loading:after {
  content: "...";
}

.hasChildren > a-folder > span, .hasChildren > span {
  transition: all .1s ease-in-out;
  cursor: pointer;
  border-bottom: 1px dotted #000;
}

.hasChildren > a-folder > span:hover, .hasChildren > span:hover {
  opacity: .5;
}

JavaScript

const Entity = can.DefineMap.extend("Entity", {
  id: "string",
  name: "string",
  parentId: "string",
  hasChildren: "boolean",
  type: "string",
  children: [{
    type: function(entity) {
      return new Entity(entity)
    }
  }],
  isOpen: {type: "boolean", default: false},
  toggleOpen: function(){      
    this.isOpen = !this.isOpen;
  }
});

const rootEntity = new Entity(rootEntityData);

const template = can.stache.from("entities-template");
can.stache.registerPartial("entities", template);

const frag = template(rootEntity);

document.body.appendChild( frag );