CanJS File Navigator (Advanced)

Finished version of the CanJS File Navigator Guide (Advanced)

by donejs

HTML

<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>
<script type="text/stache" id="app-template">
  <a-folder folder:from="this" isOpen:from="true" />
</script>

<script type="text/stache" id="folder-template">
  <span on:click="toggleOpen()">{{folder.name}}</span>
  {{#if isOpen}}
  {{#if entitiesPromise.isPending}}
    <div class="loading">Loading</div>
  {{else}}
    <ul>
      {{#each entitiesPromise.value}}
        <li class="{{type}} {{#if hasChildren}}hasChildren{{/if}}">
          {{#eq type 'file'}}
            📝 <span>{{name}}</span>
          {{else}}
            📁 <a-folder folder:from="this" />
          {{/eq}}
        </li>
      {{/each}}
    </ul>
  {{/if}}
  {{/if}}
</script>

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

// Stores the next entity id to use.
var entityId = 1;

// Returns an array of entities for the given `parentId`.
// Makes sure the `depth` of entities doesn’t exceed 5.
var makeEntities = function(parentId, depth) {
  if (depth > 5) {
    return [];
  }
  // The number of entities to create.
  var entitiesCount = can.fixture.rand(10);

  // The array of entities we will return.
  var entities = [];

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

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

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

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

    var entity = {
      id: id,
      name: (isFolder ? "Folder" : "File") + " " + id,
      parentId: parentId,
      type: (isFolder ? "folder" : "file"),
      hasChildren: children.length > 0
    };
    entities.push(entity);

    // Add the children of a folder
    [].push.apply(entities,  children)

  }
  return entities;
};

// Make the entities for the demo
var entities = makeEntities("0", 0);

// Add them to a client-like DB store
var entitiesStore = can.fixture.store(entities);

// Trap requests to /api/entities to read items from the entities store.
can.fixture("/api/entities", entitiesStore);

// Make requests to /api/entities take 1 second
can.fixture.delay = 1000;

var Entity = can.DefineMap.extend({
  id: "string",
  name: "string",
  parentId: "string",
  hasChildren: "boolean",
  type: "string"
});

can.connect.baseMap({
  Map: Entity,
  url: "/api/entities"
});

var folder = new Entity({
  id: "0",
  name: "ROOT/",
  hasChildren: true,
  type: "folder"
});

var FolderVM = can.DefineMap.extend({
  folder: Entity,
  entitiesPromise: {
    value: function() {
      return Entity.getList({parentId: this.folder.id});
    }
  },
  isOpen: {type: "boolean", value: false},
  toggleOpen: function() {
    this.isOpen = !this.isOpen;
  }
});

can.Component.extend({
  tag:...