diff --git a/appinfo/info.xml b/appinfo/info.xml index 64a87a21a..9ead729f8 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,6 +22,6 @@ The Notes app is a distraction free notes taking app. It provides categories for https://github.com/nextcloud/notes.git https://raw.githubusercontent.com/nextcloud/screenshots/master/apps/Notes/notes.png - + diff --git a/css/notes.scss b/css/notes.scss index cd43fcde6..4c2798dc4 100644 --- a/css/notes.scss +++ b/css/notes.scss @@ -290,6 +290,37 @@ form.category .icon-confirm { font-family: MONOSPACE; } +/* Checkboxes */ +.CodeMirror .CodeMirror-code .cm-formatting-task { + position: relative; + display: inline-block; + width: 2em; +} + +.CodeMirror .CodeMirror-code .cm-formatting-task.cm-meta::before { + content: "\2610"; + font-size: 1.7em; + background: white; + position: absolute; +} + +.CodeMirror .CodeMirror-code .cm-formatting-task.cm-property::before { + content: "\2611"; + font-size: 1.7em; + background: white; + position: absolute; +} + +.CodeMirror .CodeMirror-code .CodeMirror-line.completed-task { + color: #999; + text-decoration: line-through; +} + +/* Recommended tab size increase for readability */ +.CodeMirror .CodeMirror-code .cm-tab { + width: 2em; +} + /* distraction free styles */ :-webkit-full-screen, :-moz-full-screen, diff --git a/js/app/controllers/notecontroller.js b/js/app/controllers/notecontroller.js index 1cdcf0b06..7eb0f11e5 100644 --- a/js/app/controllers/notecontroller.js +++ b/js/app/controllers/notecontroller.js @@ -40,6 +40,22 @@ app.controller('NoteController', function($routeParams, $scope, NotesModel, t('notes', 'New note'); }; + $scope.toggleCheckbox = function (el) { + var $el = $(el); + var cm = $('.CodeMirror')[0].CodeMirror; + var doc = cm.getDoc(); + var index = $el.parents('.CodeMirror-line').index(); + var line = doc.getLineHandle(index); + + var newvalue = ( $el.text() === '[x]' ) ? '[ ]' : '[x]'; + + // + 1 for some reason... not sure why + doc.replaceRange(newvalue, + {line: index, ch: line.text.indexOf('[')}, + {line: index, ch: line.text.indexOf(']') + 1} + ); + }; + $scope.onEdit = function() { var note = $scope.note; note.unsaved = true; diff --git a/js/app/directives/editor.js b/js/app/directives/editor.js index a487980e5..cdc2777d3 100644 --- a/js/app/directives/editor.js +++ b/js/app/directives/editor.js @@ -20,6 +20,19 @@ app.directive('editor', ['$timeout', simplemde.value(scope.note.content); simplemde.codemirror.focus(); + /* Initialize Checkboxes */ + $('.CodeMirror-code').on('mousedown.checkbox touchstart.checkbox', '.cm-formatting-task', function (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + scope.toggleCheckbox(e.target); + }); + + simplemde.codemirror.on('update', function () { + // For strikethrough styling of completed tasks + $('.CodeMirror-line').removeClass('completed-task'); + $('.CodeMirror-line:contains("[x]")').addClass('completed-task'); + }); + simplemde.codemirror.on('change', function() { $timeout(function() { scope.$apply(function () { diff --git a/js/public/app.min.js b/js/public/app.min.js index 06f2663fb..4b74acc61 100644 --- a/js/public/app.min.js +++ b/js/public/app.min.js @@ -1,2 +1,2 @@ -!function(e,n,o,r,i){"use strict";var a=e.module("Notes",["restangular","ngRoute"]).config(["$provide","$routeProvider","RestangularProvider","$httpProvider","$windowProvider",function(e,t,n,r,i){r.defaults.headers.common.requesttoken=o,e.value("Constants",{saveInterval:5e3}),t.when("/notes/:noteId",{templateUrl:"note.html",controller:"NoteController",resolve:{note:["$route","$q","is","Restangular",function(e,t,n,o){var r=t.defer(),i=e.current.params.noteId;return n.loading=!0,o.one("notes",i).get().then(function(e){n.loading=!1,r.resolve(e)},function(){n.loading=!1,r.reject()}),r.promise}]}}).otherwise({redirectTo:"/"});var a=OC.generateUrl("/apps/notes");n.setBaseUrl(a)}]).run(["$rootScope","$location","NotesModel",function(e,t,o){n('link[rel="shortcut icon"]').attr("href",OC.filePath("notes","img","favicon.png")),e.$on("$routeChangeError",function(){var e=o.getAll();if(e.length>0){var n=e.sort(function(e,t){return e.modified>t.modified?1:e.modified\?]/g,""),e=e.replace(/^[\. ]+/gm,""),o.note.title=e.trim().split(/\r?\n/,2)[0]||t("notes","New note")},o.onEdit=function(){var e=o.note;e.unsaved=!0,o.autoSave(e)},o.autoSave=c(function(e){i.add(e)},1e3),o.manualSave=function(){var e=o.note;e.error=!1,i.addManual(e)},o.editCategory=!1,o.showEditCategory=function(){n("#category").val(o.note.category),o.editCategory=!0,n("#category").autocomplete({source:r.getCategories(r.getAll(),0,!1),minLength:0,position:{my:"left bottom",at:"left top",of:"#category"},open:function(){l(function(){var e=n("form.category").innerWidth()-2;n(".ui-autocomplete.ui-menu").width(e)})}}).autocomplete("widget").addClass("category-autocomplete"),n("form.category .icon-confirm").insertAfter("#category"),l(function(){n("#category").focus(),n("#category").autocomplete("search","")})},o.saveCategory=function(){var e=n("#category").val();return o.note.category===e?void(o.editCategory=!1):(o.isCategorySaving=!0,void o.note.customPUT({category:e},"category",{},{}).then(function(n){o.note.category=n.category,e!==n.category&&OC.Notification.showTemporary(t("notes","Updating the note's category has failed. Is the target directory writable?"))},function(){OC.Notification.showTemporary(t("notes","Updating the note's category has failed."))})["finally"](function(){o.isCategorySaving=!1,o.editCategory=!1}))},u.unbind("keypress.notes.save"),u.bind("keypress.notes.save",function(e){if(e.ctrlKey||e.metaKey)switch(String.fromCharCode(e.which).toLowerCase()){case"s":e.preventDefault(),o.manualSave()}}),o.toggleDistractionFree=function(){function e(e){e.requestFullscreen?e.requestFullscreen():e.mozRequestFullScreen?e.mozRequestFullScreen():e.webkitRequestFullscreen?e.webkitRequestFullscreen():e.msRequestFullscreen&&e.msRequestFullscreen()}function t(){document.exitFullscreen?document.exitFullscreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.webkitExitFullscreen&&document.webkitExitFullscreen()}document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement?t():e(document.getElementById("app-content"))},o.$watch(function(){return o.note.title},function(e){e?document.title=e+" - "+o.defaultTitle:document.title=o.defaultTitle})}]),a.controller("NotesController",["$routeParams","$scope","$location","Restangular","NotesModel","$window",function(e,o,r,i,a,c){o.route=e,o.notesLoaded=!1,o.notes=a.getAll(),o.folderSelectorOpen=!1,o.filterCategory=null,o.orderRecent=["-favorite","-modified"],o.orderAlpha=["category","-favorite","title"],o.filterOrder=o.orderRecent;var u=i.all("notes");u.getList().then(function(e){a.addAll(e),o.notesLoaded=!0}),o.create=function(){u.post({category:o.filterCategory}).then(function(e){a.add(e),r.path("/notes/"+e.id)})},o["delete"]=function(e){var t=a.get(e);t.remove().then(function(){a.remove(e),o.$emit("$routeChangeError")})},o.toggleFavorite=function(e,t){var n=a.get(e);n.customPUT({favorite:!n.favorite},"favorite",{},{}).then(function(e){n.favorite=!!e}),t.target.blur()},o.categories=[],o.$watch("notes",function(e){o.categories=a.getCategories(e,1,!0)},!0),o.toggleFolderSelector=function(){o.folderSelectorOpen=!o.folderSelectorOpen},o.setFilter=function(e){null===e?o.filterOrder=o.orderRecent:o.filterOrder=o.orderAlpha,o.filterCategory=e,o.folderSelectorOpen=!1,n("#app-navigation > ul").animate({scrollTop:0},"fast")},o.categoryFilter=function(e){if(null!==o.filterCategory){if(e.category===o.filterCategory)return!0;if(null!==e.category)return e.category.startsWith(o.filterCategory+"/")}return!0},o.isCategory=function(e){return"string"==typeof e},c.onbeforeunload=function(){for(var e=a.getAll(),n=0;n0){var c=this.nthIndexOf(a,"/",t);c>0&&(a=a.substring(0,c))}o[a]===i?o[a]=1:o[a]+=1}var u=[];for(var l in o)n?u.push({name:l,count:o[l]}):l&&u.push(l);return n||u.sort(),u}},new t}),a.factory("SaveQueue",["$q",function(e){var t=function(){this._queue={},this._flushLock=!1,this._manualSaveActive=!1};return t.prototype={add:function(e){this._queue[e.id]=e,this._flush()},addManual:function(e){this._manualSaveActive=!0,this.add(e)},_flush:function(){var t=Object.keys(this._queue);if(0!==t.length&&!this._flushLock){this._flushLock=!0;for(var n=this,o=[],r=0;r0){var n=e.sort(function(e,t){return e.modified>t.modified?1:e.modified\?]/g,""),e=e.replace(/^[\. ]+/gm,""),o.note.title=e.trim().split(/\r?\n/,2)[0]||t("notes","New note")},o.toggleCheckbox=function(e){var t=n(e),o=n(".CodeMirror")[0].CodeMirror,r=o.getDoc(),i=t.parents(".CodeMirror-line").index(),a=r.getLineHandle(i),c="[x]"===t.text()?"[ ]":"[x]";r.replaceRange(c,{line:i,ch:a.text.indexOf("[")},{line:i,ch:a.text.indexOf("]")+1})},o.onEdit=function(){var e=o.note;e.unsaved=!0,o.autoSave(e)},o.autoSave=c(function(e){i.add(e)},1e3),o.manualSave=function(){var e=o.note;e.error=!1,i.addManual(e)},o.editCategory=!1,o.showEditCategory=function(){n("#category").val(o.note.category),o.editCategory=!0,n("#category").autocomplete({source:r.getCategories(r.getAll(),0,!1),minLength:0,position:{my:"left bottom",at:"left top",of:"#category"},open:function(){l(function(){var e=n("form.category").innerWidth()-2;n(".ui-autocomplete.ui-menu").width(e)})}}).autocomplete("widget").addClass("category-autocomplete"),n("form.category .icon-confirm").insertAfter("#category"),l(function(){n("#category").focus(),n("#category").autocomplete("search","")})},o.saveCategory=function(){var e=n("#category").val();return o.note.category===e?void(o.editCategory=!1):(o.isCategorySaving=!0,void o.note.customPUT({category:e},"category",{},{}).then(function(n){o.note.category=n.category,e!==n.category&&OC.Notification.showTemporary(t("notes","Updating the note's category has failed. Is the target directory writable?"))},function(){OC.Notification.showTemporary(t("notes","Updating the note's category has failed."))})["finally"](function(){o.isCategorySaving=!1,o.editCategory=!1}))},u.unbind("keypress.notes.save"),u.bind("keypress.notes.save",function(e){if(e.ctrlKey||e.metaKey)switch(String.fromCharCode(e.which).toLowerCase()){case"s":e.preventDefault(),o.manualSave()}}),o.toggleDistractionFree=function(){function e(e){e.requestFullscreen?e.requestFullscreen():e.mozRequestFullScreen?e.mozRequestFullScreen():e.webkitRequestFullscreen?e.webkitRequestFullscreen():e.msRequestFullscreen&&e.msRequestFullscreen()}function t(){document.exitFullscreen?document.exitFullscreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.webkitExitFullscreen&&document.webkitExitFullscreen()}document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement?t():e(document.getElementById("app-content"))},o.$watch(function(){return o.note.title},function(e){e?document.title=e+" - "+o.defaultTitle:document.title=o.defaultTitle})}]),a.controller("NotesController",["$routeParams","$scope","$location","Restangular","NotesModel","$window",function(e,o,r,i,a,c){o.route=e,o.notesLoaded=!1,o.notes=a.getAll(),o.folderSelectorOpen=!1,o.filterCategory=null,o.orderRecent=["-favorite","-modified"],o.orderAlpha=["category","-favorite","title"],o.filterOrder=o.orderRecent;var u=i.all("notes");u.getList().then(function(e){a.addAll(e),o.notesLoaded=!0}),o.create=function(){u.post({category:o.filterCategory}).then(function(e){a.add(e),r.path("/notes/"+e.id)})},o["delete"]=function(e){var t=a.get(e);t.remove().then(function(){a.remove(e),o.$emit("$routeChangeError")})},o.toggleFavorite=function(e,t){var n=a.get(e);n.customPUT({favorite:!n.favorite},"favorite",{},{}).then(function(e){n.favorite=!!e}),t.target.blur()},o.categories=[],o.$watch("notes",function(e){o.categories=a.getCategories(e,1,!0)},!0),o.toggleFolderSelector=function(){o.folderSelectorOpen=!o.folderSelectorOpen},o.setFilter=function(e){null===e?o.filterOrder=o.orderRecent:o.filterOrder=o.orderAlpha,o.filterCategory=e,o.folderSelectorOpen=!1,n("#app-navigation > ul").animate({scrollTop:0},"fast")},o.categoryFilter=function(e){if(null!==o.filterCategory){if(e.category===o.filterCategory)return!0;if(null!==e.category)return e.category.startsWith(o.filterCategory+"/")}return!0},o.isCategory=function(e){return"string"==typeof e},c.onbeforeunload=function(){for(var e=a.getAll(),n=0;n0){var c=this.nthIndexOf(a,"/",t);c>0&&(a=a.substring(0,c))}o[a]===i?o[a]=1:o[a]+=1}var u=[];for(var l in o)n?u.push({name:l,count:o[l]}):l&&u.push(l);return n||u.sort(),u}},new t}),a.factory("SaveQueue",["$q",function(e){var t=function(){this._queue={},this._flushLock=!1,this._manualSaveActive=!1};return t.prototype={add:function(e){this._queue[e.id]=e,this._flush()},addManual:function(e){this._manualSaveActive=!0,this.add(e)},_flush:function(){var t=Object.keys(this._queue);if(0!==t.length&&!this._flushLock){this._flushLock=!0;for(var n=this,o=[],r=0;r\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig(function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}).run(function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote, errorMessage) {\n $scope.defaultTitle = document.title;\n\n if(lastViewedNote !== 0 && $location.path()==='') {\n $location.path('/notes/' + lastViewedNote);\n }\n if(errorMessage) {\n OC.Notification.showTemporary(errorMessage);\n }\n $scope.initSearch();\n };\n\n $scope.search = '';\n $scope.defaultTitle = null;\n\n $scope.initSearch = function() {\n new OCA.Search(\n function (query) {\n $scope.search = query;\n $scope.$apply();\n if($('#app-navigation-toggle').css('display')!=='none' &&\n !$('body').hasClass('snapjs-left')) {\n $('#app-navigation-toggle').click();\n }\n },\n function () {\n $scope.search = '';\n $scope.$apply();\n }\n );\n };\n\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note, debounce,\n $document, $timeout) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n $scope.isManualSaving = function () {\n return SaveQueue.isManualSaving();\n };\n\n $scope.updateTitle = function () {\n var content = $scope.note.content;\n\n // prepare content: remove markdown characters and empty spaces\n content = content.replace(/^\\s*[*+-]\\s+/mg, ''); // list item\n content = content.replace(/^#+\\s+(.*?)\\s*#*$/mg, '$1'); // headline\n content = content.replace(/^(=+|-+)$/mg, ''); // separate headline\n content = content.replace(/(\\*+|_+)(.*?)\\1/mg, '$2'); // emphasis\n\n // prevent directory traversal, illegal characters\n content = content.replace(/[\\*\\|\\/\\\\\\:\\\"<>\\?]/g, '');\n // prevent unintended file names\n content = content.replace(/^[\\. ]+/mg, '');\n\n // generate title from the first line of the content\n $scope.note.title = content.trim().split(/\\r?\\n/, 2)[0] ||\n t('notes', 'New note');\n };\n\n $scope.onEdit = function() {\n var note = $scope.note;\n note.unsaved = true;\n $scope.autoSave(note);\n };\n\n $scope.autoSave = debounce(function(note) {\n SaveQueue.add(note);\n }, 1000);\n\n $scope.manualSave = function() {\n var note = $scope.note;\n note.error = false;\n SaveQueue.addManual(note);\n };\n\n $scope.editCategory = false;\n $scope.showEditCategory = function() {\n $('#category').val($scope.note.category);\n $scope.editCategory = true;\n $('#category').autocomplete({\n source: NotesModel.getCategories(NotesModel.getAll(), 0, false),\n minLength: 0,\n position: { my: 'left bottom', at: 'left top', of: '#category' },\n open: function() {\n $timeout(function() {\n var width = $('form.category').innerWidth() - 2;\n $('.ui-autocomplete.ui-menu').width(width);\n });\n },\n }).autocomplete('widget').addClass('category-autocomplete');\n // fix space between input and confirm-button\n $('form.category .icon-confirm').insertAfter('#category');\n\n $timeout(function() {\n $('#category').focus();\n $('#category').autocomplete('search', '');\n });\n };\n $scope.saveCategory = function () {\n var category = $('#category').val();\n if($scope.note.category === category) {\n $scope.editCategory = false;\n return;\n }\n $scope.isCategorySaving = true;\n $scope.note.customPUT({category: category}, 'category', {}, {})\n .then(\n function (updatedNote) {\n $scope.note.category = updatedNote.category;\n if(category !== updatedNote.category) {\n OC.Notification.showTemporary(\n t('notes', 'Updating the note\\'s category has failed.'+\n ' Is the target directory writable?')\n );\n }\n },\n function () {\n OC.Notification.showTemporary(\n t('notes', 'Updating the note\\'s category has failed.')\n );\n }\n )\n .finally(\n function () {\n $scope.isCategorySaving = false;\n $scope.editCategory = false;\n }\n );\n };\n\n\n $document.unbind('keypress.notes.save');\n $document.bind('keypress.notes.save', function(event) {\n if(event.ctrlKey || event.metaKey) {\n switch(String.fromCharCode(event.which).toLowerCase()) {\n case 's':\n event.preventDefault();\n $scope.manualSave();\n break;\n }\n }\n });\n\n $scope.toggleDistractionFree = function() {\n function launchIntoFullscreen(element) {\n if(element.requestFullscreen) {\n element.requestFullscreen();\n } else if(element.mozRequestFullScreen) {\n element.mozRequestFullScreen();\n } else if(element.webkitRequestFullscreen) {\n element.webkitRequestFullscreen();\n } else if(element.msRequestFullscreen) {\n element.msRequestFullscreen();\n }\n }\n\n function exitFullscreen() {\n if(document.exitFullscreen) {\n document.exitFullscreen();\n } else if(document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if(document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n }\n\n if(document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement) {\n exitFullscreen();\n } else {\n launchIntoFullscreen(document.getElementById('app-content'));\n }\n };\n\n $scope.$watch(function() {\n return $scope.note.title;\n }, function(newValue) {\n if(newValue) {\n document.title = newValue + ' - ' + $scope.defaultTitle;\n } else {\n document.title = $scope.defaultTitle;\n }\n });\n\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel, $window) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notesLoaded = false;\n $scope.notes = NotesModel.getAll();\n\n $scope.folderSelectorOpen = false;\n $scope.filterCategory = null;\n\n $scope.orderRecent = ['-favorite','-modified'];\n $scope.orderAlpha = ['category','-favorite','title'];\n $scope.filterOrder = $scope.orderRecent;\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n $scope.notesLoaded = true;\n });\n\n $scope.create = function () {\n notesResource.post({category: $scope.filterCategory})\n .then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId, event) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n event.target.blur();\n };\n\n $scope.categories = [];\n $scope.$watch('notes', function(notes) {\n $scope.categories = NotesModel.getCategories(notes, 1, true);\n }, true);\n\n $scope.toggleFolderSelector = function () {\n $scope.folderSelectorOpen = !$scope.folderSelectorOpen;\n };\n\n $scope.setFilter = function (category) {\n if(category===null) {\n $scope.filterOrder = $scope.orderRecent;\n } else {\n $scope.filterOrder = $scope.orderAlpha;\n }\n $scope.filterCategory = category;\n $scope.folderSelectorOpen = false;\n $('#app-navigation > ul').animate({scrollTop: 0}, 'fast');\n };\n\n $scope.categoryFilter = function (note) {\n if($scope.filterCategory!==null) {\n if(note.category===$scope.filterCategory) {\n return true;\n } else if(note.category!==null) {\n return note.category.startsWith($scope.filterCategory+'/');\n }\n }\n return true;\n };\n\n $scope.isCategory = function (item) {\n return typeof item === 'string';\n };\n\n $window.onbeforeunload = function() {\n var notes = NotesModel.getAll();\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig([\"$provide\", \"$routeProvider\", \"RestangularProvider\", \"$httpProvider\", \"$windowProvider\", function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: [\"$route\", \"$q\", \"is\", \"Restangular\", function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }]\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}]).run([\"$rootScope\", \"$location\", \"NotesModel\", function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n}]);\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', [\"$scope\", \"$location\", \"is\", function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote, errorMessage) {\n $scope.defaultTitle = document.title;\n\n if(lastViewedNote !== 0 && $location.path()==='') {\n $location.path('/notes/' + lastViewedNote);\n }\n if(errorMessage) {\n OC.Notification.showTemporary(errorMessage);\n }\n $scope.initSearch();\n };\n\n $scope.search = '';\n $scope.defaultTitle = null;\n\n $scope.initSearch = function() {\n new OCA.Search(\n function (query) {\n $scope.search = query;\n $scope.$apply();\n if($('#app-navigation-toggle').css('display')!=='none' &&\n !$('body').hasClass('snapjs-left')) {\n $('#app-navigation-toggle').click();\n }\n },\n function () {\n $scope.search = '';\n $scope.$apply();\n }\n );\n };\n\n}]);\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', [\"$routeParams\", \"$scope\", \"NotesModel\", \"SaveQueue\", \"note\", \"debounce\", \"$document\", \"$timeout\", function($routeParams, $scope, NotesModel,\n SaveQueue, note, debounce,\n $document, $timeout) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n $scope.isManualSaving = function () {\n return SaveQueue.isManualSaving();\n };\n\n $scope.updateTitle = function () {\n var content = $scope.note.content;\n\n // prepare content: remove markdown characters and empty spaces\n content = content.replace(/^\\s*[*+-]\\s+/mg, ''); // list item\n content = content.replace(/^#+\\s+(.*?)\\s*#*$/mg, '$1'); // headline\n content = content.replace(/^(=+|-+)$/mg, ''); // separate headline\n content = content.replace(/(\\*+|_+)(.*?)\\1/mg, '$2'); // emphasis\n\n // prevent directory traversal, illegal characters\n content = content.replace(/[\\*\\|\\/\\\\\\:\\\"<>\\?]/g, '');\n // prevent unintended file names\n content = content.replace(/^[\\. ]+/mg, '');\n\n // generate title from the first line of the content\n $scope.note.title = content.trim().split(/\\r?\\n/, 2)[0] ||\n t('notes', 'New note');\n };\n\n $scope.onEdit = function() {\n var note = $scope.note;\n note.unsaved = true;\n $scope.autoSave(note);\n };\n\n $scope.autoSave = debounce(function(note) {\n SaveQueue.add(note);\n }, 1000);\n\n $scope.manualSave = function() {\n var note = $scope.note;\n note.error = false;\n SaveQueue.addManual(note);\n };\n\n $scope.editCategory = false;\n $scope.showEditCategory = function() {\n $('#category').val($scope.note.category);\n $scope.editCategory = true;\n $('#category').autocomplete({\n source: NotesModel.getCategories(NotesModel.getAll(), 0, false),\n minLength: 0,\n position: { my: 'left bottom', at: 'left top', of: '#category' },\n open: function() {\n $timeout(function() {\n var width = $('form.category').innerWidth() - 2;\n $('.ui-autocomplete.ui-menu').width(width);\n });\n },\n }).autocomplete('widget').addClass('category-autocomplete');\n // fix space between input and confirm-button\n $('form.category .icon-confirm').insertAfter('#category');\n\n $timeout(function() {\n $('#category').focus();\n $('#category').autocomplete('search', '');\n });\n };\n $scope.saveCategory = function () {\n var category = $('#category').val();\n if($scope.note.category === category) {\n $scope.editCategory = false;\n return;\n }\n $scope.isCategorySaving = true;\n $scope.note.customPUT({category: category}, 'category', {}, {})\n .then(\n function (updatedNote) {\n $scope.note.category = updatedNote.category;\n if(category !== updatedNote.category) {\n OC.Notification.showTemporary(\n t('notes', 'Updating the note\\'s category has failed.'+\n ' Is the target directory writable?')\n );\n }\n },\n function () {\n OC.Notification.showTemporary(\n t('notes', 'Updating the note\\'s category has failed.')\n );\n }\n )\n .finally(\n function () {\n $scope.isCategorySaving = false;\n $scope.editCategory = false;\n }\n );\n };\n\n\n $document.unbind('keypress.notes.save');\n $document.bind('keypress.notes.save', function(event) {\n if(event.ctrlKey || event.metaKey) {\n switch(String.fromCharCode(event.which).toLowerCase()) {\n case 's':\n event.preventDefault();\n $scope.manualSave();\n break;\n }\n }\n });\n\n $scope.toggleDistractionFree = function() {\n function launchIntoFullscreen(element) {\n if(element.requestFullscreen) {\n element.requestFullscreen();\n } else if(element.mozRequestFullScreen) {\n element.mozRequestFullScreen();\n } else if(element.webkitRequestFullscreen) {\n element.webkitRequestFullscreen();\n } else if(element.msRequestFullscreen) {\n element.msRequestFullscreen();\n }\n }\n\n function exitFullscreen() {\n if(document.exitFullscreen) {\n document.exitFullscreen();\n } else if(document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if(document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n }\n\n if(document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement) {\n exitFullscreen();\n } else {\n launchIntoFullscreen(document.getElementById('app-content'));\n }\n };\n\n $scope.$watch(function() {\n return $scope.note.title;\n }, function(newValue) {\n if(newValue) {\n document.title = newValue + ' - ' + $scope.defaultTitle;\n } else {\n document.title = $scope.defaultTitle;\n }\n });\n\n}]);\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', [\"$routeParams\", \"$scope\", \"$location\", \"Restangular\", \"NotesModel\", \"$window\", function($routeParams, $scope, $location,\n Restangular, NotesModel, $window) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notesLoaded = false;\n $scope.notes = NotesModel.getAll();\n\n $scope.folderSelectorOpen = false;\n $scope.filterCategory = null;\n\n $scope.orderRecent = ['-favorite','-modified'];\n $scope.orderAlpha = ['category','-favorite','title'];\n $scope.filterOrder = $scope.orderRecent;\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n $scope.notesLoaded = true;\n });\n\n $scope.create = function () {\n notesResource.post({category: $scope.filterCategory})\n .then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId, event) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n event.target.blur();\n };\n\n $scope.categories = [];\n $scope.$watch('notes', function(notes) {\n $scope.categories = NotesModel.getCategories(notes, 1, true);\n }, true);\n\n $scope.toggleFolderSelector = function () {\n $scope.folderSelectorOpen = !$scope.folderSelectorOpen;\n };\n\n $scope.setFilter = function (category) {\n if(category===null) {\n $scope.filterOrder = $scope.orderRecent;\n } else {\n $scope.filterOrder = $scope.orderAlpha;\n }\n $scope.filterCategory = category;\n $scope.folderSelectorOpen = false;\n $('#app-navigation > ul').animate({scrollTop: 0}, 'fast');\n };\n\n $scope.categoryFilter = function (note) {\n if($scope.filterCategory!==null) {\n if(note.category===$scope.filterCategory) {\n return true;\n } else if(note.category!==null) {\n return note.category.startsWith($scope.filterCategory+'/');\n }\n }\n return true;\n };\n\n $scope.isCategory = function (item) {\n return typeof item === 'string';\n };\n\n $window.onbeforeunload = function() {\n var notes = NotesModel.getAll();\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n\n/*global SimpleMDE*/\napp.directive('editor', ['$timeout',\n 'urlFinder',\n function ($timeout, urlFinder) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\n\t\t\tvar simplemde = new SimpleMDE({\n\t\t\t\telement: element[0],\n\t\t\t\tspellChecker: false,\n\t\t\t\tautoDownloadFontAwesome: false,\n\t\t\t\ttoolbar: false,\n\t\t\t\tstatus: false,\n\t\t\t\tforceSync: true\n\t\t\t});\n\t\t\tvar editorElement = $(simplemde.codemirror.getWrapperElement());\n\n\t\t\tsimplemde.value(scope.note.content);\n\t\t\tsimplemde.codemirror.focus();\n\n\t\t\tsimplemde.codemirror.on('change', function() {\n\t\t\t\t$timeout(function() {\n\t\t\t\t\tscope.$apply(function () {\n\t\t\t\t\t\tscope.note.content = simplemde.value();\n\t\t\t\t\t\tscope.onEdit();\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\n\t\t\teditorElement.on('click', '.cm-link, .cm-url', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = urlFinder(this);\n\t\t\t\t\tif(angular.isDefined(url)) {\n\t\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip({'container': 'body'});\n\n element.on('$destroy', function() {\n element.tooltip('hide');\n });\n\n element.on('click', function() {\n element.tooltip('hide');\n });\n }\n };\n});\n\n/**\n * filter by multiple words (AND operation)\n */\napp.filter('and', ['$filter', function ($filter) {\n\t'use strict';\n\treturn function (items, searchString) {\n\t\tvar searchValues = searchString.split(' ');\n\t\tvar filtered = items;\n\t\tfor(var i in searchValues) {\n\t\t\tfiltered = $filter('filter')(filtered, searchValues[i]);\n\t\t}\n\t\treturn filtered;\n\t};\n}]);\n\napp.filter('categoryTitle', function () {\n\t'use strict';\n\treturn function (str) {\n\t\tif (str && (typeof str === 'string')) {\n\t\t\treturn str.replace(/\\//g, ' / ');\n\t\t} else {\n\t\t\treturn '';\n\t\t}\n\t};\n});\n\n/**\n * group notes by (sub) category\n */\napp.filter('groupNotes', function () {\n\t'use strict';\n\treturn function (items, category) {\n\t\tif(category) {\n\t\t\tvar prevCat = null;\n\t\t\tfor(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i0) {\n var index = this.nthIndexOf(cat, '/', maxLevel);\n if(index>0) {\n cat = cat.substring(0, index);\n }\n }\n if(categories[cat]===undefined) {\n categories[cat] = 1;\n } else {\n categories[cat] += 1;\n }\n }\n var result = [];\n for(var category in categories) {\n if(details) {\n result.push({\n name: category,\n count: categories[category],\n });\n } else if(category) {\n result.push(category);\n }\n }\n if(!details) {\n result.sort();\n }\n return result;\n },\n\n };\n\n return new NotesModel();\n});\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', [\"$q\", function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n this._manualSaveActive = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n addManual: function (note) {\n this._manualSaveActive = true;\n this.add(note);\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global SimpleMDE*/\napp.directive('editor', ['$timeout',\n 'urlFinder',\n function ($timeout, urlFinder) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\n\t\t\tvar simplemde = new SimpleMDE({\n\t\t\t\telement: element[0],\n\t\t\t\tspellChecker: false,\n\t\t\t\tautoDownloadFontAwesome: false,\n\t\t\t\ttoolbar: false,\n\t\t\t\tstatus: false,\n\t\t\t\tforceSync: true\n\t\t\t});\n\t\t\tvar editorElement = $(simplemde.codemirror.getWrapperElement());\n\n\t\t\tsimplemde.value(scope.note.content);\n\t\t\tsimplemde.codemirror.focus();\n\n\t\t\tsimplemde.codemirror.on('change', function() {\n\t\t\t\t$timeout(function() {\n\t\t\t\t\tscope.$apply(function () {\n\t\t\t\t\t\tscope.note.content = simplemde.value();\n\t\t\t\t\t\tscope.onEdit();\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\n\t\t\teditorElement.on('click', '.cm-link, .cm-url', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = urlFinder(this);\n\t\t\t\t\tif(angular.isDefined(url)) {\n\t\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip({'container': 'body'});\n\n element.on('$destroy', function() {\n element.tooltip('hide');\n });\n\n element.on('click', function() {\n element.tooltip('hide');\n });\n }\n };\n});\n","/**\n * filter by multiple words (AND operation)\n */\napp.filter('and', ['$filter', function ($filter) {\n\t'use strict';\n\treturn function (items, searchString) {\n\t\tvar searchValues = searchString.split(' ');\n\t\tvar filtered = items;\n\t\tfor(var i in searchValues) {\n\t\t\tfiltered = $filter('filter')(filtered, searchValues[i]);\n\t\t}\n\t\treturn filtered;\n\t};\n}]);\n","app.filter('categoryTitle', function () {\n\t'use strict';\n\treturn function (str) {\n\t\tif (str && (typeof str === 'string')) {\n\t\t\treturn str.replace(/\\//g, ' / ');\n\t\t} else {\n\t\t\treturn '';\n\t\t}\n\t};\n});\n","/**\n * group notes by (sub) category\n */\napp.filter('groupNotes', function () {\n\t'use strict';\n\treturn function (items, category) {\n\t\tif(category) {\n\t\t\tvar prevCat = null;\n\t\t\tfor(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i0) {\n var index = this.nthIndexOf(cat, '/', maxLevel);\n if(index>0) {\n cat = cat.substring(0, index);\n }\n }\n if(categories[cat]===undefined) {\n categories[cat] = 1;\n } else {\n categories[cat] += 1;\n }\n }\n var result = [];\n for(var category in categories) {\n if(details) {\n result.push({\n name: category,\n count: categories[category],\n });\n } else if(category) {\n result.push(category);\n }\n }\n if(!details) {\n result.sort();\n }\n return result;\n },\n\n };\n\n return new NotesModel();\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n this._manualSaveActive = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n addManual: function (note) {\n this._manualSaveActive = true;\n this.add(note);\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig(function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}).run(function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote, errorMessage) {\n $scope.defaultTitle = document.title;\n\n if(lastViewedNote !== 0 && $location.path()==='') {\n $location.path('/notes/' + lastViewedNote);\n }\n if(errorMessage) {\n OC.Notification.showTemporary(errorMessage);\n }\n $scope.initSearch();\n };\n\n $scope.search = '';\n $scope.defaultTitle = null;\n\n $scope.initSearch = function() {\n new OCA.Search(\n function (query) {\n $scope.search = query;\n $scope.$apply();\n if($('#app-navigation-toggle').css('display')!=='none' &&\n !$('body').hasClass('snapjs-left')) {\n $('#app-navigation-toggle').click();\n }\n },\n function () {\n $scope.search = '';\n $scope.$apply();\n }\n );\n };\n\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', function($routeParams, $scope, NotesModel,\n SaveQueue, note, debounce,\n $document, $timeout) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n $scope.isManualSaving = function () {\n return SaveQueue.isManualSaving();\n };\n\n $scope.updateTitle = function () {\n var content = $scope.note.content;\n\n // prepare content: remove markdown characters and empty spaces\n content = content.replace(/^\\s*[*+-]\\s+/mg, ''); // list item\n content = content.replace(/^#+\\s+(.*?)\\s*#*$/mg, '$1'); // headline\n content = content.replace(/^(=+|-+)$/mg, ''); // separate headline\n content = content.replace(/(\\*+|_+)(.*?)\\1/mg, '$2'); // emphasis\n\n // prevent directory traversal, illegal characters\n content = content.replace(/[\\*\\|\\/\\\\\\:\\\"<>\\?]/g, '');\n // prevent unintended file names\n content = content.replace(/^[\\. ]+/mg, '');\n\n // generate title from the first line of the content\n $scope.note.title = content.trim().split(/\\r?\\n/, 2)[0] ||\n t('notes', 'New note');\n };\n\n $scope.toggleCheckbox = function (el) {\n var $el = $(el);\n var cm = $('.CodeMirror')[0].CodeMirror;\n var doc = cm.getDoc();\n var index = $el.parents('.CodeMirror-line').index();\n var line = doc.getLineHandle(index);\n\n var newvalue = ( $el.text() === '[x]' ) ? '[ ]' : '[x]';\n\n // + 1 for some reason... not sure why\n doc.replaceRange(newvalue,\n {line: index, ch: line.text.indexOf('[')},\n {line: index, ch: line.text.indexOf(']') + 1}\n );\n };\n\n $scope.onEdit = function() {\n var note = $scope.note;\n note.unsaved = true;\n $scope.autoSave(note);\n };\n\n $scope.autoSave = debounce(function(note) {\n SaveQueue.add(note);\n }, 1000);\n\n $scope.manualSave = function() {\n var note = $scope.note;\n note.error = false;\n SaveQueue.addManual(note);\n };\n\n $scope.editCategory = false;\n $scope.showEditCategory = function() {\n $('#category').val($scope.note.category);\n $scope.editCategory = true;\n $('#category').autocomplete({\n source: NotesModel.getCategories(NotesModel.getAll(), 0, false),\n minLength: 0,\n position: { my: 'left bottom', at: 'left top', of: '#category' },\n open: function() {\n $timeout(function() {\n var width = $('form.category').innerWidth() - 2;\n $('.ui-autocomplete.ui-menu').width(width);\n });\n },\n }).autocomplete('widget').addClass('category-autocomplete');\n // fix space between input and confirm-button\n $('form.category .icon-confirm').insertAfter('#category');\n\n $timeout(function() {\n $('#category').focus();\n $('#category').autocomplete('search', '');\n });\n };\n $scope.saveCategory = function () {\n var category = $('#category').val();\n if($scope.note.category === category) {\n $scope.editCategory = false;\n return;\n }\n $scope.isCategorySaving = true;\n $scope.note.customPUT({category: category}, 'category', {}, {})\n .then(\n function (updatedNote) {\n $scope.note.category = updatedNote.category;\n if(category !== updatedNote.category) {\n OC.Notification.showTemporary(\n t('notes', 'Updating the note\\'s category has failed.'+\n ' Is the target directory writable?')\n );\n }\n },\n function () {\n OC.Notification.showTemporary(\n t('notes', 'Updating the note\\'s category has failed.')\n );\n }\n )\n .finally(\n function () {\n $scope.isCategorySaving = false;\n $scope.editCategory = false;\n }\n );\n };\n\n\n $document.unbind('keypress.notes.save');\n $document.bind('keypress.notes.save', function(event) {\n if(event.ctrlKey || event.metaKey) {\n switch(String.fromCharCode(event.which).toLowerCase()) {\n case 's':\n event.preventDefault();\n $scope.manualSave();\n break;\n }\n }\n });\n\n $scope.toggleDistractionFree = function() {\n function launchIntoFullscreen(element) {\n if(element.requestFullscreen) {\n element.requestFullscreen();\n } else if(element.mozRequestFullScreen) {\n element.mozRequestFullScreen();\n } else if(element.webkitRequestFullscreen) {\n element.webkitRequestFullscreen();\n } else if(element.msRequestFullscreen) {\n element.msRequestFullscreen();\n }\n }\n\n function exitFullscreen() {\n if(document.exitFullscreen) {\n document.exitFullscreen();\n } else if(document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if(document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n }\n\n if(document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement) {\n exitFullscreen();\n } else {\n launchIntoFullscreen(document.getElementById('app-content'));\n }\n };\n\n $scope.$watch(function() {\n return $scope.note.title;\n }, function(newValue) {\n if(newValue) {\n document.title = newValue + ' - ' + $scope.defaultTitle;\n } else {\n document.title = $scope.defaultTitle;\n }\n });\n\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', function($routeParams, $scope, $location,\n Restangular, NotesModel, $window) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notesLoaded = false;\n $scope.notes = NotesModel.getAll();\n\n $scope.folderSelectorOpen = false;\n $scope.filterCategory = null;\n\n $scope.orderRecent = ['-favorite','-modified'];\n $scope.orderAlpha = ['category','-favorite','title'];\n $scope.filterOrder = $scope.orderRecent;\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n $scope.notesLoaded = true;\n });\n\n $scope.create = function () {\n notesResource.post({category: $scope.filterCategory})\n .then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId, event) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n event.target.blur();\n };\n\n $scope.categories = [];\n $scope.$watch('notes', function(notes) {\n $scope.categories = NotesModel.getCategories(notes, 1, true);\n }, true);\n\n $scope.toggleFolderSelector = function () {\n $scope.folderSelectorOpen = !$scope.folderSelectorOpen;\n };\n\n $scope.setFilter = function (category) {\n if(category===null) {\n $scope.filterOrder = $scope.orderRecent;\n } else {\n $scope.filterOrder = $scope.orderAlpha;\n }\n $scope.filterCategory = category;\n $scope.folderSelectorOpen = false;\n $('#app-navigation > ul').animate({scrollTop: 0}, 'fast');\n };\n\n $scope.categoryFilter = function (note) {\n if($scope.filterCategory!==null) {\n if(note.category===$scope.filterCategory) {\n return true;\n } else if(note.category!==null) {\n return note.category.startsWith($scope.filterCategory+'/');\n }\n }\n return true;\n };\n\n $scope.isCategory = function (item) {\n return typeof item === 'string';\n };\n\n $window.onbeforeunload = function() {\n var notes = NotesModel.getAll();\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n/* jshint unused: false */\nvar app = angular.module('Notes', ['restangular', 'ngRoute']).\nconfig([\"$provide\", \"$routeProvider\", \"RestangularProvider\", \"$httpProvider\", \"$windowProvider\", function($provide, $routeProvider, RestangularProvider, $httpProvider,\n $windowProvider) {\n 'use strict';\n\n // Always send the CSRF token by default\n $httpProvider.defaults.headers.common.requesttoken = requestToken;\n\n // you have to use $provide inside the config method to provide a globally\n // shared and injectable object\n $provide.value('Constants', {\n saveInterval: 5*1000 // miliseconds\n });\n\n // define your routes that that load templates into the ng-view\n $routeProvider.when('/notes/:noteId', {\n templateUrl: 'note.html',\n controller: 'NoteController',\n resolve: {\n // $routeParams does not work inside resolve so use $route\n // note is the name of the argument that will be injected into the\n // controller\n /* @ngInject */\n note: [\"$route\", \"$q\", \"is\", \"Restangular\", function ($route, $q, is, Restangular) {\n\n var deferred = $q.defer();\n var noteId = $route.current.params.noteId;\n is.loading = true;\n\n Restangular.one('notes', noteId).get().then(function (note) {\n is.loading = false;\n deferred.resolve(note);\n }, function () {\n is.loading = false;\n deferred.reject();\n });\n\n return deferred.promise;\n }]\n }\n }).otherwise({\n redirectTo: '/'\n });\n\n var baseUrl = OC.generateUrl('/apps/notes');\n RestangularProvider.setBaseUrl(baseUrl);\n\n\n\n}]).run([\"$rootScope\", \"$location\", \"NotesModel\", function ($rootScope, $location, NotesModel) {\n 'use strict';\n\n $('link[rel=\"shortcut icon\"]').attr(\n\t\t 'href',\n\t\t OC.filePath('notes', 'img', 'favicon.png')\n );\n\n // handle route errors\n $rootScope.$on('$routeChangeError', function () {\n var notes = NotesModel.getAll();\n\n // route change error should redirect to the latest note if possible\n if (notes.length > 0) {\n var sorted = notes.sort(function (a, b) {\n if(a.modified > b.modified) {\n return 1;\n } else if(a.modified < b.modified) {\n return -1;\n } else {\n return 0;\n }\n });\n\n var note = notes[sorted.length-1];\n $location.path('/notes/' + note.id);\n } else {\n $location.path('/');\n }\n });\n}]);\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('AppController', [\"$scope\", \"$location\", \"is\", function ($scope, $location, is) {\n 'use strict';\n\n $scope.is = is;\n\n $scope.init = function (lastViewedNote, errorMessage) {\n $scope.defaultTitle = document.title;\n\n if(lastViewedNote !== 0 && $location.path()==='') {\n $location.path('/notes/' + lastViewedNote);\n }\n if(errorMessage) {\n OC.Notification.showTemporary(errorMessage);\n }\n $scope.initSearch();\n };\n\n $scope.search = '';\n $scope.defaultTitle = null;\n\n $scope.initSearch = function() {\n new OCA.Search(\n function (query) {\n $scope.search = query;\n $scope.$apply();\n if($('#app-navigation-toggle').css('display')!=='none' &&\n !$('body').hasClass('snapjs-left')) {\n $('#app-navigation-toggle').click();\n }\n },\n function () {\n $scope.search = '';\n $scope.$apply();\n }\n );\n };\n\n}]);\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.controller('NoteController', [\"$routeParams\", \"$scope\", \"NotesModel\", \"SaveQueue\", \"note\", \"debounce\", \"$document\", \"$timeout\", function($routeParams, $scope, NotesModel,\n SaveQueue, note, debounce,\n $document, $timeout) {\n 'use strict';\n\n NotesModel.updateIfExists(note);\n\n $scope.note = NotesModel.get($routeParams.noteId);\n\n $scope.isSaving = function () {\n return SaveQueue.isSaving();\n };\n $scope.isManualSaving = function () {\n return SaveQueue.isManualSaving();\n };\n\n $scope.updateTitle = function () {\n var content = $scope.note.content;\n\n // prepare content: remove markdown characters and empty spaces\n content = content.replace(/^\\s*[*+-]\\s+/mg, ''); // list item\n content = content.replace(/^#+\\s+(.*?)\\s*#*$/mg, '$1'); // headline\n content = content.replace(/^(=+|-+)$/mg, ''); // separate headline\n content = content.replace(/(\\*+|_+)(.*?)\\1/mg, '$2'); // emphasis\n\n // prevent directory traversal, illegal characters\n content = content.replace(/[\\*\\|\\/\\\\\\:\\\"<>\\?]/g, '');\n // prevent unintended file names\n content = content.replace(/^[\\. ]+/mg, '');\n\n // generate title from the first line of the content\n $scope.note.title = content.trim().split(/\\r?\\n/, 2)[0] ||\n t('notes', 'New note');\n };\n\n $scope.toggleCheckbox = function (el) {\n var $el = $(el);\n var cm = $('.CodeMirror')[0].CodeMirror;\n var doc = cm.getDoc();\n var index = $el.parents('.CodeMirror-line').index();\n var line = doc.getLineHandle(index);\n\n var newvalue = ( $el.text() === '[x]' ) ? '[ ]' : '[x]';\n\n // + 1 for some reason... not sure why\n doc.replaceRange(newvalue,\n {line: index, ch: line.text.indexOf('[')},\n {line: index, ch: line.text.indexOf(']') + 1}\n );\n };\n\n $scope.onEdit = function() {\n var note = $scope.note;\n note.unsaved = true;\n $scope.autoSave(note);\n };\n\n $scope.autoSave = debounce(function(note) {\n SaveQueue.add(note);\n }, 1000);\n\n $scope.manualSave = function() {\n var note = $scope.note;\n note.error = false;\n SaveQueue.addManual(note);\n };\n\n $scope.editCategory = false;\n $scope.showEditCategory = function() {\n $('#category').val($scope.note.category);\n $scope.editCategory = true;\n $('#category').autocomplete({\n source: NotesModel.getCategories(NotesModel.getAll(), 0, false),\n minLength: 0,\n position: { my: 'left bottom', at: 'left top', of: '#category' },\n open: function() {\n $timeout(function() {\n var width = $('form.category').innerWidth() - 2;\n $('.ui-autocomplete.ui-menu').width(width);\n });\n },\n }).autocomplete('widget').addClass('category-autocomplete');\n // fix space between input and confirm-button\n $('form.category .icon-confirm').insertAfter('#category');\n\n $timeout(function() {\n $('#category').focus();\n $('#category').autocomplete('search', '');\n });\n };\n $scope.saveCategory = function () {\n var category = $('#category').val();\n if($scope.note.category === category) {\n $scope.editCategory = false;\n return;\n }\n $scope.isCategorySaving = true;\n $scope.note.customPUT({category: category}, 'category', {}, {})\n .then(\n function (updatedNote) {\n $scope.note.category = updatedNote.category;\n if(category !== updatedNote.category) {\n OC.Notification.showTemporary(\n t('notes', 'Updating the note\\'s category has failed.'+\n ' Is the target directory writable?')\n );\n }\n },\n function () {\n OC.Notification.showTemporary(\n t('notes', 'Updating the note\\'s category has failed.')\n );\n }\n )\n .finally(\n function () {\n $scope.isCategorySaving = false;\n $scope.editCategory = false;\n }\n );\n };\n\n\n $document.unbind('keypress.notes.save');\n $document.bind('keypress.notes.save', function(event) {\n if(event.ctrlKey || event.metaKey) {\n switch(String.fromCharCode(event.which).toLowerCase()) {\n case 's':\n event.preventDefault();\n $scope.manualSave();\n break;\n }\n }\n });\n\n $scope.toggleDistractionFree = function() {\n function launchIntoFullscreen(element) {\n if(element.requestFullscreen) {\n element.requestFullscreen();\n } else if(element.mozRequestFullScreen) {\n element.mozRequestFullScreen();\n } else if(element.webkitRequestFullscreen) {\n element.webkitRequestFullscreen();\n } else if(element.msRequestFullscreen) {\n element.msRequestFullscreen();\n }\n }\n\n function exitFullscreen() {\n if(document.exitFullscreen) {\n document.exitFullscreen();\n } else if(document.mozCancelFullScreen) {\n document.mozCancelFullScreen();\n } else if(document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n }\n\n if(document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement) {\n exitFullscreen();\n } else {\n launchIntoFullscreen(document.getElementById('app-content'));\n }\n };\n\n $scope.$watch(function() {\n return $scope.note.title;\n }, function(newValue) {\n if(newValue) {\n document.title = newValue + ' - ' + $scope.defaultTitle;\n } else {\n document.title = $scope.defaultTitle;\n }\n });\n\n}]);\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// This is available by using ng-controller=\"NotesController\" in your HTML\napp.controller('NotesController', [\"$routeParams\", \"$scope\", \"$location\", \"Restangular\", \"NotesModel\", \"$window\", function($routeParams, $scope, $location,\n Restangular, NotesModel, $window) {\n 'use strict';\n\n $scope.route = $routeParams;\n $scope.notesLoaded = false;\n $scope.notes = NotesModel.getAll();\n\n $scope.folderSelectorOpen = false;\n $scope.filterCategory = null;\n\n $scope.orderRecent = ['-favorite','-modified'];\n $scope.orderAlpha = ['category','-favorite','title'];\n $scope.filterOrder = $scope.orderRecent;\n\n var notesResource = Restangular.all('notes');\n\n // initial request for getting all notes\n notesResource.getList().then(function (notes) {\n NotesModel.addAll(notes);\n $scope.notesLoaded = true;\n });\n\n $scope.create = function () {\n notesResource.post({category: $scope.filterCategory})\n .then(function (note) {\n NotesModel.add(note);\n $location.path('/notes/' + note.id);\n });\n };\n\n $scope.delete = function (noteId) {\n var note = NotesModel.get(noteId);\n note.remove().then(function () {\n NotesModel.remove(noteId);\n $scope.$emit('$routeChangeError');\n });\n };\n\n $scope.toggleFavorite = function (noteId, event) {\n var note = NotesModel.get(noteId);\n note.customPUT({favorite: !note.favorite},\n 'favorite', {}, {}).then(function (favorite) {\n note.favorite = favorite ? true : false;\n });\n event.target.blur();\n };\n\n $scope.categories = [];\n $scope.$watch('notes', function(notes) {\n $scope.categories = NotesModel.getCategories(notes, 1, true);\n }, true);\n\n $scope.toggleFolderSelector = function () {\n $scope.folderSelectorOpen = !$scope.folderSelectorOpen;\n };\n\n $scope.setFilter = function (category) {\n if(category===null) {\n $scope.filterOrder = $scope.orderRecent;\n } else {\n $scope.filterOrder = $scope.orderAlpha;\n }\n $scope.filterCategory = category;\n $scope.folderSelectorOpen = false;\n $('#app-navigation > ul').animate({scrollTop: 0}, 'fast');\n };\n\n $scope.categoryFilter = function (note) {\n if($scope.filterCategory!==null) {\n if(note.category===$scope.filterCategory) {\n return true;\n } else if(note.category!==null) {\n return note.category.startsWith($scope.filterCategory+'/');\n }\n }\n return true;\n };\n\n $scope.isCategory = function (item) {\n return typeof item === 'string';\n };\n\n $window.onbeforeunload = function() {\n var notes = NotesModel.getAll();\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n\n/*global SimpleMDE*/\napp.directive('editor', ['$timeout',\n 'urlFinder',\n function ($timeout, urlFinder) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\n\t\t\tvar simplemde = new SimpleMDE({\n\t\t\t\telement: element[0],\n\t\t\t\tspellChecker: false,\n\t\t\t\tautoDownloadFontAwesome: false,\n\t\t\t\ttoolbar: false,\n\t\t\t\tstatus: false,\n\t\t\t\tforceSync: true\n\t\t\t});\n\t\t\tvar editorElement = $(simplemde.codemirror.getWrapperElement());\n\n\t\t\tsimplemde.value(scope.note.content);\n\t\t\tsimplemde.codemirror.focus();\n\n\t\t\t/* Initialize Checkboxes */\n\t\t\t$('.CodeMirror-code').on('mousedown.checkbox touchstart.checkbox', '.cm-formatting-task', function (e) {\n e.preventDefault();\n e.stopImmediatePropagation();\n scope.toggleCheckbox(e.target);\n\t\t\t});\n\n simplemde.codemirror.on('update', function () {\n $('.CodeMirror-line').removeClass('completed-task');\n $('.CodeMirror-line:contains(\"[x]\")').addClass('completed-task');\n });\n\n\t\t\tsimplemde.codemirror.on('change', function() {\n\t\t\t\t$timeout(function() {\n\t\t\t\t\tscope.$apply(function () {\n\t\t\t\t\t\tscope.note.content = simplemde.value();\n\t\t\t\t\t\tscope.onEdit();\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\n\t\t\teditorElement.on('click', '.cm-link, .cm-url', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = urlFinder(this);\n\t\t\t\t\tif(angular.isDefined(url)) {\n\t\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip({'container': 'body'});\n\n element.on('$destroy', function() {\n element.tooltip('hide');\n });\n\n element.on('click', function() {\n element.tooltip('hide');\n });\n }\n };\n});\n\n/**\n * filter by multiple words (AND operation)\n */\napp.filter('and', ['$filter', function ($filter) {\n\t'use strict';\n\treturn function (items, searchString) {\n\t\tvar searchValues = searchString.split(' ');\n\t\tvar filtered = items;\n\t\tfor(var i in searchValues) {\n\t\t\tfiltered = $filter('filter')(filtered, searchValues[i]);\n\t\t}\n\t\treturn filtered;\n\t};\n}]);\n\napp.filter('categoryTitle', function () {\n\t'use strict';\n\treturn function (str) {\n\t\tif (str && (typeof str === 'string')) {\n\t\t\treturn str.replace(/\\//g, ' / ');\n\t\t} else {\n\t\t\treturn '';\n\t\t}\n\t};\n});\n\n/**\n * group notes by (sub) category\n */\napp.filter('groupNotes', function () {\n\t'use strict';\n\treturn function (items, category) {\n\t\tif(category) {\n\t\t\tvar prevCat = null;\n\t\t\tfor(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i0) {\n var index = this.nthIndexOf(cat, '/', maxLevel);\n if(index>0) {\n cat = cat.substring(0, index);\n }\n }\n if(categories[cat]===undefined) {\n categories[cat] = 1;\n } else {\n categories[cat] += 1;\n }\n }\n var result = [];\n for(var category in categories) {\n if(details) {\n result.push({\n name: category,\n count: categories[category],\n });\n } else if(category) {\n result.push(category);\n }\n }\n if(!details) {\n result.sort();\n }\n return result;\n },\n\n };\n\n return new NotesModel();\n});\n\n/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', [\"$q\", function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n this._manualSaveActive = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n addManual: function (note) {\n this._manualSaveActive = true;\n this.add(note);\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesAutofocus', function () {\n 'use strict';\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.focus();\n }\n };\n});\n","/*global SimpleMDE*/\napp.directive('editor', ['$timeout',\n 'urlFinder',\n function ($timeout, urlFinder) {\n\t'use strict';\n\treturn {\n\t\trestrict: 'A',\n\t\tlink: function(scope, element) {\n\n\t\t\tvar simplemde = new SimpleMDE({\n\t\t\t\telement: element[0],\n\t\t\t\tspellChecker: false,\n\t\t\t\tautoDownloadFontAwesome: false,\n\t\t\t\ttoolbar: false,\n\t\t\t\tstatus: false,\n\t\t\t\tforceSync: true\n\t\t\t});\n\t\t\tvar editorElement = $(simplemde.codemirror.getWrapperElement());\n\n\t\t\tsimplemde.value(scope.note.content);\n\t\t\tsimplemde.codemirror.focus();\n\n\t\t\t/* Initialize Checkboxes */\n\t\t\t$('.CodeMirror-code').on('mousedown.checkbox touchstart.checkbox', '.cm-formatting-task', function (e) {\n e.preventDefault();\n e.stopImmediatePropagation();\n scope.toggleCheckbox(e.target);\n\t\t\t});\n\n simplemde.codemirror.on('update', function () {\n $('.CodeMirror-line').removeClass('completed-task');\n $('.CodeMirror-line:contains(\"[x]\")').addClass('completed-task');\n });\n\n\t\t\tsimplemde.codemirror.on('change', function() {\n\t\t\t\t$timeout(function() {\n\t\t\t\t\tscope.$apply(function () {\n\t\t\t\t\t\tscope.note.content = simplemde.value();\n\t\t\t\t\t\tscope.onEdit();\n\t\t\t\t\t\tscope.updateTitle();\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t});\n\n\t\t\teditorElement.on('click', '.cm-link, .cm-url', function(event) {\n\t\t\t\tif(event.ctrlKey) {\n\t\t\t\t\tvar url = urlFinder(this);\n\t\t\t\t\tif(angular.isDefined(url)) {\n\t\t\t\t\t\twindow.open(url, '_blank');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t};\n}]);\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.directive('notesTooltip', function () {\n 'use strict';\n\n return {\n restrict: 'A',\n link: function (scope, element) {\n element.tooltip({'container': 'body'});\n\n element.on('$destroy', function() {\n element.tooltip('hide');\n });\n\n element.on('click', function() {\n element.tooltip('hide');\n });\n }\n };\n});\n","/**\n * filter by multiple words (AND operation)\n */\napp.filter('and', ['$filter', function ($filter) {\n\t'use strict';\n\treturn function (items, searchString) {\n\t\tvar searchValues = searchString.split(' ');\n\t\tvar filtered = items;\n\t\tfor(var i in searchValues) {\n\t\t\tfiltered = $filter('filter')(filtered, searchValues[i]);\n\t\t}\n\t\treturn filtered;\n\t};\n}]);\n","app.filter('categoryTitle', function () {\n\t'use strict';\n\treturn function (str) {\n\t\tif (str && (typeof str === 'string')) {\n\t\t\treturn str.replace(/\\//g, ' / ');\n\t\t} else {\n\t\t\treturn '';\n\t\t}\n\t};\n});\n","/**\n * group notes by (sub) category\n */\napp.filter('groupNotes', function () {\n\t'use strict';\n\treturn function (items, category) {\n\t\tif(category) {\n\t\t\tvar prevCat = null;\n\t\t\tfor(var i=0; i\n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('is', function () {\n 'use strict';\n\n return {\n loading: false\n };\n});","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\n// take care of fileconflicts by appending a number\napp.factory('NotesModel', function () {\n 'use strict';\n\n var NotesModel = function () {\n this.notes = [];\n this.notesIds = {};\n };\n\n NotesModel.prototype = {\n addAll: function (notes) {\n for(var i=0; i0) {\n var index = this.nthIndexOf(cat, '/', maxLevel);\n if(index>0) {\n cat = cat.substring(0, index);\n }\n }\n if(categories[cat]===undefined) {\n categories[cat] = 1;\n } else {\n categories[cat] += 1;\n }\n }\n var result = [];\n for(var category in categories) {\n if(details) {\n result.push({\n name: category,\n count: categories[category],\n });\n } else if(category) {\n result.push(category);\n }\n }\n if(!details) {\n result.sort();\n }\n return result;\n },\n\n };\n\n return new NotesModel();\n});\n","/**\n * Copyright (c) 2013, Bernhard Posselt \n * This file is licensed under the Affero General Public License version 3 or\n * later.\n * See the COPYING file.\n */\n\napp.factory('SaveQueue', function($q) {\n 'use strict';\n\n var SaveQueue = function () {\n this._queue = {};\n this._flushLock = false;\n this._manualSaveActive = false;\n };\n\n SaveQueue.prototype = {\n add: function (note) {\n this._queue[note.id] = note;\n this._flush();\n },\n addManual: function (note) {\n this._manualSaveActive = true;\n this.add(note);\n },\n _flush: function () {\n // if there are no changes dont execute the requests\n var keys = Object.keys(this._queue);\n if(keys.length === 0 || this._flushLock) {\n return;\n } else {\n this._flushLock = true;\n }\n\n var self = this;\n var requests = [];\n\n // iterate over updated objects and run an update request for\n // each one of them\n for(var i=0; i