diff --git a/scripts/gh-book/app.coffee b/scripts/gh-book/app.coffee index 34e80ff3..a86d38c1 100644 --- a/scripts/gh-book/app.coffee +++ b/scripts/gh-book/app.coffee @@ -9,6 +9,7 @@ define [ 'cs!collections/media-types' 'cs!gh-book/epub-container' 'cs!gh-book/xhtml-file' + 'cs!gh-book/gdoc-xhtml-file' 'cs!gh-book/opf-file' 'cs!gh-book/toc-node' 'cs!gh-book/binary-file' @@ -18,7 +19,7 @@ define [ 'cs!configs/github.coffee' 'less!styles/main' 'less!gh-book/gh-book' -], ($, _, Backbone, Marionette, logger, session, allContent, mediaTypes, EpubContainer, XhtmlFile, OpfFile, TocNode, BinaryFile, WelcomeSignInView, remoteUpdater, LoadingView, config) -> +], ($, _, Backbone, Marionette, logger, session, allContent, mediaTypes, EpubContainer, XhtmlFile, GoogleDocXhtmlFile, OpfFile, TocNode, BinaryFile, WelcomeSignInView, remoteUpdater, LoadingView, config) -> # Stop logging. logger.stop() @@ -93,6 +94,7 @@ define [ mediaTypes.add TocNode mediaTypes.add BinaryFile, {mediaType:'image/png'} mediaTypes.add BinaryFile, {mediaType:'image/jpeg'} + mediaTypes.add GoogleDocXhtmlFile, {mediaType:GoogleDocXhtmlFile::uniqueMediaType} # Views use anchors with hrefs so catch the click and send it to Backbone diff --git a/scripts/gh-book/gdoc-xhtml-file.coffee b/scripts/gh-book/gdoc-xhtml-file.coffee new file mode 100644 index 00000000..01ad25cd --- /dev/null +++ b/scripts/gh-book/gdoc-xhtml-file.coffee @@ -0,0 +1,113 @@ +define [ + 'underscore' + 'jquery' + 'backbone' + 'cs!models/content/module' + 'cs!gh-book/xhtml-file' + 'gh-book/googlejsapi' +], (_, $, Backbone, ModuleModel, XhtmlModel) -> + + GDOC_TO_HTML_URL = 'http://testing.oerpub.org/gdoc2html' # eventually `http://remix.oerpub.org/gdoc2html` + gdocsURL = (id) -> "https://docs.google.com/document/d/#{id}/export?format=html&confirm=no_antivirus" + + # the cannonical example of how to use google picker includes three functions + # newPicker(), createPicker(), and pickerCallback(). and so do we except ours + # includes promises. + + gdocPickerDeferred = undefined + + newPicker = () -> + google.load('picker', '1', {"callback" : createPicker}) + gdocPickerDeferred = $.Deferred() + return gdocPickerDeferred.promise() + + createPicker = () -> + picker = new google.picker.PickerBuilder(). + addView(google.picker.ViewId.DOCUMENTS). + setCallback(pickerCallback). + build() + picker.setVisible(true); + return picker + + pickerCallback = (data) -> + # action can be { "cancel", "picked", "received", "loaded", "uploadProgress", "uploadScheduled", "uploadStateChange" } + if data.action is google.picker.Action.PICKED + gdocPickerDeferred.resolve(data) + else if data.action is google.picker.Action.CANCEL + gdocPickerDeferred.reject() + console.warn "GOOGLE DOC IMPORT: picker dialog was cancelled" + + getGoogleDocHtml = (data) -> + gdocResourceId = data.docs[0].id + htmlUrl = gdocsURL(gdocResourceId) + gdocHtmlPromise = $.get(htmlUrl) + gdocHtmlPromise.fail -> + console.warn "GOOGLE DOC IMPORT: failed to get google doc htmlform google" + return gdocHtmlPromise + + transformGoogleDocHtml = (html) -> + gdocTransformPromise = $.ajax( + dataType: "json" + type: "POST" + async: true + url: GDOC_TO_HTML_URL + data: + html: html + textbook_html: 0 + copy_images: 0 + ) + gdocTransformPromise.fail -> + console.warn "GOOGLE DOC IMPORT: failed to transform google doc html via remix service" + return gdocTransformPromise + + # The `Content` model contains the following members: + # + # * `title` - an HTML title of the content + # * `language` - the main language (eg `en-us`) + # * `subjects` - an array of strings (eg `['Mathematics', 'Business']`) + # * `keywords` - an array of keywords (eg `['constant', 'boltzmann constant']`) + # * `authors` - an `Collection` of `User`s that are attributed as authors + return class GoogleDocXhtmlModel extends XhtmlModel + + title: 'Google Document Import' + + # **NOTE:** The mediaType (`application/xhtml+xml`) is inherited from XhtmlModel + # because a successful import will 'appear' as a XHTML document. + # This mediaType is used in the OPF manifest + + # In order to add this type to the Add dropdown for a Book (OPF File) + # this model must have a unique mediaType (not `application/xhtml+xml`) + # This is used to register with `media-types` and is in the + # list of types `opf-file` accepts as a child (so it shows up in the filtered dropdown) + uniqueMediaType: 'application/vnd.org.cnx.gdoc-import' + + _loadComplex: (fetchPromise) -> + # **NOTE:** `fetchPromise` is not used because this type can only be created as a new object + # (the fetchPromise is already resolved) + gdocImportPromise = @_importGoogleDoc() + return gdocImportPromise + + # Saves the fetched and converted Document into this model for saving + _injectHtml: (bodyhtml) -> + # bodyhtml is "..." + @set 'body', bodyhtml + + _cleanupFailedImport: () -> + return + + _importGoogleDoc: () -> + promise = newPicker() # 1. Open the picker dialog + .then((data) => + # alert "google doc selected" + getGoogleDocHtml data # 2. Get the HTML from Google + ).then((html) => + # alert "got html for google doc" + transformGoogleDocHtml html # 3. Send the HTML to the transform service + ).then((json) => + # alert "transformed google doc html via remix service" + @_injectHtml json.html # 4. Inject the cleaned HTML into the Model + ).fail(() => + console.warn "GOOGLE DOC IMPORT: was not successful" + @_cleanupFailedImport() + ) + promise diff --git a/scripts/gh-book/gh-book.less b/scripts/gh-book/gh-book.less index f8da7fe9..b7f431c9 100755 --- a/scripts/gh-book/gh-book.less +++ b/scripts/gh-book/gh-book.less @@ -5,9 +5,10 @@ @{mt-selector} > .add-content-title::before { content: "New " @add-title; color: black; } } -.x-media-type-icon('application/xhtml+xml'; "Content"; #00c; "\f0f6"); /* icon-file-alt */ -.x-media-type-icon('application/oebps-package+xml'; "Book"; #c00; "\f02d"); /* icon-book */ -.x-media-type-icon('application/vnd.org.cnx.section'; "Chapter"; #999; ""); /* icon-folder-open */ +.x-media-type-icon('application/xhtml+xml'; "Content"; #00c; "\f0f6"); /* icon-file-alt */ +.x-media-type-icon('application/oebps-package+xml'; "Book"; #c00; "\f02d"); /* icon-book */ +.x-media-type-icon('application/vnd.org.cnx.section'; "Chapter"; #999; ""); /* icon-folder-open */ +.x-media-type-icon('application/vnd.org.cnx.gdoc-import'; "Google Doc Import"; #999; "\f0d5"); /* icon-google-plus */ // Hide the "Add Book" dropdown from the workspace diff --git a/scripts/gh-book/googlejsapi.js b/scripts/gh-book/googlejsapi.js new file mode 100644 index 00000000..bba2e004 --- /dev/null +++ b/scripts/gh-book/googlejsapi.js @@ -0,0 +1,42 @@ +/* modified: http://www.google.com/jsapi e.g. ClientLocation is set to null */ + +if(!window['googleLT_']){window['googleLT_']=(new Date()).getTime();}if (!window['google']) { + window['google'] = {}; +} +if (!window['google']['loader']) { + window['google']['loader'] = {}; + google.loader.ServiceBase = 'http://www.google.com/uds'; + google.loader.GoogleApisBase = 'http://ajax.googleapis.com/ajax'; + google.loader.ApiKey = 'notsupplied'; + google.loader.KeyVerified = true; + google.loader.LoadFailure = false; + google.loader.Secure = false; + google.loader.GoogleLocale = 'www.google.com'; + google.loader.ClientLocation = null; + google.loader.AdditionalParams = ''; + (function() {var d=void 0,g=!0,h=null,j=!1,k=encodeURIComponent,l=window,m=document;function n(a,b){return a.load=b}var p="push",q="replace",r="charAt",t="indexOf",u="ServiceBase",v="name",w="getTime",x="length",y="prototype",z="setTimeout",A="loader",B="substring",C="join",D="toLowerCase";function E(a){return a in F?F[a]:F[a]=-1!=navigator.userAgent[D]()[t](a)}var F={};function G(a,b){var c=function(){};c.prototype=b[y];a.S=b[y];a.prototype=new c} + function H(a,b,c){var e=Array[y].slice.call(arguments,2)||[];return function(){var c=e.concat(Array[y].slice.call(arguments));return a.apply(b,c)}}function I(a){a=Error(a);a.toString=function(){return this.message};return a}function J(a,b){for(var c=a.split(/\./),e=l,f=0;f<\/script>"):(E("safari")||E("konqueror"))&&l[z](T,10)),P[p](a)):Q(l,"load",a)};L("google.setOnLoadCallback",google.R); + function Q(a,b,c){if(a.addEventListener)a.addEventListener(b,c,j);else if(a.attachEvent)a.attachEvent("on"+b,c);else{var e=a["on"+b];a["on"+b]=e!=h?aa([c,e]):c}}function aa(a){return function(){for(var b=0;b<\/script>'):"css"==a&&m.write('')}; + L("google.loader.writeLoadTag",google[A].d);google[A].O=function(a){O=a};L("google.loader.rfm",google[A].O);google[A].Q=function(a){for(var b in a)"string"==typeof b&&b&&":"==b[r](0)&&!N[b]&&(N[b]=new U(b[B](1),a[b]))};L("google.loader.rpl",google[A].Q);google[A].P=function(a){if((a=a.specs)&&a[x])for(var b=0;b +], (Backbone, mediaTypes, allContent, loadable, XhtmlFile, GoogleDocXhtmlFile, TocNode, TocPointerNode, Utils) -> SAVE_DELAY = 10 # ms @@ -16,7 +17,7 @@ define [ serializer = new XMLSerializer() mediaType: 'application/oebps-package+xml' - accept: [XhtmlFile::mediaType, TocNode::mediaType] + accept: [XhtmlFile::mediaType, TocNode::mediaType, GoogleDocXhtmlFile::uniqueMediaType] branch: true # This element will show up in the sidebar listing