From dd1d3c670802c6cd60f11415ac0ad4f0563d863a Mon Sep 17 00:00:00 2001 From: David Sichau Date: Tue, 17 May 2016 16:29:47 +0200 Subject: [PATCH 1/6] Use npm dependencies and module system Sharejs now imports all dependencies as npm dependencies. This avoids the setup of gitsubmodules. It uses the module system to simplify the handling of the dependencies. Most of the code was rewritten in plain js with ecmascript. --- .gitmodules | 6 -- History.md | 6 ++ demo/.meteor/packages | 6 +- demo/.meteor/versions | 6 +- demo/client/client.coffee | 69 ------------- demo/client/client.js | 106 ++++++++++++++++++++ demo/client/index.html | 1 + demo/index.coffee | 6 -- demo/index.js | 17 ++++ sharejs-ace/History.md | 4 + sharejs-ace/ace-builds | 1 - sharejs-ace/ace.js | 1 + sharejs-ace/client.coffee | 44 -------- sharejs-ace/client.js | 67 +++++++++++++ sharejs-ace/package.js | 67 ++----------- sharejs-base/loadBCSocket.js | 8 ++ sharejs-base/package.js | 30 ++---- sharejs-base/sharejs-client.coffee | 117 ---------------------- sharejs-base/sharejs-client.js | 156 +++++++++++++++++++++++++++++ sharejs-base/sharejs-server.coffee | 71 ------------- sharejs-base/sharejs-server.js | 83 +++++++++++++++ sharejs-base/textarea.js | 114 +++++++++++++++++++++ sharejs-codemirror/History.md | 4 + sharejs-codemirror/client.coffee | 33 ------ sharejs-codemirror/client.js | 62 ++++++++++++ sharejs-codemirror/codemirror | 1 - sharejs-codemirror/package.js | 32 ++---- 27 files changed, 660 insertions(+), 458 deletions(-) delete mode 100644 .gitmodules delete mode 100644 demo/client/client.coffee create mode 100644 demo/client/client.js delete mode 100644 demo/index.coffee create mode 100644 demo/index.js delete mode 160000 sharejs-ace/ace-builds delete mode 100644 sharejs-ace/client.coffee create mode 100644 sharejs-ace/client.js create mode 100644 sharejs-base/loadBCSocket.js delete mode 100644 sharejs-base/sharejs-client.coffee create mode 100644 sharejs-base/sharejs-client.js delete mode 100644 sharejs-base/sharejs-server.coffee create mode 100644 sharejs-base/sharejs-server.js create mode 100644 sharejs-base/textarea.js delete mode 100644 sharejs-codemirror/client.coffee create mode 100644 sharejs-codemirror/client.js delete mode 160000 sharejs-codemirror/codemirror diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index eefcb85..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "ace-builds"] - path = sharejs-ace/ace-builds - url = https://github.com/ajaxorg/ace-builds.git -[submodule "codemirror"] - path = sharejs-codemirror/codemirror - url = https://github.com/marijnh/codemirror diff --git a/History.md b/History.md index 467c441..3713422 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,11 @@ ## vNEXT +## v0.9.0 + +* dependencies are imported as npm modules +* Code is rewritten in ecmascript +* used module system to reduce globals + ## v0.8.0 * supports Meteor 1.3 diff --git a/demo/.meteor/packages b/demo/.meteor/packages index 09cf9e8..a04c95c 100644 --- a/demo/.meteor/packages +++ b/demo/.meteor/packages @@ -6,9 +6,11 @@ autopublish insecure standard-app-packages -coffeescript stylus -mizzao:sharejs-ace +mizzao:sharejs mizzao:sharejs-codemirror +mizzao:sharejs-ace twbs:bootstrap standard-minifiers +modules +ecmascript diff --git a/demo/.meteor/versions b/demo/.meteor/versions index c937f05..f514f3b 100644 --- a/demo/.meteor/versions +++ b/demo/.meteor/versions @@ -39,9 +39,9 @@ meteor-platform@1.2.6 minifier-css@1.1.11 minifier-js@1.1.11 minimongo@1.0.16 -mizzao:sharejs@0.8.0 -mizzao:sharejs-ace@1.3.0 -mizzao:sharejs-codemirror@4.12.1 +mizzao:sharejs@0.9.0 +mizzao:sharejs-ace@1.4.0 +mizzao:sharejs-codemirror@5.14.2 mobile-status-bar@1.0.12 modules@0.6.1 modules-runtime@0.6.3 diff --git a/demo/client/client.coffee b/demo/client/client.coffee deleted file mode 100644 index 454b7c1..0000000 --- a/demo/client/client.coffee +++ /dev/null @@ -1,69 +0,0 @@ -Template.docList.helpers - documents: -> Documents.find() - -Template.docList.events = - "click button": -> - Documents.insert - title: "untitled" - , (err, id) -> - return unless id - Session.set("document", id) - -Template.docItem.helpers - current: -> Session.equals("document", @_id) - -Template.docItem.events = - "click a": (e) -> - e.preventDefault() - Session.set("document", @_id) - -Session.setDefault("editorType", "ace") - -Template.docTitle.helpers - title: -> - # Strange bug https://github.com/meteor/meteor/issues/1447 - Documents.findOne(@+"")?.title - - editorType: (type) -> Session.equals("editorType", type) - -Template.editor.helpers - docid: -> Session.get("document") - -Template.editor.events = - "keydown input[name=title]": (e) -> - return unless e.keyCode == 13 - e.preventDefault() - - $(e.target).blur() - id = Session.get("document") - Documents.update id, - title: e.target.value - - "click button": (e) -> - e.preventDefault() - id = Session.get("document") - Session.set("document", null) - Meteor.call "deleteDocument", id - - "change input[name=editor]": (e) -> - Session.set("editorType", e.target.value) - -Template.editor.helpers - textarea: -> Session.equals("editorType", "textarea") - cm: -> Session.equals("editorType", "cm") - ace: -> Session.equals("editorType", "ace") - - configAce: -> - (ace) -> - # Set some reasonable options on the editor - ace.setTheme('ace/theme/monokai') - ace.setShowPrintMargin(false) - ace.getSession().setUseWrapMode(true) - - configCM: -> - (cm) -> - cm.setOption("theme", "default") - cm.setOption("lineNumbers", true) - cm.setOption("lineWrapping", true) - cm.setOption("smartIndent", true) - cm.setOption("indentWithTabs", true) diff --git a/demo/client/client.js b/demo/client/client.js new file mode 100644 index 0000000..4441ca6 --- /dev/null +++ b/demo/client/client.js @@ -0,0 +1,106 @@ + + +Template.docList.helpers({ + documents: function() { + return Documents.find(); + } +}); + +Template.docList.events = { + "click button": function() { + return Documents.insert({ + title: "untitled" + }, function(err, id) { + if (!id) { + return; + } + return Session.set("document", id); + }); + } +}; + +Template.docItem.helpers({ + current: function() { + return Session.equals("document", this._id); + } +}); + +Template.docItem.events = { + "click a": function(e) { + e.preventDefault(); + return Session.set("document", this._id); + } +}; + +Session.setDefault("editorType", "ace"); + +Template.docTitle.helpers({ + title: function() { + var ref; + return (ref = Documents.findOne(this + "")) != null ? ref.title : void 0; + }, + editorType: function(type) { + return Session.equals("editorType", type); + } +}); + +Template.editor.helpers({ + docid: function() { + return Session.get("document"); + } +}); + +Template.editor.events = { + "keydown input[name=title]": function(e) { + var id; + if (e.keyCode !== 13) { + return; + } + e.preventDefault(); + $(e.target).blur(); + id = Session.get("document"); + return Documents.update(id, { + title: e.target.value + }); + }, + "click button": function(e) { + var id; + e.preventDefault(); + id = Session.get("document"); + Session.set("document", null); + return Meteor.call("deleteDocument", id); + }, + "change input[name=editor]": function(e) { + return Session.set("editorType", e.target.value); + } +}; + +Template.editor.helpers({ + textarea: function() { + return Session.equals("editorType", "textarea"); + }, + cm: function() { + return Session.equals("editorType", "cm"); + }, + ace: function() { + return Session.equals("editorType", "ace"); + }, + configAce: function() { + return function(ace) { + ace.setTheme('ace/theme/monokai'); + ace.setShowPrintMargin(false); + return ace.getSession().setUseWrapMode(true); + }; + }, + configCM: function() { + return function(cm) { + cm.setOption("theme", "default"); + cm.setOption("lineNumbers", true); + cm.setOption("lineWrapping", true); + cm.setOption("smartIndent", true); + return cm.setOption("indentWithTabs", true); + }; + } +}); + + diff --git a/demo/client/index.html b/demo/client/index.html index d5a596e..5759d88 100644 --- a/demo/client/index.html +++ b/demo/client/index.html @@ -21,6 +21,7 @@ {{> docList}}
+ {{> editor}}
diff --git a/demo/index.coffee b/demo/index.coffee deleted file mode 100644 index efec0ab..0000000 --- a/demo/index.coffee +++ /dev/null @@ -1,6 +0,0 @@ -this.Documents = new Meteor.Collection("documents") - -Meteor.methods - deleteDocument: (id) -> - Documents.remove(id) - ShareJS.model.delete(id) unless @isSimulation # ignore error diff --git a/demo/index.js b/demo/index.js new file mode 100644 index 0000000..b5ccf47 --- /dev/null +++ b/demo/index.js @@ -0,0 +1,17 @@ +/** + * + * Created by dsichau on 11.05.16. + */ + +import {ShareJS} from 'meteor/mizzao:sharejs'; + +this.Documents = new Meteor.Collection("documents"); + +Meteor.methods({ + deleteDocument(id){ + Documents.remove(id); + if(Meteor.isServer) { + ShareJS.model.delete(id); + } + } +}); diff --git a/sharejs-ace/History.md b/sharejs-ace/History.md index b993a3b..3ce2599 100644 --- a/sharejs-ace/History.md +++ b/sharejs-ace/History.md @@ -1,3 +1,7 @@ +## v1.4.0 + + * Ace is added as npm dependencie + ## v1.3.0 * Support of Meteor 1.3 diff --git a/sharejs-ace/ace-builds b/sharejs-ace/ace-builds deleted file mode 160000 index 4c15514..0000000 --- a/sharejs-ace/ace-builds +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c1551465499afb012ddb053a18872de83350357 diff --git a/sharejs-ace/ace.js b/sharejs-ace/ace.js index a22d566..4ec7f99 100644 --- a/sharejs-ace/ace.js +++ b/sharejs-ace/ace.js @@ -1,5 +1,6 @@ // Generated by CoffeeScript 1.6.2 +require('ace-builds/src/ace'); (function() { var Range, applyToShareJS, requireImpl; diff --git a/sharejs-ace/client.coffee b/sharejs-ace/client.coffee deleted file mode 100644 index 91a7359..0000000 --- a/sharejs-ace/client.coffee +++ /dev/null @@ -1,44 +0,0 @@ -# Set asset path in Ace config -require('ace/config').set('basePath', '/packages/mizzao_sharejs-ace/ace-builds/src') -UndoManager = require('ace/undomanager').UndoManager - -class ShareJSAceConnector extends ShareJSConnector - createView: -> - return Blaze.With(Blaze.getData, -> Template._sharejsAce) - - rendered: (element) -> - super - @ace = ace.edit(element) - @ace.getSession().setValue("loading...") - # Configure the editor if specified - @configCallback?(@ace) - - connect: -> - @ace.setReadOnly(true); # Disable editing until share is connected - super - - attach: (doc) -> - super - doc.attach_ace(@ace) - # Reset undo stack, so that we can't undo to an empty document - # XXX It seems that we should be able to use getUndoManager().reset() - # here, but that doesn't seem to work: - # http://japhr.blogspot.com/2012/10/ace-undomanager-and-setvalue.html - @ace.getSession().setUndoManager(new UndoManager) - @ace.setReadOnly(false) - @connectCallback?(@ace) - - disconnect: -> - # Detach ace editor, if any - @doc?.detach_ace?() - super - - destroy: -> - super - # Meteor._debug "destroying Ace editor" - @ace?.destroy() - @ace = null - -UI.registerHelper "sharejsAce", new Template('sharejsAce', -> - return new ShareJSAceConnector(this).create() -) diff --git a/sharejs-ace/client.js b/sharejs-ace/client.js new file mode 100644 index 0000000..3d5cbf6 --- /dev/null +++ b/sharejs-ace/client.js @@ -0,0 +1,67 @@ +/** + * Created by dsichau on 27.04.16. + */ + +import { Template } from 'meteor/templating' +import { Blaze } from 'meteor/blaze' +import { ShareJSConnector } from 'meteor/mizzao:sharejs' + +require('ace-builds/src/ace'); +ace.require('ace/config').set('basePath', 'https://cdn.jsdelivr.net/ace/1.2.2/min'); +UndoManager = ace.require('ace/undomanager').UndoManager; +import './ace' + +class ShareJSAceConnector extends ShareJSConnector { + + createView() { + return Blaze.With(Blaze.getData(), function(){ + return Template._sharejsAce; + }); + } + rendered(element){ + super.rendered(element); + this.ace = ace.edit(element); + this.ace.getSession().setValue("loading..."); + if (typeof this.configCallback === "function") { + this.configCallback(this.ace); + } + } + connect() { + this.ace.setReadOnly(true); + super.connect(this.docIdVar.get()); + } + attach(doc){ + super.attach(doc); + doc.attach_ace(this.ace); + // Reset undo stack, so that we can't undo to an empty document + // XXX It seems that we should be able to use getUndoManager().reset() + // here, but that doesn't seem to work: + // http://japhr.blogspot.com/2012/10/ace-undomanager-and-setvalue.html + this.ace.getSession().setUndoManager(new UndoManager()); + this.ace.setReadOnly(false); + if (typeof this.connectCallback === "function") { + this.connectCallback(this.ace); + } + } + disconnect() { + const ref = this.doc; + if (ref != null) { + if (typeof ref.detach_ace === "function") { + ref.detach_ace(); + } + } + super.disconnect(); + } + destroy() { + super.destroy(); + // Meteor._debug "destroying textarea editor" + if(this.ace) { + this.ace.destroy(); + } + this.ace = null; + } +} + +Template.registerHelper("sharejsAce", new Template('sharejsAce',function(){ + return new ShareJSAceConnector(this).create(); +})); \ No newline at end of file diff --git a/sharejs-ace/package.js b/sharejs-ace/package.js index 057bfe4..582b700 100644 --- a/sharejs-ace/package.js +++ b/sharejs-ace/package.js @@ -1,76 +1,25 @@ Package.describe({ name: "mizzao:sharejs-ace", summary: "ShareJS with the Ace Editor", - version: "1.3.0", + version: "1.4.0", git: "https://github.com/mizzao/meteor-sharejs.git" }); -// Ugly-ass function stolen from http://stackoverflow.com/a/20794116/586086 -// TODO make this less ugly in future -function getFilesFromFolder(packageName, folder){ - // local imports - var _ = Npm.require("underscore"); - var fs = Npm.require("fs"); - var path = Npm.require("path"); - // helper function, walks recursively inside nested folders and return absolute filenames - function walk(folder){ - var filenames = []; - // get relative filenames from folder - var folderContent = fs.readdirSync(folder); - // iterate over the folder content to handle nested folders - _.each(folderContent, function(filename) { - // build absolute filename - var absoluteFilename = path.join(folder, filename); - // get file stats - var stat = fs.statSync(absoluteFilename); - if(stat.isDirectory()){ - // directory case => add filenames fetched from recursive call - filenames = filenames.concat(walk(absoluteFilename)); - } - else{ - // file case => simply add it - filenames.push(absoluteFilename); - } - }); - return filenames; - } - // save current working directory (something like "/home/user/projects/my-project") - var cwd = process.cwd(); - var isRunningFromApp = fs.existsSync(path.resolve("packages")); - var packagePath = isRunningFromApp ? path.resolve("packages", packageName) : ""; - - packagePath = path.resolve(packagePath); - // chdir to our package directory - process.chdir(path.join(packagePath)); - // launch initial walk - var result = walk(folder); - // restore previous cwd - process.chdir(cwd); - return result; -} +Npm.depends({ + "ace-builds": "1.2.2" +}); Package.onUse(function (api) { api.versionsFrom("1.3"); - api.use(['coffeescript', 'templating']); + api.use(['ecmascript', 'modules', 'templating']); - api.use("mizzao:sharejs@0.8.0"); + api.use("mizzao:sharejs@0.9.0"); api.imply("mizzao:sharejs"); - - var _ = Npm.require("underscore"); - - // Ace editor for the client - var aceJS = 'ace-builds/src/ace.js'; - api.addFiles(aceJS, 'client', { bare: true }); - - // Add Ace files as assets that can be loaded by the client later - var aceSettings = getFilesFromFolder("mizzao:sharejs-ace", "ace-builds/src"); - api.addAssets(_.without(aceSettings, aceJS), 'client'); + api.mainModule('client.js', 'client'); api.addFiles([ - 'templates.html', - 'client.coffee', - 'ace.js' + 'templates.html' ], 'client'); }); diff --git a/sharejs-base/loadBCSocket.js b/sharejs-base/loadBCSocket.js new file mode 100644 index 0000000..ccedf96 --- /dev/null +++ b/sharejs-base/loadBCSocket.js @@ -0,0 +1,8 @@ +/** + * Created by dsichau on 11.05.16. + */ + +//This is required to load BCSocket into the global scope for sharejs +//http://stackoverflow.com/questions/31197220/can-i-use-an-es6-2015-module-import-to-set-a-reference-in-global-scope +import {BCSocket as bc} from 'share/node_modules/browserchannel/dist/bcsocket-uncompressed' +window.BCSocket = bc; \ No newline at end of file diff --git a/sharejs-base/package.js b/sharejs-base/package.js index 97ce013..c51c51e 100644 --- a/sharejs-base/package.js +++ b/sharejs-base/package.js @@ -1,7 +1,7 @@ Package.describe({ name: "mizzao:sharejs", summary: "server (& client library) to allow concurrent editing of any kind of content", - version: "0.8.1", + version: "0.9.0", git: "https://github.com/mizzao/meteor-sharejs.git" }); @@ -14,36 +14,18 @@ Npm.depends({ Package.onUse(function (api) { api.versionsFrom("1.3"); - api.use(['coffeescript', 'underscore']); + api.use(['underscore', 'ecmascript', 'modules']); api.use(['handlebars', 'templating'], 'client'); - api.use(['mongo-livedata', 'routepolicy', 'webapp'], 'server'); + api.use(['coffeescript', 'mongo-livedata', 'routepolicy', 'webapp'], 'server'); - // ShareJS script files - api.addFiles([ - '.npm/package/node_modules/share/node_modules/browserchannel/dist/bcsocket-uncompressed.js', - '.npm/package/node_modules/share/webclient/share.uncompressed.js' - ], 'client'); - - // Add the ShareJS connectors - api.addFiles('.npm/package/node_modules/share/webclient/textarea.js', 'client'); + api.mainModule('sharejs-client.js', 'client'); + api.mainModule('sharejs-server.js', 'server'); // Our files api.addFiles([ - 'sharejs-templates.html', - 'sharejs-client.coffee' + 'sharejs-templates.html' ], 'client'); - // Server files - api.addFiles([ - 'sharejs-meteor-auth.coffee', - 'sharejs-server.coffee' - ], 'server'); - - // Export the ShareJS interface - api.export('ShareJS', 'server'); - - // For subpackages to extend client functionality - api.export('ShareJSConnector', 'client'); }); Package.onTest(function (api) { diff --git a/sharejs-base/sharejs-client.coffee b/sharejs-base/sharejs-client.coffee deleted file mode 100644 index 02efc52..0000000 --- a/sharejs-base/sharejs-client.coffee +++ /dev/null @@ -1,117 +0,0 @@ -class ShareJSConnector - - getOptions = -> - origin: '//' + window.location.host + '/channel' - authentication: Meteor.userId?() or null # accounts-base may not be in the app - - constructor: (parentView) -> - # Create a ReactiveVar that tracks the docId that was passed in - docIdVar = new Blaze.ReactiveVar - - parentView.onViewReady -> - this.autorun -> - data = Blaze.getData() - docIdVar.set(data.docid) - - parentView.onViewDestroyed => - this.destroy() - - @isCreated = false - @docIdVar = docIdVar - - # Configure any callbacks if specified - params = Blaze.getData(parentView) - @configCallback = params.onRender - @connectCallback = params.onConnect - - create: -> - throw new Error("Already created") if @isCreated - connector = this - @isCreated = true - - @view = @createView() - @view.onViewReady -> - connector.rendered( this.firstNode() ) - - this.autorun -> - # By grabbing docId here, we ensure that we only try to connect when - # this is rendered. - docId = connector.docIdVar.get() - - # Disconnect any existing connections - connector.disconnect() - connector.connect(docId) if docId - - return @view - - # Set up the context when rendered. - rendered: (element) -> - this.element = element - - # Connect to a document. - connect: (docId, element) -> - @connectingId = docId - - sharejs.open docId, 'text', getOptions(), (error, doc) => - if error - Meteor._debug(error) - return - - # Don't attach if re-render happens too quickly and we're trying to - # connect to a different document now. - unless @connectingId is doc.name - doc.close() # Close immediately - else - @attach(doc) - - # Attach shareJS to the on-screen editor - attach: (doc) -> - @doc = doc - - # Disconnect from ShareJS. This should be idempotent. - disconnect: -> - # Close connection to the ShareJS doc - if @doc? - @doc.close() - @doc = null - - # Destroy the connector and make sure everything's disconnected. - destroy: -> - throw new Error("Already destroyed") if @isDestroyed - - @disconnect() - @view = null - @isDestroyed = true - -class ShareJSTextConnector extends ShareJSConnector - - createView: -> - return Blaze.With(Blaze.getData, -> Template._sharejsText) - - rendered: (element) -> - super - @textarea = element - @configCallback?(@textarea) - - connect: -> - @textarea.disabled = true - super - - attach: (doc) -> - super - doc.attach_textarea(@textarea) - @textarea.disabled = false - @connectCallback?(@textarea) - - disconnect: -> - @textarea?.detach_share?() - super - - destroy: -> - super - # Meteor._debug "destroying textarea editor" - @textarea = null - -UI.registerHelper "sharejsText", new Template('sharejsText', -> - return new ShareJSTextConnector(this).create() -) diff --git a/sharejs-base/sharejs-client.js b/sharejs-base/sharejs-client.js new file mode 100644 index 0000000..bb03019 --- /dev/null +++ b/sharejs-base/sharejs-client.js @@ -0,0 +1,156 @@ +/** + * Created by dsichau on 27.04.16. + */ +import { Template } from 'meteor/templating' +import { ReactiveVar } from 'meteor/reactive-var' +import { Blaze } from 'meteor/blaze' + +import './loadBCSocket' +import 'share/webclient/share.uncompressed' +import './textarea' + +export class ShareJSConnector { + + getOptions() { + return { + origin: '//' + window.location.host + '/channel', + authentication: (typeof Meteor.userId === "function" ? Meteor.userId() : void 0) || null + }; + }; + + constructor(parentView) { + const docIdVar = new ReactiveVar(); + + parentView.onViewReady(function() { + return this.autorun(function() { + const data = Blaze.getData(); + return docIdVar.set(data.docid); + }); + }); + + parentView.onViewDestroyed(()=> { + this.destroy() + }); + + this.isCreated = false; + this.docIdVar = docIdVar; + // Configure any callbacks if specified + const params = Blaze.getData(parentView); + this.configCallback = params.onRender; + this.connectCallback = params.onConnect; + } + + create() { + if (this.isCreated) { + throw new Error("Already created"); + } + const connector = this; + this.isCreated = true; + this.view = this.createView(); + this.view.onViewReady(function() { + connector.rendered(this.firstNode()); + return this.autorun(function() { + var docId; + docId = connector.docIdVar.get(); + connector.disconnect(); + if (docId) { + return connector.connect(docId); + } + }); + }); + return this.view; + } + + rendered(element) { + this.element = element; + } + + connect(docId, element) { + this.connectingId = docId; + sharejs.open(docId, 'text', this.getOptions(), (error, doc) => { + if (error) { + console.log(error); + return + } + // Don't attach if re-render happens too quickly and we' re trying to + // connect to a different document now. + if(this.connectingId !== doc.name) { + doc.close(); + } + else { + this.attach(doc); + } + }); + } + + attach(doc) { + this.doc = doc; + } + + disconnect() { + if(this.doc) { + this.doc.close(); + this.doc = null; + } + } + + destroy() { + if(this.isDestroyed) { + throw new Error("Already destroyed") + } + this.disconnect(); + this.view = null; + this.isDestroyed = true; + } +} + +class ShareJSTextConnector extends ShareJSConnector { + + createView() { + return Blaze.With(Blaze.getData(),function() { + return Template._sharejsText; + }) + } + + rendered(element) { + super.rendered(element); + this.textarea = element; + if (typeof this.configCallback === "function") { + this.configCallback(this.textarea); + } + } + + connect() { + this.textarea.disabled = true; + super.connect(this.docIdVar.get()); + } + + attach(doc) { + super.attach(doc); + doc.attach_textarea(this.textarea); + this.textarea.disabled = false; + if (typeof this.connectCallback === "function") { + this.connectCallback(this.textarea); + } + } + + disconnect() { + const ref = this.textarea; + if (ref != null) { + if (typeof ref.detach_share === "function") { + ref.detach_share(); + } + } + super.disconnect(); + } + + destroy() { + super.destroy(); + // Meteor._debug "destroying textarea editor" + this.textarea = null + } +} + +Template.registerHelper("sharejsText", new Template('sharejsText', function() { + return new ShareJSTextConnector(this).create() +})); \ No newline at end of file diff --git a/sharejs-base/sharejs-server.coffee b/sharejs-base/sharejs-server.coffee deleted file mode 100644 index 40676d4..0000000 --- a/sharejs-base/sharejs-server.coffee +++ /dev/null @@ -1,71 +0,0 @@ -# Creates a (persistent) ShareJS server -# Based on https://github.com/share/ShareJS/wiki/Getting-started -Future = Npm.require('fibers/future') - -ShareJS = ShareJS || {} -# See docs for options. Uses mongo by default to enable persistence. - -# Using special options from https://github.com/share/ShareJS/blob/master/src/server/index.coffee -options = _.extend { - staticPath: null # Meteor is serving up this application - # rest: null # disable REST interface? - db: { - # Default option is Mongo because Meteor provides it - type: 'mongo' - # A doc/op indexed collection keeps the namespace cleaner in a Meteor app. - opsCollectionPerDoc: false - } -}, Meteor.settings.sharejs?.options - -switch options.db.type - when 'mongo' - ### - ShareJS 0.6.3 mongo driver: - https://github.com/share/ShareJS/blob/v0.6.3/src/server/db/mongo.coffee - It will create its own indices on the 'ops' collection. - ### - options.db.client = MongoInternals.defaultRemoteCollectionDriver().mongo.db - - # Disable the open command due to the bug introduced in ShareJS 0.6.3 - # where an open database connection is not accepted - # https://github.com/share/ShareJS/commit/f98a4adeca396df3ec6b1d838b965ff158f452a3 - - # Meteor has already opened the database connection, so this should work, - # but watch monkey-patch carefully with changes in how - # https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/mongo_driver.js - # uses the API at - # http://mongodb.github.io/node-mongodb-native/api-generated/mongoclient.html - options.db.client.open = -> - - if options.accounts_auth? - options.auth = new MeteorAccountsAuthHandler(options.accounts_auth, options.db.client).handle - else - Meteor._debug "ShareJS: using unsupported db type " + options.db.type + ", falling back to in-memory." - -# Declare the path that ShareJS uses to Meteor -RoutePolicy.declare('/channel/', 'network'); - -# Attach the sharejs REST and bcsocket interfaces as middleware to the meteor connect server -Npm.require('share').server.attach(WebApp.connectHandlers, options); - -### - ShareJS attaches the server API to a weird place. Oh well... - https://github.com/share/ShareJS/blob/v0.6.2/src/server/index.coffee -### -ShareJS.model = WebApp.connectHandlers.model - -# A convenience function for creating a document on the server. -ShareJS.initializeDoc = (docName, content) -> - ShareJS.model.create docName, 'text', {}, (err) -> - if err - console.log(err) - return - # One op; insert all the content at position 0 - # https://github.com/share/ShareJS/wiki/Server-api - opData = { - op: [ {i: content, p: 0} ] - v: 0 - meta: {} - } - ShareJS.model.applyOp docName, opData, (err, res) -> - console.log(err) if err diff --git a/sharejs-base/sharejs-server.js b/sharejs-base/sharejs-server.js new file mode 100644 index 0000000..e624d70 --- /dev/null +++ b/sharejs-base/sharejs-server.js @@ -0,0 +1,83 @@ +// Creates a (persistent) ShareJS server +// Based on https://github.com/share/ShareJS/wiki/Getting-started + +import "./sharejs-meteor-auth.coffee" + +import { Meteor } from 'meteor/meteor'; + +const Future = Npm.require('fibers/future'); + +export const ShareJS = ShareJS || {}; +// See docs for options. Uses mongo by default to enable persistence. + +// Using special options from https://github.com/share/ShareJS/blob/master/src/server/index.coffee +const options = _.extend({ + staticPath: null, + db: { + type: 'mongo', // Default option is Mongo because Meteor provides it + opsCollectionPerDoc: false // A doc/op indexed collection keeps the namespace cleaner in a Meteor app. + } +}, (ref = Meteor.settings.sharejs) != null ? ref.options : void 0); + +switch (options.db.type) { + case 'mongo': + + /* + ShareJS 0.6.3 mongo driver: + https://github.com/share/ShareJS/blob/v0.6.3/src/server/db/mongo.coffee + It will create its own indices on the 'ops' collection. + */ + options.db.client = MongoInternals.defaultRemoteCollectionDriver().mongo.db; + /* Disable the open command due to the bug introduced in ShareJS 0.6.3 + where an open database connection is not accepted + https://github.com/share/ShareJS/commit/f98a4adeca396df3ec6b1d838b965ff158f452a3 + + Meteor has already opened the database connection, so this should work, + but watch monkey-patch carefully with changes in how + https://github.com/meteor/meteor/blob/devel/packages/mongo-livedata/mongo_driver.js + uses the API at + http://mongodb.github.io/node-mongodb-native/api-generated/mongoclient.html + */ + options.db.client.open = function() {}; + if (options.accounts_auth != null) { + options.auth = new MeteorAccountsAuthHandler(options.accounts_auth, options.db.client).handle; + } + break; + default: + Meteor._debug("ShareJS: using unsupported db type " + options.db.type + ", falling back to in-memory."); +} +//Declare the path that ShareJS uses to Meteor +RoutePolicy.declare('/channel/', 'network'); +// Attach the sharejs REST and bcsocket interfaces as middleware to the meteor connect server +Npm.require('share').server.attach(WebApp.connectHandlers, options); + + +/* + ShareJS attaches the server API to a weird place. Oh well... + https://github.com/share/ShareJS/blob/v0.6.2/src/server/index.coffee + */ + +ShareJS.model = WebApp.connectHandlers.model; +// A convenience function for creating a document on the server. +ShareJS.initializeDoc = function(docName, content) { + return ShareJS.model.create(docName, 'text', {}, function(err) { + var opData; + if (err) { + console.log(err); + return; + } + //One op; insert all the content at position 0 + //https://github.com/share/ShareJS/wiki/Server-api + opData = { + op: [{ i: content, p: 0 }], + v: 0, + meta: {} + }; + return ShareJS.model.applyOp(docName, opData, function(err, res) { + if (err) { + return console.log(err); + } + }); + }); +}; + diff --git a/sharejs-base/textarea.js b/sharejs-base/textarea.js new file mode 100644 index 0000000..36e5408 --- /dev/null +++ b/sharejs-base/textarea.js @@ -0,0 +1,114 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + var applyChange; + + applyChange = function(doc, oldval, newval) { + var commonEnd, commonStart; + + if (oldval === newval) { + return; + } + commonStart = 0; + while (oldval.charAt(commonStart) === newval.charAt(commonStart)) { + commonStart++; + } + commonEnd = 0; + while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) && commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) { + commonEnd++; + } + if (oldval.length !== commonStart + commonEnd) { + doc.del(commonStart, oldval.length - commonStart - commonEnd); + } + if (newval.length !== commonStart + commonEnd) { + return doc.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd)); + } + }; + + sharejs.extendDoc('attach_textarea', function(elem) { + var delete_listener, doc, event, genOp, insert_listener, prevvalue, replaceText, _i, _len, _ref, + _this = this; + + doc = this; + elem.value = this.getText(); + prevvalue = elem.value; + replaceText = function(newText, transformCursor) { + var newSelection, scrollTop; + + newSelection = [transformCursor(elem.selectionStart), transformCursor(elem.selectionEnd)]; + scrollTop = elem.scrollTop; + elem.value = newText; + if (elem.scrollTop !== scrollTop) { + elem.scrollTop = scrollTop; + } + if (window.document.activeElement === elem) { + return elem.selectionStart = newSelection[0], elem.selectionEnd = newSelection[1], newSelection; + } + }; + this.on('insert', insert_listener = function(pos, text) { + var transformCursor; + + transformCursor = function(cursor) { + if (pos < cursor) { + return cursor + text.length; + } else { + return cursor; + } + }; + prevvalue = elem.value.replace(/\r\n/g, '\n'); + return replaceText(prevvalue.slice(0, pos) + text + prevvalue.slice(pos), transformCursor); + }); + this.on('delete', delete_listener = function(pos, text) { + var transformCursor; + + transformCursor = function(cursor) { + if (pos < cursor) { + return cursor - Math.min(text.length, cursor - pos); + } else { + return cursor; + } + }; + prevvalue = elem.value.replace(/\r\n/g, '\n'); + return replaceText(prevvalue.slice(0, pos) + prevvalue.slice(pos + text.length), transformCursor); + }); + genOp = function(event) { + var onNextTick; + + onNextTick = function(fn) { + return setTimeout(fn, 0); + }; + return onNextTick(function() { + if (elem.value !== prevvalue) { + prevvalue = elem.value; + return applyChange(doc, doc.getText(), elem.value.replace(/\r\n/g, '\n')); + } + }); + }; + _ref = ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste']; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + event = _ref[_i]; + if (elem.addEventListener) { + elem.addEventListener(event, genOp, false); + } else { + elem.attachEvent('on' + event, genOp); + } + } + return elem.detach_share = function() { + var _j, _len1, _ref1, _results; + + _this.removeListener('insert', insert_listener); + _this.removeListener('delete', delete_listener); + _ref1 = ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste']; + _results = []; + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + event = _ref1[_j]; + if (elem.removeEventListener) { + _results.push(elem.removeEventListener(event, genOp, false)); + } else { + _results.push(elem.detachEvent('on' + event, genOp)); + } + } + return _results; + }; + }); + +}).call(this); diff --git a/sharejs-codemirror/History.md b/sharejs-codemirror/History.md index 3936698..dfc322c 100644 --- a/sharejs-codemirror/History.md +++ b/sharejs-codemirror/History.md @@ -1,3 +1,7 @@ +## v5.14.2 + +* use Codemirror as npm dependency + ## v4.12.0 * Update to v4.12.0 of CodeMirror editor. diff --git a/sharejs-codemirror/client.coffee b/sharejs-codemirror/client.coffee deleted file mode 100644 index a37c325..0000000 --- a/sharejs-codemirror/client.coffee +++ /dev/null @@ -1,33 +0,0 @@ -class ShareJSCMConnector extends ShareJSConnector - createView: -> - return Blaze.With(Blaze.getData, -> Template._sharejsCM) - - rendered: (element) -> - super - @cm = new CodeMirror(element) - @configCallback?(@cm) - - connect: -> - @cm.readOnly = true - super - - attach: (doc) -> - super - doc.attach_cm(@cm) - @cm.readOnly = false - @connectCallback?(@cm) - - disconnect: -> - # from share/webclient/cm.js - @doc?.detach_cm?() - super - - destroy: -> - super - # Blaze will take it off the DOM, - # Nothing needs to be done explicitly to clean up. - @cm = null - -UI.registerHelper "sharejsCM", new Template('sharejsCM', -> - return new ShareJSCMConnector(this).create() -) diff --git a/sharejs-codemirror/client.js b/sharejs-codemirror/client.js new file mode 100644 index 0000000..3978abc --- /dev/null +++ b/sharejs-codemirror/client.js @@ -0,0 +1,62 @@ +/** + * Created by dsichau on 29.04.16. + */ +import { Template } from 'meteor/templating' +import { Blaze } from 'meteor/blaze' +import { ShareJSConnector } from 'meteor/mizzao:sharejs' + +import CodeMirror from 'codemirror'; +import 'codemirror/addon/fold/foldcode'; +import 'codemirror/addon/fold/foldgutter'; +import 'codemirror/addon/fold/indent-fold'; +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/display/placeholder'; + +import 'codemirror/lib/codemirror.css'; +import 'codemirror/theme/monokai.css'; +import 'codemirror/addon/fold/foldgutter.css'; +import 'codemirror/addon/hint/show-hint.css'; + +import './cm' + +class ShareJSCMConnector extends ShareJSConnector { + createView() { + return Blaze.With(Blaze.getData(), function(){ + return Template._sharejsCM + }); + } + rendered(element){ + super.rendered(element); + this.cm = new CodeMirror(element, {readOnly: true, value: "loading..."}); + if (typeof this.configCallback === "function") { + this.configCallback(this.cm); + } + } + connect() { + super.connect(this.docIdVar.get()); + } + attach(doc){ + super.attach(doc); + doc.attach_cm(this.cm); + this.cm.setOption("readOnly", false); + if (typeof this.connectCallback === "function") { + this.connectCallback(this.cm); + } + } + disconnect() { + const ref = this.doc; + if (ref != null) { + if (typeof ref.detach_cm === "function") { + ref.detach_cm(); + } + } + super.disconnect(); + } + destroy() { + super.destroy(); + this.cm = null; + } +} +Template.registerHelper("sharejsCM", new Template('sharejsCM',function(){ + return new ShareJSCMConnector(this).create(); +})); \ No newline at end of file diff --git a/sharejs-codemirror/codemirror b/sharejs-codemirror/codemirror deleted file mode 160000 index bed3de3..0000000 --- a/sharejs-codemirror/codemirror +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bed3de39aa62616393db2bab4b5fec6ee2238747 diff --git a/sharejs-codemirror/package.js b/sharejs-codemirror/package.js index b9001a9..f622930 100644 --- a/sharejs-codemirror/package.js +++ b/sharejs-codemirror/package.js @@ -1,36 +1,24 @@ Package.describe({ name: "mizzao:sharejs-codemirror", summary: "ShareJS with the CodeMirror Editor", - version: "4.12.1", + version: "5.14.2", git: "https://github.com/mizzao/meteor-sharejs.git" }); +Npm.depends({ + codemirror: "5.14.2" +}); + Package.onUse(function (api) { - api.versionsFrom("1.3"); + api.versionsFrom("1.3.2"); - api.use(['coffeescript', 'templating']); + api.use(['ecmascript', 'modules', 'templating']); - api.use("mizzao:sharejs@0.8.0"); + api.use("mizzao:sharejs@0.9.0"); api.imply("mizzao:sharejs"); - // CM editor for the client - api.addFiles([ - 'codemirror/lib/codemirror.js', - 'codemirror/lib/codemirror.css', - 'codemirror/theme/monokai.css', - 'codemirror/addon/fold/foldgutter.css', - 'codemirror/addon/fold/foldcode.js', - 'codemirror/addon/fold/foldgutter.js', - 'codemirror/addon/fold/indent-fold.js', - 'codemirror/addon/hint/show-hint.js', - 'codemirror/addon/display/placeholder.js', - 'codemirror/addon/hint/show-hint.css' - /* include any extra codemirror ADDONS or MODES or THEMES here !!!! */ - ], 'client', { bare: true }); - + api.mainModule('client.js', 'client'); api.addFiles([ - 'templates.html', - 'client.coffee', - 'cm.js' + 'templates.html' ], 'client'); }); From 773912e7ea8d84853e5d8cd99ccd4d37e59e97e9 Mon Sep 17 00:00:00 2001 From: David Sichau Date: Tue, 17 May 2016 16:45:46 +0200 Subject: [PATCH 2/6] fixed tests --- sharejs-base/package.js | 5 +-- sharejs-base/tests/server_test.js | 42 ++++++++++++++++++++++++++ sharejs-base/tests/server_tests.coffee | 31 ------------------- 3 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 sharejs-base/tests/server_test.js delete mode 100644 sharejs-base/tests/server_tests.coffee diff --git a/sharejs-base/package.js b/sharejs-base/package.js index c51c51e..1be29d0 100644 --- a/sharejs-base/package.js +++ b/sharejs-base/package.js @@ -31,12 +31,13 @@ Package.onUse(function (api) { Package.onTest(function (api) { api.use([ 'random', - 'coffeescript', + 'ecmascript', + 'modules', 'tinytest', 'test-helpers' ]); api.use("mizzao:sharejs"); - api.addFiles('tests/server_tests.coffee', 'server'); + api.addFiles('tests/server_test.js', 'server'); }); diff --git a/sharejs-base/tests/server_test.js b/sharejs-base/tests/server_test.js new file mode 100644 index 0000000..ae660b5 --- /dev/null +++ b/sharejs-base/tests/server_test.js @@ -0,0 +1,42 @@ +/** + * Created by dsichau on 17.05.16. + */ + +import {ShareJS} from 'meteor/sharejs'; +var Docs, Ops, sleep; + +Docs = new Meteor.Collection("docs"); + +Ops = new Meteor.Collection("ops"); + +Docs.remove({}); + +Ops.remove({}); + +sleep = Meteor.wrapAsync(function(time, cb) { + return Meteor.setTimeout((function() { + return cb(void 0); + }), time); +}); + +Tinytest.add("server - model initialized properly", function(test) { + return test.isTrue(ShareJS.model); +}); + +Tinytest.addAsync("server - initialize document with data", function(test, next) { + var doc, docId; + docId = Random.id(); + ShareJS.initializeDoc(docId, "foo"); + sleep(100); + doc = Docs.findOne(docId); + test.isTrue(doc); + test.equal(doc != null ? doc._id : void 0, docId); + test.isTrue(Ops.find().count() > 0); + return ShareJS.model.getSnapshot(docId, function(err, res) { + if (err != null) { + test.fail(); + } + test.equal(res.snapshot, "foo"); + return next(); + }); +}); \ No newline at end of file diff --git a/sharejs-base/tests/server_tests.coffee b/sharejs-base/tests/server_tests.coffee deleted file mode 100644 index df40007..0000000 --- a/sharejs-base/tests/server_tests.coffee +++ /dev/null @@ -1,31 +0,0 @@ -# Hook into the ShareJS collection with Meteor -Docs = new Meteor.Collection("docs") -# Unfortunately, we can't query from this collection because ShareJS uses -# compound ids that aren't supported in Meteor. We can remove from it, however. -Ops = new Meteor.Collection("ops") - -Docs.remove {} -Ops.remove {} - -sleep = Meteor.wrapAsync((time, cb) -> Meteor.setTimeout (-> cb undefined), time) - -Tinytest.add "server - model initialized properly", (test) -> - test.isTrue(ShareJS.model) - -# This test has the added benefit of ensuring that the stack is initialized properly on the server. -Tinytest.addAsync "server - initialize document with data", (test, next) -> - docId = Random.id() - ShareJS.initializeDoc(docId, "foo") - - sleep(100) - - doc = Docs.findOne(docId) - test.isTrue(doc) - test.equal doc?._id, docId - - test.isTrue Ops.find().count() > 0 - - ShareJS.model.getSnapshot docId, (err, res) -> - test.fail() if err? - test.equal res.snapshot, "foo" - next() From f03f732917c0b48a73a356af874676504abd5452 Mon Sep 17 00:00:00 2001 From: David Sichau Date: Wed, 18 May 2016 10:23:43 +0200 Subject: [PATCH 3/6] avoid external cdn --- sharejs-ace/client.js | 2 +- sharejs-ace/package.js | 55 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/sharejs-ace/client.js b/sharejs-ace/client.js index 3d5cbf6..97ae739 100644 --- a/sharejs-ace/client.js +++ b/sharejs-ace/client.js @@ -7,7 +7,7 @@ import { Blaze } from 'meteor/blaze' import { ShareJSConnector } from 'meteor/mizzao:sharejs' require('ace-builds/src/ace'); -ace.require('ace/config').set('basePath', 'https://cdn.jsdelivr.net/ace/1.2.2/min'); +ace.require('ace/config').set('basePath', '/packages/mizzao_sharejs-ace/.npm/package/node_modules/ace-builds/src/'); UndoManager = ace.require('ace/undomanager').UndoManager; import './ace' diff --git a/sharejs-ace/package.js b/sharejs-ace/package.js index 582b700..258a028 100644 --- a/sharejs-ace/package.js +++ b/sharejs-ace/package.js @@ -5,11 +5,56 @@ Package.describe({ git: "https://github.com/mizzao/meteor-sharejs.git" }); - Npm.depends({ "ace-builds": "1.2.2" }); +// Ugly-ass function stolen from http://stackoverflow.com/a/20794116/586086 +// TODO make this less ugly in future +function getFilesFromFolder(packageName, folder){ + // local imports + var _ = Npm.require("underscore"); + var fs = Npm.require("fs"); + var path = Npm.require("path"); + // helper function, walks recursively inside nested folders and return absolute filenames + function walk(folder){ + var filenames = []; + // get relative filenames from folder + var folderContent = fs.readdirSync(folder); + // iterate over the folder content to handle nested folders + _.each(folderContent, function(filename) { + // build absolute filename + var absoluteFilename = path.join(folder, filename); + // get file stats + var stat = fs.statSync(absoluteFilename); + if(stat.isDirectory()){ + // directory case => add filenames fetched from recursive call + filenames = filenames.concat(walk(absoluteFilename)); + } + else{ + // file case => simply add it + filenames.push(absoluteFilename); + } + }); + return filenames; + } + // save current working directory (something like "/home/user/projects/my-project") + var cwd = process.cwd(); + + var isRunningFromApp = fs.existsSync(path.resolve("packages")); + var packagePath = isRunningFromApp ? path.resolve("packages", packageName) : ""; + console.log(packagePath); + + packagePath = path.resolve(packagePath); + // chdir to our package directory + process.chdir(path.join(packagePath)); + // launch initial walk + var result = walk(folder); + // restore previous cwd + process.chdir(cwd); + return result; +} + Package.onUse(function (api) { api.versionsFrom("1.3"); @@ -17,7 +62,13 @@ Package.onUse(function (api) { api.use("mizzao:sharejs@0.9.0"); api.imply("mizzao:sharejs"); - + + var _ = Npm.require("underscore"); + + // Add Ace files as assets that can be loaded by the client later + var aceSettings = getFilesFromFolder("mizzao:sharejs-ace", ".npm/package/node_modules/ace-builds/src"); + api.addAssets(aceSettings, 'client'); + api.mainModule('client.js', 'client'); api.addFiles([ 'templates.html' From 6f0751cda3e4a64174241d50f096a092911f7c6a Mon Sep 17 00:00:00 2001 From: David Sichau Date: Wed, 18 May 2016 10:40:14 +0200 Subject: [PATCH 4/6] Removed the authentication system The current authentication system did not add any additional secirity as the Meteor.userId is not a secret token. --- History.md | 3 +- README.md | 74 +----------- settings-example.json | 23 ---- sharejs-ace/client.js | 1 + sharejs-base/package.js | 2 +- sharejs-base/sharejs-client.js | 3 +- sharejs-base/sharejs-meteor-auth.coffee | 153 ------------------------ sharejs-base/sharejs-server.js | 5 - 8 files changed, 6 insertions(+), 258 deletions(-) delete mode 100644 sharejs-base/sharejs-meteor-auth.coffee diff --git a/History.md b/History.md index 3713422..281abcb 100644 --- a/History.md +++ b/History.md @@ -1,10 +1,11 @@ ## vNEXT -## v0.9.0 +## v0.9.0 unreleased * dependencies are imported as npm modules * Code is rewritten in ecmascript * used module system to reduce globals +* removed the authentication system, as it added no real security ## v0.8.0 diff --git a/README.md b/README.md index de19350..7ffa784 100644 --- a/README.md +++ b/README.md @@ -82,77 +82,7 @@ You can also use `db.type` of `none` to have all documents and operations in mem ### Meteor User-Accounts Integration -In case you are using Mongo to mirror the internal sharejs DB with an external Meteor collection as in the user-accounts demo app (find it [here](https://github.com/kbdaitch/meteor-documents-demo) deployed on [meteor](http://documents-users.meteor.com)), both authorization and authentication are available using the [ShareJS auth mechanism](https://github.com/share/ShareJS/wiki/User-access-control). - -The use of this feature becomes effective if you store addional metadata such as owners and invites in the document collection like [here](https://github.com/kbdaitch/meteor-documents-demo/blob/master/client/client.coffee#L22) in the demo app. - -Your settings file should look like the following: - -```js -{ - "sharejs": { - "options": { - "accounts_auth": { - "authorize": { - "collection": "documents", - "token_validations": { - "or": { - "invitedUsers": "is_in_array", - "userId": "is_equal" - } - }, - "apply_on": [ - "read", - "update", - "create", - "delete" - ] - }, - "authenticate": { - "collection": "users", - "token_validations": { - "_id": "is_equal" - } - } - } - } - } -} -``` - -All authorize and authenticate settings are under their respective categories. Please note that both of them are completely optional, however once present, they must have at least a field called `collection`. - -* `sharejs.options.accounts_auth.authorize` -* `sharejs.options.accounts_auth.authenticate` - -The sub-categories for both define the allowed operations and validation. - -* `collection`: database collection to fetch the document/user metadata from. -* `token_validations`: contains the boolean logic and supports the keywords `is_equal`, `isnt_equal`, `is_in_array` and `isnt_in_array`. Both `or` and `and` logical operators can be used, provided at any one level of JSON, there is only one or none of them. -* `apply_on`: you can select operations from [here](https://github.com/share/ShareJS/wiki/User-access-control#actions) `Type` column except `connect` which is reserved for authentication. - -### Validations - -All validations are run against the token. The client-side of sharejs auth would use `Meteor.userId()` for the token. So, for a returned document from database, the following will check for equality of `userId` and token or presence of token in `invitedUsers`. - -```js -"token_validations": { - "or": { - "userId": "is_equal", - "invitedUsers": "is_in_array" - } -} -``` - -### Authentication - -Usually, the presence of the user in the collection is sufficient for allowing the connection to sharejs. However, criteria such as the following can also be used. - -```js -"token_validations": { - "_id": "is_equal" -} -``` +The Authorization was removed in version 0.9.0, because the current implementation did not added any security as `Meteor.userId` is no secret token. ## Advanced @@ -161,9 +91,7 @@ You can access the [ShareJS Server API](https://github.com/share/ShareJS/wiki/Se ## Notes - When using the default mongo driver, you must not use collections called `docs` or `ops`. [These are used by ShareJS](https://github.com/share/ShareJS/blob/v0.6.2/src/server/db/mongo.coffee). -- When not using accounts integration, ShareJS is agnostic to the Meteor users, and doesn't keep track of who did what op. The document ids are used for access. - It's best to create a `Meteor.Collection` for your documents which generates good unique ids to connect to ShareJS with. Use these to render the templates above. See the [demo](demo) for examples. -- Importing ace dependencies is somewhat unsatisfactory. Waiting for improvements to Meteor package management system. Please submit pull requests for better features and cooperation! diff --git a/settings-example.json b/settings-example.json index ad61ed4..ebbcb57 100644 --- a/settings-example.json +++ b/settings-example.json @@ -4,29 +4,6 @@ "db": { "type": "mongo", "opsCollectionPerDoc": false - }, - "accounts_auth": { - "authorize": { - "collection": "documents", - "token_validations": { - "or": { - "invitedUsers": "is_in_array", - "userId": "is_equal" - } - }, - "apply_on": [ - "read", - "update", - "create", - "delete" - ] - }, - "authenticate": { - "collection": "users", - "token_validations": { - "_id": "is_equal" - } - } } } } diff --git a/sharejs-ace/client.js b/sharejs-ace/client.js index 97ae739..b307e5e 100644 --- a/sharejs-ace/client.js +++ b/sharejs-ace/client.js @@ -22,6 +22,7 @@ class ShareJSAceConnector extends ShareJSConnector { super.rendered(element); this.ace = ace.edit(element); this.ace.getSession().setValue("loading..."); + this.ace.$blockScrolling = Infinity; if (typeof this.configCallback === "function") { this.configCallback(this.ace); } diff --git a/sharejs-base/package.js b/sharejs-base/package.js index 1be29d0..25803d9 100644 --- a/sharejs-base/package.js +++ b/sharejs-base/package.js @@ -16,7 +16,7 @@ Package.onUse(function (api) { api.use(['underscore', 'ecmascript', 'modules']); api.use(['handlebars', 'templating'], 'client'); - api.use(['coffeescript', 'mongo-livedata', 'routepolicy', 'webapp'], 'server'); + api.use(['mongo-livedata', 'routepolicy', 'webapp'], 'server'); api.mainModule('sharejs-client.js', 'client'); diff --git a/sharejs-base/sharejs-client.js b/sharejs-base/sharejs-client.js index bb03019..d843d50 100644 --- a/sharejs-base/sharejs-client.js +++ b/sharejs-base/sharejs-client.js @@ -13,8 +13,7 @@ export class ShareJSConnector { getOptions() { return { - origin: '//' + window.location.host + '/channel', - authentication: (typeof Meteor.userId === "function" ? Meteor.userId() : void 0) || null + origin: '//' + window.location.host + '/channel' }; }; diff --git a/sharejs-base/sharejs-meteor-auth.coffee b/sharejs-base/sharejs-meteor-auth.coffee deleted file mode 100644 index 2e7d2d3..0000000 --- a/sharejs-base/sharejs-meteor-auth.coffee +++ /dev/null @@ -1,153 +0,0 @@ -# Auth API helpers -Fiber = Npm.require('fibers'); -Future = Npm.require('fibers/future') - -# Metaprogramming aids -LogicalOps = - 'or' : (a, b) -> a or b - 'and' : (a, b) -> a and b - -# Recursively evaluate the validations provided in settings.json -runValidations = (currentOp, validations, doc, token) -> - if currentOp is null - if validations?.or? - # recurse into or - return runValidations("or", validations.or, doc, token) - else if validations?.and? - # recurse into and - return runValidations("and", validations.and, doc, token) - else if validations? - # If no higher level "or" and "and", default to "and" - return runValidations("and", validations, doc, token) - else - # No validations being asked to run - user possibly just wants to check - # for presence of user/doc - return true - else if currentOp? - if currentOp is "or" - result = false - else if currentOp = "and" - result = true - - for k, v of validations - if k in ["or", "and"] - # recurse into or/and - nestedResult = runValidations(k, v, doc, token) - result = LogicalOps[currentOp](result, nestedResult) - else - switch v - when "is_in_array" - result = LogicalOps[currentOp](result, token in doc[k]) - when "isnt_in_array" - lookIn = doc.k or [] - result = LogicalOps[currentOp](result, token not in doc[k]) - when "is_equal" - result = LogicalOps[currentOp](result, token is doc[k]) - when "isnt_equal" - result = LogicalOps[currentOp](result, token is not doc[k]) - return result - -_submitOpMonkeyPatched = false - -_monkeyPatch = (agent) -> - UserAgent = Object.getPrototypeOf(agent) - model = ShareJS.model - # Overriding https://github.com/share/ShareJS/blob/v0.6.2/src/server/useragent.coffee, - # including variables in closure. >.< @josephg - UserAgent.submitOp = (docName, opData, callback) -> - opData.meta ||= {} - opData.meta.userId = @name - opData.meta.source = @sessionId - dupIfSource = opData.dupIfSource or [] - - # If ops and meta get coalesced, they should be separated here. - if opData.op - @doAuth {docName, op:opData.op, v:opData.v, meta:opData.meta, dupIfSource}, 'submit op', callback, => - model.applyOp docName, opData, callback - else - @doAuth {docName, meta:opData.meta}, 'submit meta', callback, => - model.applyMetaOp docName, opData, callback - - console.log "ShareJS: patched UserAgent submitOp function to record Meteor userId" - _submitOpMonkeyPatched = true - -# Based on https://github.com/share/ShareJS/wiki/User-access-control -class @MeteorAccountsAuthHandler - constructor: (@options, @client) -> - - # Get a future that resolves to the entry from the database for given query - fetchDocument: (collection, key) -> - future = new Future - @client.collection collection, (err, collection) -> - return future.throw(err) if err - - collection.findOne {_id: key}, (err, doc) -> - console.warn "failed to get doc in #{collection} with key #{key}: #{err}" if err - future.throw(null) if err - future.return(doc) - - return future - - # Get a future that would resolve to authentication as a bool - getAuthentication: (agent) -> - token = agent.authentication - validations = @options.authenticate.token_validations - collection = @options.authenticate.collection - - future = new Future - - user = @fetchDocument(collection, agent.authentication).wait() - # Not having user necessitates bailing out! - # Having both "and" and "or" on the top level is not allowed - unless user? or (validations.or? and validations.and?) - future.return false - - future.return runValidations(null, validations, user, token) - - return future - - # Get a future that would resolve to authorization as a bool - getAuthorization: (agent, action) -> - token = agent.authentication - validations = @options.authorize.token_validations - collection = @options.authorize.collection - - future = new Future - - doc = @fetchDocument(collection, action.docName).wait() - # Not having document necessitates bailing out! - # Having both "and" and "or" on the top level is not allowed - unless doc? or (validations.or? and validations.and?) - future.return false - - future.return runValidations(null, validations, doc, token) - - return future - - handle: (agent, action) => - # This is ugly, but we have no other way to store Meteor usernames in ShareJS 0.6.2 - _monkeyPatch(agent) unless _submitOpMonkeyPatched - - authenticate = @options.authenticate? - authorize = @options.authorize? - opsToAuthorize = @options.authorize?.apply_on - - (Fiber (=> - res = false - - if authenticate and (action.type is "connect") - res = @getAuthentication(agent).wait() - # Save Meteor userId if we successfully authenticated - agent.name = agent.authentication if res - - else if authorize and action.type in opsToAuthorize - res = @getAuthorization(agent, action).wait() - else - # Accept all other actions - res = true - - if res - action.accept() - else - action.reject() - )).run() diff --git a/sharejs-base/sharejs-server.js b/sharejs-base/sharejs-server.js index e624d70..5887d9e 100644 --- a/sharejs-base/sharejs-server.js +++ b/sharejs-base/sharejs-server.js @@ -1,8 +1,6 @@ // Creates a (persistent) ShareJS server // Based on https://github.com/share/ShareJS/wiki/Getting-started -import "./sharejs-meteor-auth.coffee" - import { Meteor } from 'meteor/meteor'; const Future = Npm.require('fibers/future'); @@ -39,9 +37,6 @@ switch (options.db.type) { http://mongodb.github.io/node-mongodb-native/api-generated/mongoclient.html */ options.db.client.open = function() {}; - if (options.accounts_auth != null) { - options.auth = new MeteorAccountsAuthHandler(options.accounts_auth, options.db.client).handle; - } break; default: Meteor._debug("ShareJS: using unsupported db type " + options.db.type + ", falling back to in-memory."); From 9b3c9fefb6cc504a08b0af9ad94af564fdd72683 Mon Sep 17 00:00:00 2001 From: David Sichau Date: Wed, 6 Jul 2016 13:28:58 +0200 Subject: [PATCH 5/6] Imported all Codemirror files as static assets Closes #73 --- README.md | 2 +- demo/.meteor/versions | 1 - sharejs-ace/ace.js | 2 +- sharejs-ace/client.js | 4 +-- sharejs-ace/package.js | 2 +- sharejs-codemirror/package.js | 51 +++++++++++++++++++++++++++++++++++ 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7ffa784..b824ad8 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ You can also use `db.type` of `none` to have all documents and operations in mem ### Meteor User-Accounts Integration -The Authorization was removed in version 0.9.0, because the current implementation did not added any security as `Meteor.userId` is no secret token. +The Authorization was removed in version 0.9.0, because the current implementation did not added any security as `Meteor.userId` is not a secret token. ## Advanced diff --git a/demo/.meteor/versions b/demo/.meteor/versions index f514f3b..1c2974a 100644 --- a/demo/.meteor/versions +++ b/demo/.meteor/versions @@ -12,7 +12,6 @@ caching-compiler@1.0.4 caching-html-compiler@1.0.6 callback-hook@1.0.8 check@1.2.1 -coffeescript@1.0.17 ddp@1.2.5 ddp-client@1.2.7 ddp-common@1.2.5 diff --git a/sharejs-ace/ace.js b/sharejs-ace/ace.js index 4ec7f99..3afc0fd 100644 --- a/sharejs-ace/ace.js +++ b/sharejs-ace/ace.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.6.2 -require('ace-builds/src/ace'); +require('ace-builds/src-noconflict/ace'); (function() { var Range, applyToShareJS, requireImpl; diff --git a/sharejs-ace/client.js b/sharejs-ace/client.js index b307e5e..428f499 100644 --- a/sharejs-ace/client.js +++ b/sharejs-ace/client.js @@ -6,8 +6,8 @@ import { Template } from 'meteor/templating' import { Blaze } from 'meteor/blaze' import { ShareJSConnector } from 'meteor/mizzao:sharejs' -require('ace-builds/src/ace'); -ace.require('ace/config').set('basePath', '/packages/mizzao_sharejs-ace/.npm/package/node_modules/ace-builds/src/'); +require('ace-builds/src-noconflict/ace'); +ace.require('ace/config').set('basePath', '/packages/mizzao_sharejs-ace/.npm/package/node_modules/ace-builds/src-noconflict/'); UndoManager = ace.require('ace/undomanager').UndoManager; import './ace' diff --git a/sharejs-ace/package.js b/sharejs-ace/package.js index 258a028..535db5e 100644 --- a/sharejs-ace/package.js +++ b/sharejs-ace/package.js @@ -66,7 +66,7 @@ Package.onUse(function (api) { var _ = Npm.require("underscore"); // Add Ace files as assets that can be loaded by the client later - var aceSettings = getFilesFromFolder("mizzao:sharejs-ace", ".npm/package/node_modules/ace-builds/src"); + var aceSettings = getFilesFromFolder("mizzao:sharejs-ace", ".npm/package/node_modules/ace-builds/src-noconflict"); api.addAssets(aceSettings, 'client'); api.mainModule('client.js', 'client'); diff --git a/sharejs-codemirror/package.js b/sharejs-codemirror/package.js index f622930..4638373 100644 --- a/sharejs-codemirror/package.js +++ b/sharejs-codemirror/package.js @@ -9,6 +9,53 @@ Npm.depends({ codemirror: "5.14.2" }); + +// Ugly-ass function stolen from http://stackoverflow.com/a/20794116/586086 +// TODO make this less ugly in future +function getFilesFromFolder(packageName, folder){ + // local imports + var _ = Npm.require("underscore"); + var fs = Npm.require("fs"); + var path = Npm.require("path"); + // helper function, walks recursively inside nested folders and return absolute filenames + function walk(folder){ + var filenames = []; + // get relative filenames from folder + var folderContent = fs.readdirSync(folder); + // iterate over the folder content to handle nested folders + _.each(folderContent, function(filename) { + // build absolute filename + var absoluteFilename = path.join(folder, filename); + // get file stats + var stat = fs.statSync(absoluteFilename); + if(stat.isDirectory()){ + // directory case => add filenames fetched from recursive call + filenames = filenames.concat(walk(absoluteFilename)); + } + else{ + // file case => simply add it + filenames.push(absoluteFilename); + } + }); + return filenames; + } + // save current working directory (something like "/home/user/projects/my-project") + var cwd = process.cwd(); + + var isRunningFromApp = fs.existsSync(path.resolve("packages")); + var packagePath = isRunningFromApp ? path.resolve("packages", packageName) : ""; + console.log(packagePath); + + packagePath = path.resolve(packagePath); + // chdir to our package directory + process.chdir(path.join(packagePath)); + // launch initial walk + var result = walk(folder); + // restore previous cwd + process.chdir(cwd); + return result; +} + Package.onUse(function (api) { api.versionsFrom("1.3.2"); @@ -17,6 +64,10 @@ Package.onUse(function (api) { api.use("mizzao:sharejs@0.9.0"); api.imply("mizzao:sharejs"); + // Add Ace files as assets that can be loaded by the client later + var codemirrorData = getFilesFromFolder("mizzao:sharejs-codemirror", ".npm/package/node_modules/codemirror"); + api.addAssets(codemirrorData, 'client'); + api.mainModule('client.js', 'client'); api.addFiles([ 'templates.html' From b4bc6b5cfe6b629edd18825aa6f602eb2f187db3 Mon Sep 17 00:00:00 2001 From: David Sichau Date: Wed, 6 Jul 2016 13:36:37 +0200 Subject: [PATCH 6/6] fixed test import --- sharejs-base/tests/server_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharejs-base/tests/server_test.js b/sharejs-base/tests/server_test.js index ae660b5..cffedb8 100644 --- a/sharejs-base/tests/server_test.js +++ b/sharejs-base/tests/server_test.js @@ -2,7 +2,7 @@ * Created by dsichau on 17.05.16. */ -import {ShareJS} from 'meteor/sharejs'; +import {ShareJS} from 'meteor/mizzao:sharejs'; var Docs, Ops, sleep; Docs = new Meteor.Collection("docs");