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/',