diff --git a/scripts/app.coffee b/scripts/app.coffee
index ede27dee..1c7d0d8b 100644
--- a/scripts/app.coffee
+++ b/scripts/app.coffee
@@ -4,12 +4,13 @@ define [
'cs!session'
'cs!collections/content'
'cs!collections/media-types'
+ 'cs!models/workspace-root'
'cs!models/content/book'
'cs!models/content/book-toc-node'
'cs!models/content/folder'
'cs!models/content/module'
'less!styles/main.less'
-], (Backbone, Marionette, session, allContent, mediaTypes, Book, BookTocNode, Folder, Module) ->
+], (Backbone, Marionette, session, allContent, mediaTypes, workspaceRoot, Book, BookTocNode, Folder, Module) ->
app = new Marionette.Application()
@@ -32,6 +33,8 @@ define [
# set the main div for all the layouts
controller.main = @main
+ controller.setRootNode(workspaceRoot)
+
# The controller fires a navigate event, the app then updates the url
# as it sees fit.
controller.on 'navigate', (route) -> controller.navigate route
diff --git a/scripts/controllers/routing.coffee b/scripts/controllers/routing.coffee
index 26bf952d..ae636b19 100644
--- a/scripts/controllers/routing.coffee
+++ b/scripts/controllers/routing.coffee
@@ -13,6 +13,8 @@ define [
# Only reason to extend Backbone.Router is to get the @navigate method
return new class AppController extends Marionette.AppRouter
+ setRootNode: (@rootNode) ->
+
# For all the views ensure there is a layout.
# There is a cyclic dependency between the controller and `menuLayout`
# because `menuLayout` has a "Home" button.
@@ -31,7 +33,7 @@ define [
# the user can click an item in the ToC to `goEdit`.
_showWorkspacePane: (SidebarView) ->
if not @layout.workspace.currentView
- @layout.workspace.show(new SidebarView {collection:allContent})
+ @layout.workspace.show(new SidebarView {model:@rootNode})
# Show Workspace
diff --git a/scripts/gh-book/app.coffee b/scripts/gh-book/app.coffee
index 6a7cb377..05ef94bc 100644
--- a/scripts/gh-book/app.coffee
+++ b/scripts/gh-book/app.coffee
@@ -27,7 +27,7 @@ define [
onceAll = (promises) -> return $.when.apply($, promises)
# Singleton that gets reloaded when the repo changes
- epubContainer = new EpubContainer()
+ epubContainer = EpubContainer::instance()
allContent.on 'add', (model, collection, options) ->
return if options.loading
@@ -43,10 +43,6 @@ define [
allContent.each (book) ->
book.manifest?.add(model) # Only books have a manifest
- allContent.on 'remove', (model, collection, options) ->
- epubContainer.removeChild(model)
-
-
# The WelcomeSignInView is overloaded to show Various Dialogs.
#
# - SignIn
@@ -55,8 +51,6 @@ define [
# When there is a failure show the Settings/SignIn Modal
welcomeView = new WelcomeSignInView {model:session}
-
-
# This is a utility that wraps a promise and alerts when the promise fails.
onFail = (promise, message='There was a problem.') ->
complete = 0
@@ -98,7 +92,7 @@ define [
mediaTypes.add BinaryFile, {mediaType:'image/png'}
mediaTypes.add BinaryFile, {mediaType:'image/jpeg'}
- # set which media formats are allowed
+ # set which media formats are allowed
# at the toplevel of the content
for type in EpubContainer::accept
mediaTypes.type(type)::toplevel = true
@@ -235,6 +229,8 @@ define [
# Tell the controller which region to put all the views/layouts in
controller.main = App.main
+ controller.setRootNode(epubContainer)
+
# Custom routes to configure the Github User and Repo from the browser
router = new class GithubRouter extends Backbone.Router
@@ -311,7 +307,7 @@ define [
promise = onFail(epubContainer.reload(), 'There was a problem re-loading the repo')
.done () ->
# Get the first book from the epub
- opf = epubContainer.children.at(0)
+ opf = epubContainer.getChildren().at(0)
if opf
opf.load().done () ->
# When that book is loaded, edit it.
diff --git a/scripts/gh-book/epub-container.coffee b/scripts/gh-book/epub-container.coffee
index 5c90ce09..b698c285 100644
--- a/scripts/gh-book/epub-container.coffee
+++ b/scripts/gh-book/epub-container.coffee
@@ -15,6 +15,8 @@ define [
'cs!gh-book/opf-file'
], (allContent, Saveable, loadableMixin, treeMixin, OpfFile) ->
+ instance = undefined
+
class EpubContainer extends Saveable
mediaType: 'application/epub+zip'
accept: [OpfFile::mediaType]
@@ -23,21 +25,23 @@ define [
urlRoot: ''
id: 'META-INF/container.xml'
+
initialize: () ->
- @children = new Backbone.Collection()
- @children.on 'add remove', (collection, options) =>
+ @_initializeTreeHandlers({root:@})
+
+ @getChildren().on 'add remove', (collection, options) =>
@_markDirty(options, true)
- @children.on 'reset', (collection, options) =>
+ @getChildren().on 'reset', (collection, options) =>
return if options.loading
- allContent.reset(@children.models)
+ allContent.reset(@getChildren().models)
# Extend the `load()` to wait until all content is loaded
_loadComplex: (fetchPromise) ->
return fetchPromise.then () =>
- contentPromises = @children.map (model) => model.load()
+ contentPromises = @getChildren().map (model) => model.load()
# Return a new promise that finishes once all the contentPromises have loaded
return $.when.apply($, contentPromises)
@@ -58,14 +62,14 @@ define [
allContent.add(model, {loading:true})
ret.push model
- return @children.reset(ret, {loading:true})
+ return @getChildren().reset(ret, {loading:true})
serialize: () ->
return if not @$xml
rootfiles = @$xml.find('rootfiles').empty()
- @children.each (child) ->
+ @getChildren().each (child) ->
jQuery('')
.attr('media-type', child.mediaType)
.attr('full-path', child.id)
@@ -76,11 +80,16 @@ define [
serializer.serializeToString(@$xml.get(0))
# Called by `loadableMixin.reload` when the repo settings change
- reset: () -> @children.reset()
+ reset: () -> @getChildren().reset()
- addChild: (book) -> @children.add(book)
+ addChild: (book) -> @getChildren().add(book)
removeChild: (book) -> @children.remove(book)
EpubContainer = EpubContainer.extend loadableMixin
+ EpubContainer = EpubContainer.extend treeMixin
+
+ EpubContainer::instance = () ->
+ return instance || instance = new EpubContainer()
+
# All content in the Workspace
return EpubContainer
diff --git a/scripts/gh-book/opf-file.coffee b/scripts/gh-book/opf-file.coffee
index 0f33663f..14a84a40 100644
--- a/scripts/gh-book/opf-file.coffee
+++ b/scripts/gh-book/opf-file.coffee
@@ -114,6 +114,11 @@ define [
# save opf files on creation
@_save() if @_isNew
+ # return null so `TocPointerNode.getRoot()` returns the OPF file instead of the EPUBContainer for new books
+ # HACK because when a new book is added to EPUBComtainer the parent is set.
+ # Then, in toc-branch, goEdit uses model.getRoot() to determine what to render in the sidebar
+ getParent: () -> null
+
# Add an `- ` to the OPF.
# Called from `@manifest.add` and `@resolveSaveConflict`
_addItem: (model, options={}, force=true) ->
@@ -416,7 +421,9 @@ define [
# Override the tree's removeMe (which just asks the parent to remove the child)
removeMe: ->
- allContent.remove(@).save()
+ require ['cs!gh-book/epub-container'], (EpubContainer) =>
+ EpubContainer::instance().removeChild(@)
+ allContent.save()
newNode: (options) ->
model = options.model
diff --git a/scripts/mixins/loadable.coffee b/scripts/mixins/loadable.coffee
index 3fb6b0f2..7b598f37 100644
--- a/scripts/mixins/loadable.coffee
+++ b/scripts/mixins/loadable.coffee
@@ -33,9 +33,6 @@ define ['jquery'], ($) ->
@_loading.fail (err) =>
# Since we failed clear the promise so we can try again later.
@_loading = null
- @trigger('load:fail', err)
- @_loading.done () =>
- @trigger('load:done')
return @_loading
diff --git a/scripts/mixins/tree.coffee b/scripts/mixins/tree.coffee
index fe912b2b..ff60c27c 100644
--- a/scripts/mixins/tree.coffee
+++ b/scripts/mixins/tree.coffee
@@ -19,9 +19,7 @@ define ['backbone'], (Backbone) ->
throw 'BUG: Missing root' if not options.root
#throw 'BUG: Missing title or model' if not options.title
- @_tree_root = options.root
-
- @_tree_children = new Backbone.Collection()
+ @_tree_children = options.children or new Backbone.Collection()
@_tree_children.on 'add', (child, collection, options) =>
# Parent is useful for DnD but since we don't use a `TocNode`
@@ -33,12 +31,10 @@ define ['backbone'], (Backbone) ->
child._tree_parent.removeChild(child, options) if child._tree_parent and child._tree_parent != @
child._tree_parent = @
- child._tree_root = @_tree_root
@trigger 'tree:add', child, collection, options
@_tree_children.on 'remove', (child, collection, options) =>
delete child._tree_parent
- delete child._tree_root
@trigger 'tree:remove', child, collection, options
@_tree_children.on 'change', (child, options) =>
@@ -120,11 +116,9 @@ define ['backbone'], (Backbone) ->
# Before adding the model make sure it's a Tree Node (does it have `._tree_children`.
# If not, wrap it in a node
if ! (model._tree_children)
- model = @_tree_root.newNode {model:model}
+ model = (@getRoot() or @).newNode {model:model}
children.add(model, {at:at})
- # When moving from one book to another make sure the root is set
- model._tree_root = @_tree_root
return treeMixin
diff --git a/scripts/models/content/book.coffee b/scripts/models/content/book.coffee
index cada3427..6aeb7e28 100644
--- a/scripts/models/content/book.coffee
+++ b/scripts/models/content/book.coffee
@@ -9,6 +9,14 @@ define [
'cs!models/utils'
], (_, Backbone, allContent, Module, loadable, TocNode, TocPointerNode, Utils) ->
+ # Copy/Pasta from Saveable.coffee
+ INTERNAL_ATTRIBUTES = [
+ '_original'
+ '_selected'
+ '_isDirty'
+ '_hasRemoteChanges'
+ ]
+
return class BookModel extends (TocNode.extend loadable)
mediaType: 'application/vnd.org.cnx.collection'
accept: [Module::mediaType, TocNode::mediaType]
@@ -69,7 +77,11 @@ define [
@tocNodes.on 'add remove tree:change', (model, collection, options) =>
setNavModel(options)
- @tocNodes.on 'change reset', (collection, options) =>
+ # Explicitly enumerate the change properties to listen to so `_selected` does not trigger the node to be marked dirty
+ @tocNodes.on 'change reset', (model, options) =>
+ # Do not mark dirty if only "_*" attributes are changed
+ return if _.isEmpty _.omit(model.changedAttributes?(), INTERNAL_ATTRIBUTES)
+
setNavModel(options)
# These store the added items since last successful save.
@@ -242,6 +254,10 @@ define [
@_localTitlesChanged = {}
+ getRoot: () -> @
+ # Prevent the whole workspace from loading up in the book sidebar when a Module is clicked in the picker
+ getParent: () -> null
+
newNode: (options) ->
model = options.model
node = @tocNodes.get model.id
diff --git a/scripts/models/content/inherits/saveable.coffee b/scripts/models/content/inherits/saveable.coffee
index 6e900494..ed073689 100644
--- a/scripts/models/content/inherits/saveable.coffee
+++ b/scripts/models/content/inherits/saveable.coffee
@@ -62,10 +62,12 @@ define ['backbone'], (Backbone) ->
@set(attrs)
onSaved: () ->
+ # Set _isNew before calling `@set(...)` because the Save button will render when `@set()` is called but not when `@isNew` is changed
+ # and `@isDirty()` returns `@_isNew or get('_isDirty')`
+ @_isNew = false # Set in loadable
+
# If the content was just added, squirrel away the content into _ooriginal for visual Diffing later
@set
_original: @serialize?()
_hasRemoteChanges: false
_isDirty: false
-
- @_isNew = false # Set in loadable
diff --git a/scripts/models/content/toc-pointer-node.coffee b/scripts/models/content/toc-pointer-node.coffee
index 12acafda..a92b0043 100644
--- a/scripts/models/content/toc-pointer-node.coffee
+++ b/scripts/models/content/toc-pointer-node.coffee
@@ -1,4 +1,4 @@
-define ['cs!./toc-node'], (TocNode) ->
+define ['underscore', 'cs!./toc-node'], (_, TocNode) ->
class TocPointerNode extends TocNode
mediaType: 'application/BUG-mediaType-not-set' # This will get overridden to be whatever this node points to
@@ -27,7 +27,14 @@ define ['cs!./toc-node'], (TocNode) ->
if options.passThroughChanges
@on 'change:title', (model, value, options) => @model.set('title', value, options)
- @model.on 'all', () => @trigger.apply @, arguments
+ @model.on 'all', () =>
+ # Since some views use filteredCollection (which uses the `model` argument in the event handler, splice in the pointer)
+ # This causes problems when filteredCollection tries to keep its collection in sync.
+ # Replace the 2nd arg (the @model) with the pointer (@)
+ args = _.toArray(arguments)
+ throw new Error 'BUG: Expecting 2nd argument to be the model this pointer points to' if @model != args[1]
+ args.splice(1, 1, @)
+ @trigger.apply @, args
# Set the title on the model if an overridden one has not been set (github-book "shortcut")
# TODO: see how the github-book works with these 2 lines commented: @set('title', @model.get('title')) if not options.title
diff --git a/scripts/models/workspace-root.coffee b/scripts/models/workspace-root.coffee
new file mode 100644
index 00000000..31b291f0
--- /dev/null
+++ b/scripts/models/workspace-root.coffee
@@ -0,0 +1,29 @@
+define [
+ 'backbone'
+ 'cs!models/content/inherits/saveable'
+ 'cs!mixins/tree'
+ 'cs!models/content/book'
+ 'cs!models/content/module'
+ 'cs!models/content/folder'
+ 'cs!collections/content'
+ 'filtered-collection'
+], (Backbone, Saveable, treeMixin, Book, Module, Folder, allContent) ->
+
+ return new class WorkspaceRoot extends Saveable.extend(treeMixin)
+ accept: [Book::mediaType, Module::mediaType, Folder::mediaType]
+ initialize: (options) ->
+ super(options)
+
+ content = new Backbone.FilteredCollection(null, {collection:allContent})
+
+ # Allow `.add` to be called on filtered collections (for new Books)
+ content.add = allContent.add.bind(allContent)
+
+ # Filter the Workspace sidebar to only contain Book and Folder
+ content.setFilter (model) => return model.mediaType in [Book::mediaType, Folder::mediaType]
+
+ @_initializeTreeHandlers {root:@, children:content}
+
+ # Just return the node; for a Book this would return options.model wrapped in a TocPointerNode
+ newNode: (options) ->
+ return options.model
diff --git a/scripts/views/layouts/workspace/sidebar.coffee b/scripts/views/layouts/workspace/sidebar.coffee
index 410bdd66..501594f2 100644
--- a/scripts/views/layouts/workspace/sidebar.coffee
+++ b/scripts/views/layouts/workspace/sidebar.coffee
@@ -11,8 +11,12 @@ define [
return class Sidebar extends Marionette.Layout
template: sidebarTemplate
- initialize: () ->
+ initialize: (options) ->
@filteredMediaTypes = new Backbone.FilteredCollection(null, {collection:mediaTypes})
+ @collection = new Backbone.FilteredCollection(null, {collection:@model.getChildren()})
+
+ # Filter the "Add" button by what the model accepts
+ @filteredMediaTypes.setFilter (type) => return type.id in @model.accept
regions:
addContent: '.add-content'
@@ -30,15 +34,6 @@ define [
model = @model
collection = @collection or model.getChildren?()
- if model
- # This is a tree sidebar
- @filteredMediaTypes.setFilter (type) -> return type.id in model.accept
- else
- # This is the Picker/Roots Sidebar
- collection = new Backbone.FilteredCollection(null, {collection:collection})
- collection.setFilter (content) -> return content.getChildren
- @filteredMediaTypes.setFilter (type) -> return type.get('modelType')::toplevel
-
# TODO: Make the collection a FilteredCollection that only shows @model.accepts
@addContent.show(new AddView {context:model, collection:@filteredMediaTypes})
@toc.show(new TocView {model:model, collection:collection})
diff --git a/scripts/views/workspace/sidebar/toc-branch.coffee b/scripts/views/workspace/sidebar/toc-branch.coffee
index f2cbf62f..c384bc5c 100644
--- a/scripts/views/workspace/sidebar/toc-branch.coffee
+++ b/scripts/views/workspace/sidebar/toc-branch.coffee
@@ -217,6 +217,8 @@ define [
if not model.getRoot?()
# Find the 1st leaf node (editable model)
model = model.findDescendantDFS? (model) -> return model.getChildren().isEmpty()
+ # if @model does not have `.findDescendantDFS` then use the original model
+ model = model or @model
controller.goEdit(model, model.getRoot?())
diff --git a/styles/workspace/sidebar.less b/styles/workspace/sidebar.less
index 73b02cc5..70bc0772 100755
--- a/styles/workspace/sidebar.less
+++ b/styles/workspace/sidebar.less
@@ -339,6 +339,9 @@ aside > div {
}
+// Workspace Picker should not have a title so hide it
+aside#workspace #workspace-sidebar-toc > h4 { display: none; }
+
// Feed each panel's width to the
mixin below
aside#workspace > div { // Picker
.x-indent-compensate-panel(@picker-width - 70);
diff --git a/test/demo-main.js b/test/demo-main.js
index 6694ac9f..f6b736a2 100644
--- a/test/demo-main.js
+++ b/test/demo-main.js
@@ -1,6 +1,11 @@
(function () {
"use strict";
+ /* Load relative URLs based on the CSS file containing url() instead of the root LESS file */
+ window.less = {
+ relativeUrls: true
+ };
+
require({
baseUrl: '../scripts/',