From dd760018dfd710d020d3e5fb07edc2cc9a28b6cb Mon Sep 17 00:00:00 2001 From: Andrew Miner Date: Fri, 5 Jun 2015 22:47:40 -0700 Subject: [PATCH] Add UI/models to allow uploading images --- .../markdown_image_controller.coffee | 40 +++++++++++ .../markdown_image_list_controller.coffee | 55 ++++++++++++++++ .../markdown_section_controller.coffee | 19 ++++-- src/coffee/models/markdown_image.coffee | 46 +++++++++++++ src/coffee/models/markdown_image_list.coffee | 66 +++++++++++++++++++ src/jade/templates/markdown_image.jade | 11 ++++ src/jade/templates/markdown_image_list.jade | 10 +++ src/jade/templates/markdown_section.jade | 7 +- src/scss/templates/index.scss | 2 + src/scss/templates/markdown_image.scss | 32 +++++++++ src/scss/templates/markdown_image_list.scss | 18 +++++ test/markdown_image_list.test.coffee | 65 ++++++++++++++++++ 12 files changed, 361 insertions(+), 10 deletions(-) create mode 100644 src/coffee/controllers/markdown_image_controller.coffee create mode 100644 src/coffee/controllers/markdown_image_list_controller.coffee create mode 100644 src/coffee/models/markdown_image.coffee create mode 100644 src/coffee/models/markdown_image_list.coffee create mode 100644 src/jade/templates/markdown_image.jade create mode 100644 src/jade/templates/markdown_image_list.jade create mode 100644 src/scss/templates/markdown_image.scss create mode 100644 src/scss/templates/markdown_image_list.scss create mode 100644 test/markdown_image_list.test.coffee diff --git a/src/coffee/controllers/markdown_image_controller.coffee b/src/coffee/controllers/markdown_image_controller.coffee new file mode 100644 index 000000000..4109da59f --- /dev/null +++ b/src/coffee/controllers/markdown_image_controller.coffee @@ -0,0 +1,40 @@ +### +Crafting Guide - markdown_image_controller.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +BaseController = require './base_controller' + +######################################################################################################################## + +module.exports = class MarkdownImageController extends BaseController + + constructor: (options={})-> + if not options.model? then throw new Error 'options.model is required' + options.templateName = 'markdown_image' + super + + # BaseController Overrides ##################################################################### + + onDidRender: -> + @$image = @$('img') + @$fileName = @$('.fileName p') + @$button = @$('button') + super + + refresh: -> + if @model.mimeType? and @model.encodedData? + @$image.attr 'src', "data:#{@model.mimeType};base64,#{@model.encodedData}" + else + @$image.attr 'src', '/images/unknown.png' + + @$fileName.html @model.fileName + + if @model.encodedData? + @$button.html 'Update' + else + @$button.html 'Choose' + + super diff --git a/src/coffee/controllers/markdown_image_list_controller.coffee b/src/coffee/controllers/markdown_image_list_controller.coffee new file mode 100644 index 000000000..41f98ec63 --- /dev/null +++ b/src/coffee/controllers/markdown_image_list_controller.coffee @@ -0,0 +1,55 @@ +### +Crafting Guide - markdown_image_list_controller.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +BaseController = require './base_controller' +MarkdownImageController = require './markdown_image_controller' +MarkdownImageList = require '../models/markdown_image_list' + +######################################################################################################################## + +module.exports = class MarkdownImageListController extends BaseController + + constructor: (options={})-> + options.model ?= new MarkdownImageList + options.templateName = 'markdown_image_list' + super options + + # Property Methods ############################################################################# + + getMarkdownText: -> + return @model.markdownText + + setMarkdownText: (markdownText)-> + @model.markdownText = markdownText + + Object.defineProperties @prototype, + markdownText: {get:@prototype.getMarkdownText, set:@prototype.setMarkdownText} + + # BaseController Overrides ##################################################################### + + onDidRender: -> + @$imageContainer = @$('.image_container') + super + + refresh: -> + @_controllers ?= [] + index = 0 + + for image in @model.all + controller = @_controllers[index] + if not controller? + controller = new MarkdownImageController model:image + @_controllers.push controller + @$imageContainer.append controller.$el + controller.render() + else + controller.model = image + + index += 1 + + while @_controllers.length > index + @_controllers.pop().remove() diff --git a/src/coffee/controllers/markdown_section_controller.coffee b/src/coffee/controllers/markdown_section_controller.coffee index ba247a5f4..56fe9aaab 100644 --- a/src/coffee/controllers/markdown_section_controller.coffee +++ b/src/coffee/controllers/markdown_section_controller.coffee @@ -5,12 +5,13 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### -BaseController = require './base_controller' -convertMarkdown = require 'marked' -_ = require 'underscore' -{Event} = require '../constants' -{Url} = require '../constants' -w = require 'when' +_ = require 'underscore' +BaseController = require './base_controller' +convertMarkdown = require 'marked' +{Event} = require '../constants' +MarkdownImageListController = require './markdown_image_list_controller' +{Url} = require '../constants' +w = require 'when' ######################################################################################################################## @@ -132,6 +133,8 @@ module.exports = class MarkdownSectionController extends BaseController # BaseController Overrides ##################################################################### onDidRender: -> + @_imageListController = @addChild MarkdownImageListController, '.image_list' + @$errorText = @$('.error p') @$markdownPanel = @$('.markdown') @$sizer = @$('.sizer') @@ -144,6 +147,7 @@ module.exports = class MarkdownSectionController extends BaseController refresh: -> @$title.html @title + @_imageListController.markdownText = @model @_updateSizer() @_updatePreview() @@ -210,6 +214,7 @@ module.exports = class MarkdownSectionController extends BaseController editButton: @$('button.edit') editorPanel: @$('.editor') errorPanel: @$('.error') + imageList: @$('.image_list') markdownPanel: @$markdownPanel previewButton: @$('button.preview') returnButton: @$('button.return') @@ -226,7 +231,7 @@ module.exports = class MarkdownSectionController extends BaseController else if @state is State.creating visible = buttonPanel:true, creatingPanel:true, editButton:true else if @state is State.editing - visible = buttonPanel:true, cancelButton:true, editorPanel:true, previewButton:true + visible = buttonPanel:true, cancelButton:true, editorPanel:true, previewButton:true, imageList:true else if @state is State.previewing visible = buttonPanel:true, errorPanel:true, markdownPanel:true, returnButton:true, saveButton:true errorText = "remember: your changes aren't saved yet!" diff --git a/src/coffee/models/markdown_image.coffee b/src/coffee/models/markdown_image.coffee new file mode 100644 index 000000000..e84cc3deb --- /dev/null +++ b/src/coffee/models/markdown_image.coffee @@ -0,0 +1,46 @@ +### +Crafting Guide - markdown_image.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +BaseModel = require './base_model' +{Event} = require '../constants' + +######################################################################################################################## + +module.exports = class MarkdownImage extends BaseModel + + @MIMETYPES = [ + {patern:/\.png$/i, text:'image/png'} + {patern:/\.gif$/i, text:'image/gif'} + {patern:/\.jpg$/i, text:'image/jpeg'} + {patern:/\.jpeg$/i, text:'image/jpeg'} + ] + + constructor: (attributes={}, options={})-> + attributes.encodedData ?= null + attributes.fileName ?= '' + attributes.path ?= '' + attributes.sha ?= null + super + + @on Event.change + ':fileName', => @_mimeType = null + + # Property Methods ############################################################################# + + getFullPath: -> + return "#{@path}/#{@fileName}" + + getMimeType: -> + if not @_mimeType? + for mimeType in MarkdownImage.MIMETYPES + if mimeType.pattern.test @fileName + @_mimeType = mimeType.text + break + + return @_mimeType + + Object.defineProperties @prototype, + fullPath: {get:@prototype.getFullPath} diff --git a/src/coffee/models/markdown_image_list.coffee b/src/coffee/models/markdown_image_list.coffee new file mode 100644 index 000000000..21e3cf261 --- /dev/null +++ b/src/coffee/models/markdown_image_list.coffee @@ -0,0 +1,66 @@ +### +Crafting Guide - markdown_image_list.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +BaseModel = require './base_model' +{Event} = require '../constants' +MarkdownImage = require './markdown_image' + +######################################################################################################################## + +module.exports = class MarkdownImageList extends BaseModel + + constructor: (attributes={}, options={})-> + attributes.markdownText ?= null + super attributes, options + + @_images = {} + + @on Event.change + ':markdownText', => @_analyzeMarkdownText() + @_analyzeMarkdownText() + + # Property Methods ############################################################################# + + getAll: -> + fileNames = (fileName for fileName, image of @_images).sort() + result = [] + for fileName in fileNames + result.push @_images[fileName] + + return result + + Object.defineProperties @prototype, + all: {get:@prototype.getAll} + + # Private Methods ############################################################################## + + _analyzeMarkdownText: -> + regex = /\!\[([^\]]*)\]\(([^\)]*)\)/g + newImages = {} + changed = false + + if @markdownText? + while true + match = regex.exec @markdownText + break unless match? + + fileName = match[2] + image = @_images[fileName] + if not image? + image = new MarkdownImage fileName:fileName + @trigger Event.add, this, image + changed = true + + newImages[fileName] = image + + for fileName, image of @_images + if not newImages[fileName]? + @trigger Event.remove, this, image + changed = true + + if changed + @_images = newImages + @trigger Event.change, this diff --git a/src/jade/templates/markdown_image.jade b/src/jade/templates/markdown_image.jade new file mode 100644 index 000000000..d8521e040 --- /dev/null +++ b/src/jade/templates/markdown_image.jade @@ -0,0 +1,11 @@ +//- +//- Crafting Guide - markdown_image.jade +//- +//- Copyright (c) 2015 by Redwood Labs +//- All rights reserved. +//- + +.view__markdown_image + img + .fileName: p + button diff --git a/src/jade/templates/markdown_image_list.jade b/src/jade/templates/markdown_image_list.jade new file mode 100644 index 000000000..376cdb5db --- /dev/null +++ b/src/jade/templates/markdown_image_list.jade @@ -0,0 +1,10 @@ +//- +//- Crafting Guide - markdown_image_list.jade +//- +//- Copyright (c) 2015 by Redwood Labs +//- All rights reserved. +//- + +.view__markdown_image_list + h3 Images Referenced + .image_container \ No newline at end of file diff --git a/src/jade/templates/markdown_section.jade b/src/jade/templates/markdown_section.jade index 777e6f337..e5af16ed2 100644 --- a/src/jade/templates/markdown_section.jade +++ b/src/jade/templates/markdown_section.jade @@ -10,6 +10,8 @@ .panel .waiting.hideable: img(src='/images/wait.gif') .markdown.hideable.hidden + .creating.hideable.hidden + p Please help by adding a description! .editor.hideable.hidden .instructions p. @@ -17,8 +19,7 @@ Flavored Markdown. Use [[Item Name]] to link to another item. textarea .sizer - .creating.hideable.hidden - p Please help by adding a description! + .image_list .buttons.hideable.hidden .error.hideable.hidden: p button.return.hideable.hidden return @@ -31,4 +32,4 @@ p Your changes will appear on the site in a few minutes. .appologizing.hideable.hidden p Sorry, but something went wrong. - p Please try again after a few moments. + p Please try again after a few moments. \ No newline at end of file diff --git a/src/scss/templates/index.scss b/src/scss/templates/index.scss index e2022ba53..8f30e12c3 100644 --- a/src/scss/templates/index.scss +++ b/src/scss/templates/index.scss @@ -19,6 +19,8 @@ All rights reserved. @import 'item_selector'; @import 'item_selector_element'; @import 'login_page'; +@import 'markdown_image'; +@import 'markdown_image_list'; @import 'markdown_section'; @import 'minimal_recipe'; @import 'mod'; diff --git a/src/scss/templates/markdown_image.scss b/src/scss/templates/markdown_image.scss new file mode 100644 index 000000000..0081b73cb --- /dev/null +++ b/src/scss/templates/markdown_image.scss @@ -0,0 +1,32 @@ +/* +Crafting Guide - markdown_image.scss + +Copyright (C) 2015 by Redwood Labs +All rights reserved. +*/ + +.view__markdown_image { + position: relative; min-width: 33em; + + display: inline-block; + margin-right: 2em; + vertical-align: top; + + img { + position: absolute; height: 10em; width: 10em; + } + + .fileName { + height: 3em; + + margin: 1em 0 6em 11em; + + p { + font-weight: bold; + } + } + + button { + position: absolute; left: 7.33em; bottom: 0.66em; + } +} \ No newline at end of file diff --git a/src/scss/templates/markdown_image_list.scss b/src/scss/templates/markdown_image_list.scss new file mode 100644 index 000000000..7edae70da --- /dev/null +++ b/src/scss/templates/markdown_image_list.scss @@ -0,0 +1,18 @@ +/* +Crafting Guide - markdown_image_list.scss + +Copyright (C) 2015 by Redwood Labs +All rights reserved. +*/ + +.view__markdown_image_list { + position: relative; min-width: 33em; width: 100%; + + border-bottom: 0.1em solid $color-gray-medium; + margin: 2em 0; + padding: 0 0 2em 0; + + h3 { + margin-bottom: 1em; + } +} diff --git a/test/markdown_image_list.test.coffee b/test/markdown_image_list.test.coffee new file mode 100644 index 000000000..ce67e3d89 --- /dev/null +++ b/test/markdown_image_list.test.coffee @@ -0,0 +1,65 @@ +### +Crafting Guide - markdown_image_list_test.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +MarkdownImageList = require '../src/coffee/models/markdown_image_list' + +######################################################################################################################## + +list = null + +######################################################################################################################## + +describe 'markdown_image_list.coffee', -> + + beforeEach -> + list = new MarkdownImageList + + describe '_analyzeMarkdownText', -> + + it 'finds nothing in a null string', -> + list.all.should.eql [] + + it 'finds nothing in an empty string', -> + list.markdownText = '' + list.all.should.eql [] + + it 'finds nothing when there are no images in the markdown', -> + list.markdownText = 'alpha bravo charlie delta' + list.all.should.eql [] + + it 'finds a single image alone', -> + list.markdownText = '![](alpha.png)' + (i.fileName for i in list.all).sort().should.eql ['alpha.png'] + + it 'finds a single image inside a markdown document', -> + list.markdownText = '# alpha\n bravo charlie\n\n| ![](alpha.png) |\n|:----------:|\ndelta' + (i.fileName for i in list.all).sort().should.eql ['alpha.png'] + + it 'finds multiple images inside a markdown document', -> + list.markdownText = '| Images |\n|:------:|\n| ![](alpha.png) |\n| ![](bravo.png) |\n| ![](charlie.png) |\n' + (i.fileName for i in list.all).sort().should.eql ['alpha.png', 'bravo.png', 'charlie.png'] + + it 'adds new images when markdown changes', -> + list.markdownText = '![](alpha.png)' + (i.fileName for i in list.all).sort().should.eql ['alpha.png'] + + list.markdownText = '![](alpha.png), ![](bravo.png)' + (i.fileName for i in list.all).sort().should.eql ['alpha.png', 'bravo.png'] + + it 'removes missing images when markdown changes', -> + list.markdownText = '![](alpha.png), ![](bravo.png)' + (i.fileName for i in list.all).sort().should.eql ['alpha.png', 'bravo.png'] + + list.markdownText = '![](alpha.png)' + (i.fileName for i in list.all).sort().should.eql ['alpha.png'] + + it 'does not change remaining images when still present in changed markdown', -> + list.markdownText = '![](alpha.png)' + list.all[0].testField = 'foo' + + list.markdownText = '![](alpha.png) bravo' + list.all[0].testField.should.equal 'foo'