├── .gitattributes ├── app ├── .buildignore ├── robots.txt ├── favicon.ico ├── images │ └── yeoman.png ├── scripts │ ├── controllers │ │ ├── auth.js │ │ └── main.js │ ├── app.js │ └── dropbox-client-example.js ├── views │ └── main.html ├── index.html ├── 404.html ├── styles │ └── main.scss └── .htaccess ├── .jshintignore ├── .bowerrc ├── .gitignore ├── .travis.yml ├── test ├── runner.html ├── spec │ └── controllers │ │ └── main.js └── .jshintrc ├── .jshintrc ├── .editorconfig ├── bower.json ├── karma-e2e.conf.js ├── package.json ├── karma.conf.js ├── README.md └── Gruntfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | app/scripts/dropbox-client-example.js -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | app/bower_components 6 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thrashr888/dropbox-taskpaper-editor/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thrashr888/dropbox-taskpaper-editor/HEAD/app/images/yeoman.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.8' 4 | - '0.10' 5 | before_script: 6 | - 'npm install -g bower grunt-cli' 7 | - 'bower install' 8 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /test/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('dropboxTaskpaperApp')); 7 | 8 | var MainCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | MainCtrl = $controller('MainCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of awesomeThings to the scope', function () { 20 | expect(scope.awesomeThings.length).toBe(3); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropbox-taskpaper", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "angular": "1.2.6", 6 | "json3": "~3.2.6", 7 | "es5-shim": "~2.1.0", 8 | "jquery": "~1.10.2", 9 | "sass-bootstrap": "~3.0.2", 10 | "angular-animate": "1.2.6", 11 | "angular-resource": "1.2.6", 12 | "angular-cookies": "1.2.6", 13 | "angular-sanitize": "1.2.6", 14 | "angular-route": "1.2.6", 15 | "ngDropbox": "christiansmith/ngDropbox", 16 | "jsTaskPaper": "https://github.com/dhilowitz/jsTaskPaper.git" 17 | }, 18 | "devDependencies": { 19 | "angular-mocks": "1.2.6", 20 | "angular-scenario": "1.2.6" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/scripts/controllers/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('dropboxTaskpaperApp') 4 | .controller('AuthCtrl', function (Dropbox, DropboxLocalStorageOAuthKey, $rootScope, $location) { 5 | // $timeout(function() { 6 | // dropbox.authenticate(); 7 | // }, 200); 8 | 9 | if($rootScope.checkAuthenticated() && Dropbox.isAuthenticated()) { 10 | $location.path('/').replace(); 11 | return; 12 | } 13 | 14 | Dropbox.authenticate().then(function (oauth) { 15 | if(oauth.uid){ 16 | localStorage[DropboxLocalStorageOAuthKey] = angular.toJson(oauth); 17 | $rootScope.uid = oauth.uid; 18 | $rootScope.isAuthenticated = true; 19 | $location.path('/').replace(); 20 | } 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['ng-scenario'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'test/e2e/**/*.js' 15 | ], 16 | 17 | // list of files / patterns to exclude 18 | exclude: [], 19 | 20 | // web server port 21 | port: 8080, 22 | 23 | // level of logging 24 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 25 | logLevel: config.LOG_INFO, 26 | 27 | 28 | // enable / disable watching file and executing tests whenever any file changes 29 | autoWatch: false, 30 | 31 | 32 | // Start these browsers, currently available: 33 | // - Chrome 34 | // - ChromeCanary 35 | // - Firefox 36 | // - Opera 37 | // - Safari (only Mac) 38 | // - PhantomJS 39 | // - IE (only Windows) 40 | browsers: ['Chrome'], 41 | 42 | 43 | // Continuous Integration mode 44 | // if true, it capture browsers, run tests and exit 45 | singleRun: false 46 | 47 | // Uncomment the following lines if you are using grunt's server to run the tests 48 | // proxies: { 49 | // '/': 'http://localhost:9000/' 50 | // }, 51 | // URL root prevent conflicts with the site root 52 | // urlRoot: '_karma_' 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropboxtaskpaper", 3 | "version": "0.0.0", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "grunt": "~0.4.1", 7 | "grunt-autoprefixer": "~0.4.0", 8 | "grunt-bower-install": "~0.7.0", 9 | "grunt-concurrent": "~0.4.1", 10 | "grunt-contrib-clean": "~0.5.0", 11 | "grunt-contrib-coffee": "~0.7.0", 12 | "grunt-contrib-compass": "~0.6.0", 13 | "grunt-contrib-concat": "~0.3.0", 14 | "grunt-contrib-connect": "~0.5.0", 15 | "grunt-contrib-copy": "~0.4.1", 16 | "grunt-contrib-cssmin": "~0.7.0", 17 | "grunt-contrib-htmlmin": "~0.1.3", 18 | "grunt-contrib-imagemin": "~0.3.0", 19 | "grunt-contrib-jshint": "~0.7.1", 20 | "grunt-contrib-uglify": "~0.2.0", 21 | "grunt-contrib-watch": "~0.5.2", 22 | "grunt-google-cdn": "~0.2.0", 23 | "grunt-newer": "~0.5.4", 24 | "grunt-ngmin": "~0.0.2", 25 | "grunt-rev": "~0.1.0", 26 | "grunt-svgmin": "~0.2.0", 27 | "grunt-usemin": "~2.0.0", 28 | "jshint-stylish": "~0.1.3", 29 | "load-grunt-tasks": "~0.2.0", 30 | "time-grunt": "~0.2.1", 31 | "karma-ng-scenario": "~0.1.0", 32 | "grunt-karma": "~0.6.2", 33 | "karma-script-launcher": "~0.1.0", 34 | "karma-chrome-launcher": "~0.1.2", 35 | "karma-firefox-launcher": "~0.1.3", 36 | "karma-html2js-preprocessor": "~0.1.0", 37 | "karma-jasmine": "~0.1.5", 38 | "requirejs": "~2.1.9", 39 | "karma-requirejs": "~0.2.1", 40 | "karma-coffee-preprocessor": "~0.1.2", 41 | "karma-phantomjs-launcher": "~0.1.1", 42 | "karma": "~0.10.9", 43 | "karma-ng-html2js-preprocessor": "~0.1.0" 44 | }, 45 | "engines": { 46 | "node": ">=0.8.0" 47 | }, 48 | "scripts": { 49 | "test": "grunt test" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'app/bower_components/angular/angular.js', 15 | 'app/bower_components/angular-mocks/angular-mocks.js', 16 | 'app/bower_components/angular-resource/angular-resource.js', 17 | 'app/bower_components/angular-cookies/angular-cookies.js', 18 | 'app/bower_components/angular-sanitize/angular-sanitize.js', 19 | 'app/bower_components/angular-route/angular-route.js', 20 | 'app/scripts/*.js', 21 | 'app/scripts/**/*.js', 22 | 'test/mock/**/*.js', 23 | 'test/spec/**/*.js' 24 | ], 25 | 26 | // list of files / patterns to exclude 27 | exclude: [], 28 | 29 | // web server port 30 | port: 8080, 31 | 32 | // level of logging 33 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 34 | logLevel: config.LOG_INFO, 35 | 36 | 37 | // enable / disable watching file and executing tests whenever any file changes 38 | autoWatch: false, 39 | 40 | 41 | // Start these browsers, currently available: 42 | // - Chrome 43 | // - ChromeCanary 44 | // - Firefox 45 | // - Opera 46 | // - Safari (only Mac) 47 | // - PhantomJS 48 | // - IE (only Windows) 49 | browsers: ['Chrome'], 50 | 51 | 52 | // Continuous Integration mode 53 | // if true, it capture browsers, run tests and exit 54 | singleRun: false 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /app/views/main.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |

