From 4391ead7e8ad19887c37da9fa9c2ff9784c4e28d Mon Sep 17 00:00:00 2001 From: korelstar Date: Sat, 23 Jun 2018 20:53:19 +0200 Subject: [PATCH] show category selector in navigation --- controller/notescontroller.php | 4 +- css/notes.css | 29 +++++++++ img/folder-empty.svg | 1 + img/folder.svg | 1 + img/recent.svg | 1 + img/star.svg | 1 + js/app/controllers/notescontroller.js | 39 ++++++++++++- js/app/filters/categoryTitle.js | 10 ++++ js/app/filters/groupNotes.js | 26 +++++++++ js/app/services/notesmodel.js | 84 ++++++++++++++------------- js/public/app.min.js | 2 +- js/public/app.min.js.map | 2 +- templates/main.php | 59 +++++++++++++++++-- templates/note.php | 2 +- 14 files changed, 209 insertions(+), 52 deletions(-) create mode 100644 img/folder-empty.svg create mode 100644 img/folder.svg create mode 100644 img/recent.svg create mode 100644 img/star.svg create mode 100644 js/app/filters/categoryTitle.js create mode 100644 js/app/filters/groupNotes.js diff --git a/controller/notescontroller.php b/controller/notescontroller.php index 4bbba76c7..3aa46cdcc 100644 --- a/controller/notescontroller.php +++ b/controller/notescontroller.php @@ -82,10 +82,10 @@ public function get($id) { * * @param string $content */ - public function create($content="") { + public function create($content='', $category=null) { $note = $this->notesService->create($this->userId); $note = $this->notesService->update( - $note->getId(), $content, $this->userId + $note->getId(), $content, $this->userId, $category ); return new DataResponse($note); } diff --git a/css/notes.css b/css/notes.css index 8de9cfbb3..5fe88eae3 100644 --- a/css/notes.css +++ b/css/notes.css @@ -220,6 +220,7 @@ form.category .icon-confirm { } .ui-autocomplete.category-autocomplete { z-index: 5000 !important; + position: fixed; max-height: 200px; overflow-y: auto; overflow-x: hidden; @@ -348,3 +349,31 @@ form.category .icon-confirm { padding-left: 90px; } } + + +/* icons for sidebar */ +.nav-icon-emptyfolder { + background-image: url('../img/folder-empty.svg?v=1'); +} +.nav-icon-files { + background-image: url('../img/folder.svg?v=1'); +} +.nav-icon-recent { + background-image: url('../img/recent.svg?v=1'); +} +.nav-icon-favorites { + background-image: url('../img/star.svg?v=1'); +} + + + +.separator-below { + border-bottom: 1px solid var(--color-border); +} +.separator-above { + border-top: 1px solid var(--color-border); +} +.current-category-item > a, +a.current-category-item { + font-weight: bold; +} diff --git a/img/folder-empty.svg b/img/folder-empty.svg new file mode 100644 index 000000000..018072773 --- /dev/null +++ b/img/folder-empty.svg @@ -0,0 +1 @@ + diff --git a/img/folder.svg b/img/folder.svg new file mode 100644 index 000000000..7c2779153 --- /dev/null +++ b/img/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/recent.svg b/img/recent.svg new file mode 100644 index 000000000..cb681fb70 --- /dev/null +++ b/img/recent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/star.svg b/img/star.svg new file mode 100644 index 000000000..949850f6c --- /dev/null +++ b/img/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/app/controllers/notescontroller.js b/js/app/controllers/notescontroller.js index ac0191e18..5846933c2 100644 --- a/js/app/controllers/notescontroller.js +++ b/js/app/controllers/notescontroller.js @@ -14,6 +14,13 @@ app.controller('NotesController', function($routeParams, $scope, $location, $scope.notesLoaded = false; $scope.notes = NotesModel.getAll(); + $scope.folderSelectorOpen = false; + $scope.filterCategory = null; + + $scope.orderRecent = ['-favorite','-modified']; + $scope.orderAlpha = ['category','-favorite','title']; + $scope.filterOrder = $scope.orderRecent; + var notesResource = Restangular.all('notes'); // initial request for getting all notes @@ -23,7 +30,8 @@ app.controller('NotesController', function($routeParams, $scope, $location, }); $scope.create = function () { - notesResource.post().then(function (note) { + notesResource.post({category: $scope.filterCategory}) + .then(function (note) { NotesModel.add(note); $location.path('/notes/' + note.id); }); @@ -46,6 +54,35 @@ app.controller('NotesController', function($routeParams, $scope, $location, event.target.blur(); }; + $scope.getCategories = _.memoize(function (notes) { + return NotesModel.getCategories(notes, 1, true); + }); + + $scope.toggleFolderSelector = function () { + $scope.folderSelectorOpen = !$scope.folderSelectorOpen; + }; + + $scope.setFilter = function (category) { + if(category===null) { + $scope.filterOrder = $scope.orderRecent; + } else { + $scope.filterOrder = $scope.orderAlpha; + } + $scope.filterCategory = category; + $scope.folderSelectorOpen = false; + $('#app-navigation > ul').animate({scrollTop: 0}, 'fast'); + }; + + $scope.categoryFilter = function (note) { + if($scope.filterCategory!==null) { + if(note.category===$scope.filterCategory) { + return true; + } else if(note.category!==null) { + return note.category.startsWith($scope.filterCategory+'/'); + } + } + return true; + }; $window.onbeforeunload = function() { var notes = NotesModel.getAll(); diff --git a/js/app/filters/categoryTitle.js b/js/app/filters/categoryTitle.js new file mode 100644 index 000000000..11eccc455 --- /dev/null +++ b/js/app/filters/categoryTitle.js @@ -0,0 +1,10 @@ +app.filter('categoryTitle', function () { + 'use strict'; + return function (str) { + if (str && (typeof str === 'string')) { + return str.replace(/\//g, ' / '); + } else { + return ''; + } + }; +}); diff --git a/js/app/filters/groupNotes.js b/js/app/filters/groupNotes.js new file mode 100644 index 000000000..8fa2059af --- /dev/null +++ b/js/app/filters/groupNotes.js @@ -0,0 +1,26 @@ +/** + * group notes by (sub) category + */ +app.filter('groupNotes', ['$filter', function () { + 'use strict'; + return _.memoize(function (notes, category) { + if(category) { + var items = []; + var prevCat = null; + for(var i=0; i0) { - var index = this.nthIndexOf(cat, '/', maxLevel); - if(index>0) { - cat = cat.substring(0, index); + nthIndexOf: function(str, pattern, n) { + var i = -1; + while (n-- && i++ < str.length) { + i = str.indexOf(pattern, i); + if (i < 0) { + break; } - } - if(categories[cat]===undefined) { - categories[cat] = 1; - } else { - categories[cat] += 1; - } - } - var result = []; - for(var category in categories) { - if(details) { - result.push({ name: category, count: categories[category]}); - } else if(category) { - result.push(category); - } - } - if(!details) { - result.sort(); - } - return result; - }), + } + return i; + }, + + getCategories: function (notes, maxLevel, details) { + var categories = {}; + for(var i=0; i0) { + var index = this.nthIndexOf(cat, '/', maxLevel); + if(index>0) { + cat = cat.substring(0, index); + } + } + if(categories[cat]===undefined) { + categories[cat] = 1; + } else { + categories[cat] += 1; + } + } + var result = []; + for(var category in categories) { + if(details) { + result.push({ + name: category, + count: categories[category], + }); + } else if(category) { + result.push(category); + } + } + if(!details) { + result.sort(); + } + return result; + }, }; diff --git a/js/public/app.min.js b/js/public/app.min.js index c594268be..be2dced2b 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=u(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(){c(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"),c(function(){n("#category").focus(),n("#category").autocomplete("search","")})},o.closeCategory=function(){o.editCategory=!1;var e=n("#category").val();o.note.category!==e&&(o.note.category=e,o.note.unsaved=!0,o.autoSave(o.note))},s.unbind("keypress.notes.save"),s.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,n,o,r,i,a){n.route=e,n.notesLoaded=!1,n.notes=i.getAll();var u=r.all("notes");u.getList().then(function(e){i.addAll(e),n.notesLoaded=!0}),n.create=function(){u.post().then(function(e){i.add(e),o.path("/notes/"+e.id)})},n["delete"]=function(e){var t=i.get(e);t.remove().then(function(){i.remove(e),n.$emit("$routeChangeError")})},n.toggleFavorite=function(e,t){var n=i.get(e);n.customPUT({favorite:!n.favorite},"favorite",{},{}).then(function(e){n.favorite=!!e}),t.target.blur()},a.onbeforeunload=function(){for(var e=i.getAll(),n=0;n0){var u=this.nthIndexOf(a,"/",t);u>0&&(a=a.substring(0,u))}o[a]===i?o[a]=1:o[a]+=1}var s=[];for(var c in o)n?s.push({name:c,count:o[c]}):c&&s.push(c);return n||s.sort(),s})},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.onEdit=function(){var e=o.note;e.unsaved=!0,o.autoSave(e)},o.autoSave=u(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(){c(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"),c(function(){n("#category").focus(),n("#category").autocomplete("search","")})},o.closeCategory=function(){o.editCategory=!1;var e=n("#category").val();o.note.category!==e&&(o.note.category=e,o.note.unsaved=!0,o.autoSave(o.note))},l.unbind("keypress.notes.save"),l.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,u){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 l=i.all("notes");l.getList().then(function(e){a.addAll(e),o.notesLoaded=!0}),o.create=function(){l.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.getCategories=_.memoize(function(e){return a.getCategories(e,1,!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},u.onbeforeunload=function(){for(var e=a.getAll(),n=0;n0){var u=this.nthIndexOf(a,"/",t);u>0&&(a=a.substring(0,u))}o[a]===i?o[a]=1:o[a]+=1}var l=[];for(var c in o)n?l.push({name:c,count:o[c]}):c&&l.push(c);return n||l.sort(),l}},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, useSearchAPI) {\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 if(useSearchAPI) {\n $scope.initSearch();\n }\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.closeCategory = function() {\n $scope.editCategory = false;\n var category = $('#category').val();\n if($scope.note.category !== category) {\n $scope.note.category = category;\n $scope.note.unsaved = true;\n $scope.autoSave($scope.note);\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 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().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\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, useSearchAPI) {\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 if(useSearchAPI) {\n $scope.initSearch();\n }\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.closeCategory = function() {\n $scope.editCategory = false;\n var category = $('#category').val();\n if($scope.note.category !== category) {\n $scope.note.category = category;\n $scope.note.unsaved = true;\n $scope.autoSave($scope.note);\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 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().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\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('wordCount', function () {\n\t'use strict';\n\treturn function (value) {\n\t\tif (value && (typeof value === 'string')) {\n\t\t\tvar wordCount = value.split(/\\s+/).filter(\n\t\t\t\t// only count words containing\n\t\t\t\t// at least one alphanumeric character\n\t\t\t\tfunction(value) {\n\t\t\t\t\treturn value.search(/[A-Za-z0-9]/) !== -1;\n\t\t\t\t}\n\t\t\t).length;\n\t\t\treturn window.n('notes', '%n word', '%n words', wordCount);\n\t\t} else {\n\t\t\treturn 0;\n\t\t}\n\t};\n});\n\n/**\n * Copyright (c) 2016, Hendrik Leppelsack\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('debounce', ['$timeout', function($timeout) {\n\t'use strict';\n\n\treturn function debounce(func, delay) {\n\t\tvar timeout;\n\n\t\treturn function() {\n\t\t\tvar context = this, args = arguments;\n\n\t\t\tif(timeout) {\n\t\t\t\t$timeout.cancel(timeout);\n\t\t\t}\n\t\t\ttimeout = $timeout(function() {\n\t\t\t\tfunc.apply(context, args);\n\t\t\t}, delay);\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.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({ name: category, count: categories[category]});\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('wordCount', function () {\n\t'use strict';\n\treturn function (value) {\n\t\tif (value && (typeof value === 'string')) {\n\t\t\tvar wordCount = value.split(/\\s+/).filter(\n\t\t\t\t// only count words containing\n\t\t\t\t// at least one alphanumeric character\n\t\t\t\tfunction(value) {\n\t\t\t\t\treturn value.search(/[A-Za-z0-9]/) !== -1;\n\t\t\t\t}\n\t\t\t).length;\n\t\t\treturn window.n('notes', '%n word', '%n words', wordCount);\n\t\t} else {\n\t\t\treturn 0;\n\t\t}\n\t};\n});\n","/**\n * Copyright (c) 2016, Hendrik Leppelsack\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('debounce', ['$timeout', function($timeout) {\n\t'use strict';\n\n\treturn function debounce(func, delay) {\n\t\tvar timeout;\n\n\t\treturn function() {\n\t\t\tvar context = this, args = arguments;\n\n\t\t\tif(timeout) {\n\t\t\t\t$timeout.cancel(timeout);\n\t\t\t}\n\t\t\ttimeout = $timeout(function() {\n\t\t\t\tfunc.apply(context, args);\n\t\t\t}, delay);\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.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({ name: category, count: categories[category]});\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, useSearchAPI) {\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 if(useSearchAPI) {\n $scope.initSearch();\n }\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.closeCategory = function() {\n $scope.editCategory = false;\n var category = $('#category').val();\n if($scope.note.category !== category) {\n $scope.note.category = category;\n $scope.note.unsaved = true;\n $scope.autoSave($scope.note);\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.getCategories = _.memoize(function (notes) {\n return NotesModel.getCategories(notes, 1, true);\n });\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 $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, useSearchAPI) {\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 if(useSearchAPI) {\n $scope.initSearch();\n }\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.closeCategory = function() {\n $scope.editCategory = false;\n var category = $('#category').val();\n if($scope.note.category !== category) {\n $scope.note.category = category;\n $scope.note.unsaved = true;\n $scope.autoSave($scope.note);\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.getCategories = _.memoize(function (notes) {\n return NotesModel.getCategories(notes, 1, true);\n });\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 $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', ['$filter', function () {\n\t'use strict';\n\treturn _.memoize(function (notes, category) {\n\t\tif(category) {\n\t\t\tvar items = [];\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', ['$filter', function () {\n\t'use strict';\n\treturn _.memoize(function (notes, category) {\n\t\tif(category) {\n\t\t\tvar items = [];\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 -