Dropbox Taskpaper Editor

6 |
7 | 8 |
9 |

Dropbox Taskpaper Editor

10 |

11 | The Web-based Dropbox-synced Taskpaper Document Editor 12 |

13 | Log In 14 |
15 | 16 |
17 |
{{message}}
18 | 19 |
20 |

Taskpaper Documents

21 |
    22 |
  1. Loading list of documents...
  2. 23 |
  3. {{file}}
  4. 24 |
25 |
26 | 27 |
28 |

{{currentFile}} ⚠ Conflict Loading document...

29 |
30 | 31 |
32 |

Editor

33 | 34 |
35 | 36 |
37 |

Preview

38 |
39 |
40 | 41 |
42 | 43 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dropbox Taskpaper Editor 2 | ======================== 3 | 4 | "It's certainly an impressive piece of work." — [Macdrifter](http://macdrifter.com/2014/02/the-taskpaper-rd-notebook.html) 5 | 6 | This app allows you to edit your Taskpaper documents stored in Dropbox directly from the browser. It requires "Full Dropbox" permissions in order to edit the Taskpaper files. 7 | 8 | This is a client-side app using AngularJS and Dropbox APIs. It does not require a server to run. You should be able to deploy directly to Dropbox, S3, Google Drive or other static file hosts. If so, you'll need to setup your own Dropbox App [https://www.dropbox.com/developers/apps] and add the ngDropbox callback url to the whitelist. For example: `http://localhost:9000/bower_components/ngDropbox/callback.html`. Then add your appKey in `app/scripts/app.js`. Outside of localhost, the Dropbox authentication only works with https. 9 | 10 | Demo site deployed on S3 and CloudFront: [Dropbox Taskpaper Editor](https://dgiu4ye9xtr28.cloudfront.net/) 11 | 12 | Install & Run 13 | ------------- 14 | 15 | > npm install -g grunt-cli bower 16 | > npm install 17 | > bower install 18 | > grunt serve 19 | 20 | Thanks 21 | ------ 22 | 23 | This app uses: 24 | 25 | - ngDropbox 26 | - JSTaskPaper 27 | - AngularJS 28 | - Yeoman 29 | - Grunt 30 | - Bootstrap 31 | - jQuery 32 | 33 | License 34 | ------- 35 | 36 | Copyright (C) 2013-2014 Paul Thrasher 37 | 38 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 39 | 40 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 43 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('dropboxTaskpaperApp', [ 4 | 'ngCookies', 5 | 'ngResource', 6 | 'ngSanitize', 7 | 'ngRoute', 8 | 'ngAnimate', 9 | 'dropbox' 10 | ]) 11 | .config(function ($routeProvider, $locationProvider) { 12 | $locationProvider.html5Mode(true); 13 | $routeProvider 14 | .when('/', { 15 | templateUrl: 'views/main.html', 16 | controller: 'MainCtrl' 17 | }) 18 | .when('/auth', { 19 | templateUrl: 'views/main.html', 20 | controller: 'AuthCtrl' 21 | }) 22 | .otherwise({ 23 | redirectTo: '/' 24 | }); 25 | }) 26 | .value('DropboxClientId', '563mc3wfk1qd68q') 27 | .value('DropboxRedirectUri', 'https://' + window.location.host + '/bower_components/ngDropbox/callback.html') 28 | .value('DropboxLocalStorageOAuthKey', 'ngDropbox.oauth') 29 | .config(function (DropboxProvider) { 30 | DropboxProvider.config('563mc3wfk1qd68q', 31 | 'https://' + window.location.host + '/bower_components/ngDropbox/callback.html'); 32 | }) 33 | // .constant('DROPBOX_APP_KEY', '563mc3wfk1qd68q') 34 | // .service('dropbox', function (DROPBOX_APP_KEY, $rootScope) { 35 | // // Exposed for easy access in the browser console. 36 | // var dropbox = new Dropbox.Client({ 37 | // key: DROPBOX_APP_KEY 38 | // }); 39 | 40 | // dropbox.authenticate({ 41 | // interactive: false 42 | // }, function(error) { 43 | // if (error) { 44 | // console.log('Authentication error', error); 45 | // } else { 46 | // $rootScope.authenticated = true; 47 | // } 48 | // }); 49 | 50 | // dropbox.getAccountInfo({}, function(info){ 51 | // console.log(info); 52 | // }) 53 | 54 | // if (dropbox.isAuthenticated()) { 55 | // $rootScope.authenticated = true; 56 | // } 57 | // return dropbox; 58 | // }) 59 | // .service('datastore', function (dropbox, $q, $rootScope) { 60 | // var promise = $q.defer(); 61 | 62 | // // console.log(dropbox.getDatastoreManager()); 63 | // if (dropbox.isAuthenticated()) { 64 | // // dropbox is authenticated. Display UI. 65 | // console.log(dropbox.getDatastoreManager()); 66 | // dropbox.getDatastoreManager().openDefaultDatastore(function(error, datastore) { 67 | // if (error) { 68 | // console.error('Error opening default datastore', error); 69 | // } 70 | 71 | // promise.resolve(datastore); 72 | // }); 73 | // } else { 74 | // promise.reject(); 75 | // } 76 | // return promise.promise; 77 | // }) 78 | .run(function ($rootScope, Dropbox, DropboxLocalStorageOAuthKey) { 79 | $rootScope.isAuthenticated = false; 80 | 81 | $rootScope.checkAuthenticated = function () { 82 | // console.log(localStorage); 83 | if (localStorage[DropboxLocalStorageOAuthKey]) { 84 | var oauth = angular.fromJson(localStorage[DropboxLocalStorageOAuthKey]); 85 | if(oauth) { 86 | 87 | } 88 | Dropbox.setCredentials(oauth); 89 | $rootScope.uid = oauth.uid; 90 | $rootScope.isAuthenticated = true; 91 | return true; 92 | } else { 93 | return false; 94 | } 95 | }; 96 | 97 | $rootScope.checkAuthenticated(); 98 | 99 | }) 100 | ; 101 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Dropbox Taskpaper Editor 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 |
30 | 31 | 32 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/scripts/dropbox-client-example.js: -------------------------------------------------------------------------------- 1 | // Insert your Dropbox app key here: 2 | var DROPBOX_APP_KEY = '563mc3wfk1qd68q'; 3 | 4 | // Exposed for easy access in the browser console. 5 | var client = new Dropbox.Client({ 6 | key: DROPBOX_APP_KEY 7 | }); 8 | var taskTable; 9 | 10 | $(function() { 11 | // Insert a new task record into the table. 12 | function insertTask(text) { 13 | taskTable.insert({ 14 | taskname: text, 15 | created: new Date(), 16 | completed: false 17 | }); 18 | } 19 | 20 | // updateList will be called every time the table changes. 21 | function updateList() { 22 | $('#tasks').empty(); 23 | 24 | var records = taskTable.query(); 25 | 26 | // Sort by creation time. 27 | records.sort(function(taskA, taskB) { 28 | if (taskA.get('created') < taskB.get('created')) return -1; 29 | if (taskA.get('created') > taskB.get('created')) return 1; 30 | return 0; 31 | }); 32 | 33 | // Add an item to the list for each task. 34 | for (var i = 0; i < records.length; i++) { 35 | var record = records[i]; 36 | $('#tasks').append( 37 | renderTask(record.getId(), record.get('completed'), record.get('taskname'))); 38 | } 39 | 40 | addListeners(); 41 | $('#newTask').focus(); 42 | } 43 | 44 | // The login button will start the authentication process. 45 | $('#loginButton').click(function(e) { 46 | e.preventDefault(); 47 | // This will redirect the browser to OAuth login. 48 | client.authenticate(); 49 | }); 50 | 51 | // Try to finish OAuth authorization. 52 | client.authenticate({ 53 | interactive: false 54 | }, function(error) { 55 | if (error) { 56 | alert('Authentication error: ' + error); 57 | } 58 | }); 59 | 60 | if (client.isAuthenticated()) { 61 | // Client is authenticated. Display UI. 62 | $('#loginButton').hide(); 63 | $('#main').show(); 64 | 65 | client.getDatastoreManager().openDefaultDatastore(function(error, datastore) { 66 | if (error) { 67 | alert('Error opening default datastore: ' + error); 68 | } 69 | 70 | taskTable = datastore.getTable('tasks'); 71 | 72 | // Populate the initial task list. 73 | updateList(); 74 | 75 | // Ensure that future changes update the list. 76 | datastore.recordsChanged.addListener(updateList); 77 | }); 78 | } 79 | 80 | // Set the completed status of a task with the given ID. 81 | function setCompleted(id, completed) { 82 | taskTable.get(id).set('completed', completed); 83 | } 84 | 85 | // Delete the record with a given ID. 86 | function deleteRecord(id) { 87 | taskTable.get(id).deleteRecord(); 88 | } 89 | 90 | // Render the HTML for a single task. 91 | function renderTask(id, completed, text) { 92 | return $('
  • ').attr('id', id).append( 93 | $('