mirror of https://github.com/nextcloud/bookmarks
Initial pass at a vue rewrite
This commit is contained in:
parent
c6767cf077
commit
0fbe1e82e8
|
@ -0,0 +1,92 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
node: true,
|
||||
jest: true
|
||||
},
|
||||
globals: {
|
||||
t: true,
|
||||
n: true,
|
||||
OC: true,
|
||||
OCA: true,
|
||||
Vue: true,
|
||||
VueRouter: true
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
ecmaVersion: 6
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings',
|
||||
'plugin:node/recommended',
|
||||
'plugin:vue/essential',
|
||||
//'plugin:vue/recommended',
|
||||
'standard'
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
webpack: {
|
||||
config: 'webpack.common.js'
|
||||
},
|
||||
node: {
|
||||
paths: ['src'],
|
||||
extensions: ['.js', '.vue']
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: ['vue', 'node'],
|
||||
rules: {
|
||||
semi: ['error', 'always'],
|
||||
// space before function ()
|
||||
'space-before-function-paren': ['error', 'never'],
|
||||
// curly braces always space
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
// stay consistent with array brackets
|
||||
'array-bracket-newline': ['error', 'consistent'],
|
||||
// 1tbs brace style
|
||||
'brace-style': 'error',
|
||||
// tabs only
|
||||
indent: ['error', 'tab'],
|
||||
'no-tabs': 0,
|
||||
//'vue/html-indent': ['error', 'tab'],
|
||||
// only debug console
|
||||
'no-console': ['error', { allow: ['error', 'warn', 'info', 'debug'] }],
|
||||
// classes blocks
|
||||
'padded-blocks': ['error', { classes: 'always' }],
|
||||
// always have the operator in front
|
||||
'operator-linebreak': ['error', 'before'],
|
||||
// ternary on multiline
|
||||
'multiline-ternary': ['error', 'always-multiline'],
|
||||
// force proper JSDocs
|
||||
'valid-jsdoc': [
|
||||
2,
|
||||
{
|
||||
prefer: {
|
||||
return: 'returns'
|
||||
},
|
||||
requireReturn: false,
|
||||
requireReturnDescription: false
|
||||
}
|
||||
],
|
||||
// es6 import/export and require
|
||||
'node/no-unpublished-require': ['off'],
|
||||
'node/no-unsupported-features/es-syntax': ['off'],
|
||||
// kebab case components for vuejs
|
||||
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
|
||||
// code spacing with attributes
|
||||
'vue/max-attributes-per-line': [
|
||||
'error',
|
||||
{
|
||||
singleline: 3,
|
||||
multiline: {
|
||||
max: 3,
|
||||
allowFirstLine: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"useTabs": true
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
module.exports = {
|
||||
extends: 'stylelint-config-recommended-scss',
|
||||
rules: {
|
||||
indentation: 'tab',
|
||||
'selector-type-no-unknown': null,
|
||||
'number-leading-zero': null,
|
||||
'rule-empty-line-before': [
|
||||
'always',
|
||||
{
|
||||
ignore: ['after-comment', 'inside-block']
|
||||
}
|
||||
],
|
||||
'declaration-empty-line-before': [
|
||||
'never',
|
||||
{
|
||||
ignore: ['after-declaration']
|
||||
}
|
||||
],
|
||||
'comment-empty-line-before': null,
|
||||
'selector-type-case': null,
|
||||
'selector-list-comma-newline-after': null,
|
||||
'no-descending-specificity': null,
|
||||
'string-quotes': 'single'
|
||||
},
|
||||
plugins: ['stylelint-scss']
|
||||
}
|
36
js/admin.js
36
js/admin.js
|
@ -1,36 +0,0 @@
|
|||
(function(window, OCP, $) {
|
||||
[
|
||||
{
|
||||
el: '#bookmarks_previews_screenly_token',
|
||||
setting: 'previews.screenly.token'
|
||||
},
|
||||
{
|
||||
el: '#bookmarks_previews_screenly_url',
|
||||
setting: 'previews.screenly.url'
|
||||
}
|
||||
].forEach(function(entry) {
|
||||
var $el = $(entry.el);
|
||||
var $statusSuccess = $(entry.el + ' ~ .success-status');
|
||||
var $statusError = $(entry.el + ' ~ .error-status');
|
||||
|
||||
$statusSuccess.hide();
|
||||
$statusError.hide();
|
||||
|
||||
$el.on('change', function() {
|
||||
OCP.AppConfig.setValue('bookmarks', entry.setting, $el.val(), {
|
||||
success: function() {
|
||||
$statusSuccess.show();
|
||||
setTimeout(function() {
|
||||
$statusSuccess.fadeOut();
|
||||
}, 3000);
|
||||
},
|
||||
error: function() {
|
||||
$statusError.show();
|
||||
setTimeout(function() {
|
||||
$statusError.fadeOut();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})(window, OCP, $);
|
|
@ -1,19 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import Tags from '../models/Tags';
|
||||
import BookmarkletView from '../views/Bookmarklet';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
|
||||
export default Marionette.Application.extend({
|
||||
region: '#bookmarklet_form',
|
||||
onBeforeStart: function() {
|
||||
var that = this;
|
||||
this.tags = new Tags;
|
||||
this.tags.fetch({
|
||||
data: {count: true},
|
||||
});
|
||||
},
|
||||
onStart: function() {
|
||||
this.showView(new BookmarkletView({app: this}));
|
||||
},
|
||||
});
|
|
@ -1,73 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import Bookmarks from '../models/Bookmarks';
|
||||
import Folder from '../models/Folder';
|
||||
import Folders from '../models/Folders';
|
||||
import Tag from '../models/Tag';
|
||||
import Tags from '../models/Tags';
|
||||
import Settings from '../models/Settings';
|
||||
import Router from './MainRouter';
|
||||
import AppView from '../views/App';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
|
||||
export default Marionette.Application.extend({
|
||||
onBeforeStart: function() {
|
||||
var that = this;
|
||||
this.bookmarks = new Bookmarks();
|
||||
this.settings = new Settings();
|
||||
this.folders = new Folders();
|
||||
this.folders.fetch({
|
||||
reset: true
|
||||
});
|
||||
this.folders.once('reset', function() {
|
||||
setTimeout(function() {
|
||||
Backbone.history.start();
|
||||
}, 100);
|
||||
});
|
||||
this.tags = new Tags();
|
||||
this.tags.fetch({
|
||||
reset: true,
|
||||
data: { count: true },
|
||||
success: function() {
|
||||
// we sadly cannot listen ot 'sync', which would fire after fetching, so we have to listen to these and add some timeout
|
||||
that.listenTo(that.tags, 'sync add remove', that.onTagChanged);
|
||||
}
|
||||
});
|
||||
this.listenTo(this.bookmarks, 'sync', this.onBookmarkTagsChanged);
|
||||
this.listenTo(this.settings, 'change:sorting', this.onSortingChanged);
|
||||
this.router = new Router({ app: this });
|
||||
},
|
||||
onStart: function() {
|
||||
this.view = new AppView({ app: this });
|
||||
this.view.render();
|
||||
},
|
||||
onTagChanged: function(tag) {
|
||||
var that = this;
|
||||
if (!(tag instanceof Tag)) return; // we can also receive 'sync' events from the collection, which we don't want here
|
||||
if (this.bookmarkChanged) return (this.bookmarkChanged = false); // set to true by onBookmarkTagsChanged
|
||||
this.tagChanged = true;
|
||||
|
||||
// we need to wait 'till the tag change has been acknowledged by the server
|
||||
setTimeout(function() {
|
||||
that.bookmarks
|
||||
.filter(function(bm) {
|
||||
return bm.get('tags').some(function(t) {
|
||||
return t === tag.get('name') || t === tag.previous('name');
|
||||
});
|
||||
})
|
||||
.forEach(function(bm) {
|
||||
bm.fetch();
|
||||
});
|
||||
}, 100);
|
||||
},
|
||||
onBookmarkTagsChanged: function() {
|
||||
var that = this;
|
||||
if (this.tagChanged === true) return (this.tagChanged = false);
|
||||
this.bokmarkChanged = true;
|
||||
that.tags.fetch({ data: { count: true } }); // we listen to 'sync', so we can fetch immediately
|
||||
},
|
||||
onSortingChanged: function() {
|
||||
this.bookmarks.setSortBy(this.settings.get('sorting'));
|
||||
this.bookmarks.fetchPage();
|
||||
}
|
||||
});
|
|
@ -1,64 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.AppRouter.extend({
|
||||
controller: {
|
||||
index: function() {
|
||||
setTimeout(function() {
|
||||
Backbone.history.navigate('all', { trigger: true });
|
||||
}, 1);
|
||||
},
|
||||
all: function() {
|
||||
this.app.bookmarks.setFetchQuery({});
|
||||
this.app.bookmarks.fetchPage();
|
||||
Radio.channel('nav').trigger('navigate', 'all');
|
||||
},
|
||||
favorites: function() {
|
||||
Radio.channel('nav').trigger('navigate', 'favorites');
|
||||
},
|
||||
shared: function() {
|
||||
this.app.bookmarks.setFetchQuery({});
|
||||
this.app.bookmarks.fetchPage();
|
||||
Radio.channel('nav').trigger('navigate', 'shared');
|
||||
},
|
||||
tags: function(tagString) {
|
||||
var tags = tagString ? tagString.split(',') : [];
|
||||
this.app.bookmarks.setFetchQuery({ tags: tags, conjunction: 'and' });
|
||||
this.app.bookmarks.fetchPage();
|
||||
Radio.channel('nav').trigger('navigate', 'tags', tags);
|
||||
},
|
||||
folder: function(folderId) {
|
||||
this.app.bookmarks.setFetchQuery({ folder: folderId });
|
||||
this.app.bookmarks.fetchPage();
|
||||
Radio.channel('nav').trigger('navigate', 'folder', folderId);
|
||||
},
|
||||
search: function(query) {
|
||||
this.app.bookmarks.setFetchQuery({
|
||||
search: decodeURIComponent(query).split(' '),
|
||||
conjunction: 'and'
|
||||
});
|
||||
this.app.bookmarks.fetchPage();
|
||||
Radio.channel('nav').trigger('navigate', 'search', query);
|
||||
},
|
||||
untagged: function() {
|
||||
this.app.bookmarks.setFetchQuery({ untagged: true });
|
||||
this.app.bookmarks.fetchPage();
|
||||
Radio.channel('nav').trigger('navigate', 'untagged');
|
||||
}
|
||||
},
|
||||
appRoutes: {
|
||||
'': 'index',
|
||||
all: 'all',
|
||||
favorites: 'favorites',
|
||||
shared: 'shared',
|
||||
'tags(/*tags)': 'tags',
|
||||
'folder/:folderId': 'folder',
|
||||
'search/:query': 'search',
|
||||
untagged: 'untagged'
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.controller.app = options.app;
|
||||
}
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Marionette from 'backbone.marionette';
|
||||
import select2 from 'select2';
|
||||
import App from './apps/Bookmarklet';
|
||||
import fixBackboneSync from './utils/FixBackboneSync';
|
||||
|
||||
// init
|
||||
|
||||
var app = new App();
|
||||
$(function() {
|
||||
app.start();
|
||||
});
|
13
js/main.js
13
js/main.js
|
@ -1,13 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import Marionette from 'backbone.marionette';
|
||||
import select2 from 'select2';
|
||||
import App from './apps/Main';
|
||||
import fixBackboneSync from './utils/FixBackboneSync';
|
||||
import fixInteract from './utils/FixInteract';
|
||||
|
||||
// init
|
||||
|
||||
var app = new App();
|
||||
$(function() {
|
||||
app.start();
|
||||
});
|
|
@ -1,25 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import $ from 'jquery';
|
||||
|
||||
export default Backbone.Model.extend({
|
||||
urlRoot: 'bookmark',
|
||||
parse: function(json) {
|
||||
if (json.item) {
|
||||
return json.item;
|
||||
}
|
||||
return json;
|
||||
},
|
||||
clickLink: function() {
|
||||
const url = encodeURIComponent(this.get('url'));
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: 'bookmark/click?url=' + url,
|
||||
headers: {
|
||||
requesttoken: oc_requesttoken
|
||||
}
|
||||
});
|
||||
},
|
||||
getColor: function() {
|
||||
return '#666';
|
||||
}
|
||||
});
|
|
@ -1,90 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Bookmark from './Bookmark';
|
||||
|
||||
const BATCH_SIZE = 34; // roughly two screens
|
||||
|
||||
export default Backbone.Collection.extend({
|
||||
model: Bookmark,
|
||||
url: 'bookmark',
|
||||
parse: function(json) {
|
||||
return json.data;
|
||||
},
|
||||
initialize: function() {
|
||||
this.loadingState = new Backbone.Model({
|
||||
page: 0,
|
||||
query: {},
|
||||
fetching: false,
|
||||
reachedEnd: false
|
||||
});
|
||||
},
|
||||
setFetchQuery: function(data) {
|
||||
this.loadingState.set({
|
||||
page: 0,
|
||||
query: data,
|
||||
fetching: false,
|
||||
reachedEnd: false
|
||||
});
|
||||
this.abortCurrentRequest();
|
||||
},
|
||||
setSortBy: function(sortby) {
|
||||
this.sortby = sortby;
|
||||
this.loadingState.set({ page: 0, reachedEnd: false });
|
||||
this.abortCurrentRequest();
|
||||
},
|
||||
abortCurrentRequest: function() {
|
||||
if (this.currentRequest) {
|
||||
this.currentRequest.abort();
|
||||
}
|
||||
if (this.spinnerTimeout) {
|
||||
clearTimeout(this.spinnerTimeout);
|
||||
}
|
||||
if (this.secondPageTimeout) {
|
||||
clearTimeout(this.secondPageTimeout);
|
||||
}
|
||||
},
|
||||
fetchPage: function() {
|
||||
var that = this;
|
||||
if (this.loadingState.get('reachedEnd')) {
|
||||
return;
|
||||
}
|
||||
const nextPage = this.loadingState.get('page');
|
||||
const firstPage = nextPage === 0;
|
||||
if (!firstPage && this.loadingState.get('fetching') === true) {
|
||||
return;
|
||||
}
|
||||
var sortby = this.sortby;
|
||||
this.loadingState.set({ page: nextPage + 1, fetching: true });
|
||||
|
||||
// Show spinner after 1.5s if we're fetching a new query
|
||||
this.abortCurrentRequest();
|
||||
this.spinnerTimeout = setTimeout(() => {
|
||||
firstPage && this.reset();
|
||||
}, 1500);
|
||||
|
||||
const currentQuery = this.loadingState.get('query');
|
||||
|
||||
this.currentRequest = this.fetch({
|
||||
data: _.extend({}, this.loadingState.get('query'), {
|
||||
page: nextPage,
|
||||
limit: BATCH_SIZE,
|
||||
sortby: sortby
|
||||
}),
|
||||
reset: firstPage,
|
||||
remove: false,
|
||||
success: function(collections, response) {
|
||||
clearTimeout(that.spinnerTimeout);
|
||||
let reachedEnd = response.data.length < BATCH_SIZE;
|
||||
that.loadingState.set({
|
||||
fetching: false,
|
||||
reachedEnd: reachedEnd
|
||||
});
|
||||
if (!reachedEnd && nextPage % 2 == 0) {
|
||||
that.secondPageTimeout = setTimeout(function() {
|
||||
that.fetchPage();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import { Folder } from './Folders';
|
||||
|
||||
export default Folder;
|
|
@ -1,48 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
|
||||
export var Folder = Backbone.Model.extend({
|
||||
urlRoot: 'folder',
|
||||
initialize: function() {
|
||||
this.listenTo(
|
||||
this.get('children'),
|
||||
'change',
|
||||
this.trigger.bind(this, 'change')
|
||||
);
|
||||
},
|
||||
parse: function(obj) {
|
||||
return obj.item
|
||||
? Object.assign(obj.item, {
|
||||
children: new Folders(obj.item.children, { parse: true })
|
||||
})
|
||||
: Object.assign(obj, {
|
||||
children: new Folders(obj.children, { parse: true })
|
||||
});
|
||||
},
|
||||
contains: function(id) {
|
||||
return this.get('children').contains(id);
|
||||
}
|
||||
});
|
||||
|
||||
var Folders = Backbone.Collection.extend({
|
||||
model: Folder,
|
||||
comparator: 'title',
|
||||
url: 'folder',
|
||||
parse: function(obj) {
|
||||
var list = obj.data ? obj.data : obj;
|
||||
return list.map(function(attributes) {
|
||||
return new Folder(attributes, { parse: true });
|
||||
});
|
||||
},
|
||||
contains: function(id) {
|
||||
if (~this.pluck('id').indexOf(id)) return true;
|
||||
if (
|
||||
this.some(function(folder) {
|
||||
return folder.contains(id);
|
||||
})
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
export default Folders;
|
|
@ -1,46 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import $ from 'jquery';
|
||||
|
||||
export default Backbone.Model.extend({
|
||||
urlRoot: 'settings',
|
||||
initialize: function() {
|
||||
this.fetch({
|
||||
url: 'settings/sort'
|
||||
});
|
||||
this.fetch({
|
||||
url: 'settings/view'
|
||||
});
|
||||
},
|
||||
setSorting: function(sorting) {
|
||||
var that = this;
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: 'settings/sort',
|
||||
headers: {
|
||||
requesttoken: oc_requesttoken
|
||||
},
|
||||
data: {
|
||||
sorting: sorting
|
||||
},
|
||||
success: function() {
|
||||
that.set({ sorting: sorting });
|
||||
}
|
||||
});
|
||||
},
|
||||
setViewMode: function(viewMode) {
|
||||
var that = this;
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
url: 'settings/view',
|
||||
headers: {
|
||||
requesttoken: oc_requesttoken
|
||||
},
|
||||
data: {
|
||||
viewMode: viewMode
|
||||
},
|
||||
success: function() {
|
||||
that.set({ viewMode: viewMode });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
|
||||
export default Backbone.Model.extend({
|
||||
idAttribute: 'name',
|
||||
urlRoot: 'tag'
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import Tag from './Tag';
|
||||
|
||||
var Tags = Backbone.Collection.extend({
|
||||
model: Tag,
|
||||
comparator: function(t) {return -t.get('count');},
|
||||
url: 'tag',
|
||||
parse: function(json) {
|
||||
return json;
|
||||
}
|
||||
});
|
||||
|
||||
export default Tags;
|
|
@ -1,15 +0,0 @@
|
|||
<li class="link">
|
||||
<a href="#" class="icon-add"><%- t('bookmarks', 'Add a Bookmark') %></a>
|
||||
</li>
|
||||
<li class="form" style="display: none">
|
||||
<input
|
||||
type="text"
|
||||
value=""
|
||||
placeholder="<%- t('bookmarks', 'Address') %>"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
title="<%- t('bookmarks', 'Add this to my bookmarks') %>"
|
||||
class="icon-add"
|
||||
></button>
|
||||
</li>
|
|
@ -1,8 +0,0 @@
|
|||
<a href="#" class="icon-add"><%- t('bookmarks', 'Add folder') %></a>
|
||||
<div class="app-navigation-entry-edit">
|
||||
<div>
|
||||
<input type="text" value="" class="title" placeholder="<%- t('bookmarks', 'Title') %>">
|
||||
<input type="submit" value="" class="icon-close action cancel">
|
||||
<input type="submit" value="" class="icon-checkmark action submit">
|
||||
</div>
|
||||
</div>
|
|
@ -1,59 +0,0 @@
|
|||
<input
|
||||
type="checkbox"
|
||||
name="select"
|
||||
class="checkbox select-mode-checkbox"
|
||||
/><label for="select"></label>
|
||||
<div class="panel">
|
||||
<a target="_blank" draggable="false" href="<%- url %>">
|
||||
<h2
|
||||
title="<%- t('bookmarks', 'Open website in new tab') %>"
|
||||
style="background-image: url('bookmark/<%- id %>/favicon');"
|
||||
class="with-favicon"
|
||||
>
|
||||
<%- title %>
|
||||
</h2></a
|
||||
>
|
||||
<button
|
||||
class="toggle-actions icon-more"
|
||||
title="<%- t('bookmarks', 'Actions') %>"
|
||||
></button>
|
||||
<div class="popovermenu closed">
|
||||
<ul>
|
||||
<li>
|
||||
<button class="menu-item-checkbox menu-filter-add">
|
||||
<span
|
||||
><input type="checkbox" name="select" class="checkbox"/><label
|
||||
for="select"
|
||||
></label
|
||||
></span>
|
||||
<span><%- t('bookmarks', 'Select') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-item-checkbox menu-filter-remove">
|
||||
<span
|
||||
><input
|
||||
type="checkbox"
|
||||
name="select"
|
||||
checked
|
||||
class="checkbox"/><label for="select"></label
|
||||
></span>
|
||||
<span><%- t('bookmarks', 'Deselect') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-details">
|
||||
<span class="icon-rename"></span>
|
||||
<span><%- t('bookmarks', 'Details') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-delete">
|
||||
<span class="icon-delete"></span>
|
||||
<span><%- t('bookmarks', 'Delete') %></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tags"></div>
|
|
@ -1,37 +0,0 @@
|
|||
<div class="close icon-close" title="<%- t('bookmarks', 'Close') %>"></div>
|
||||
<div class="status">
|
||||
<span class="message message-saving"><span class="icon-loading"></span></span>
|
||||
<span class="message message-saved"
|
||||
><span class="icon-checkmark"></span> <%- t('bookmarks', 'Saved') %></span
|
||||
>
|
||||
</div>
|
||||
<div class="preview"></div>
|
||||
<h1
|
||||
data-attribute="title"
|
||||
class="<%- !title.trim()? 'empty' : '' %>"
|
||||
title="<%- t('bookmarks', 'Click to edit title') %>"
|
||||
style="background-image: url('bookmark/<%- id %>/favicon');"
|
||||
>
|
||||
<%- title.trim()? title : t('bookmarks', 'Add a title...') %>
|
||||
</h1>
|
||||
<h3><%- t('bookmarks', 'Date added') %></h3>
|
||||
<span class="icon-calendar-dark" style="display: inline-block"></span> <%- new
|
||||
Date(Number(added)*1000).toLocaleDateString() %>
|
||||
<h3><%- t('bookmarks', 'Link') %></h3>
|
||||
<h2 data-attribute="url">
|
||||
<a target="_blank" href="<%- url %>"
|
||||
><span class="icon-external"></span><%- url %></a
|
||||
>
|
||||
<a href="#" class="edit"><span class="icon-rename"></span></a>
|
||||
</h2>
|
||||
<h3><%- t('bookmarks', 'Tags') %></h3>
|
||||
<div class="tags"></div>
|
||||
<h3><%- t('bookmarks', 'Description') %></h3>
|
||||
<div
|
||||
class="description <%- !description.trim()? 'empty' : '' %>"
|
||||
data-attribute="description"
|
||||
title="<%- t('bookmarks', 'Click to edit description') %>"
|
||||
>
|
||||
<%- description.trim()? description : t('bookmarks', 'Add a description...')
|
||||
%>
|
||||
</div>
|
|
@ -1,8 +0,0 @@
|
|||
<button
|
||||
class="icon-toggle-pictures action-gridview"
|
||||
title="Use grid view"
|
||||
></button>
|
||||
<button
|
||||
class="icon-toggle-filelist action-listview"
|
||||
title="Use list view"
|
||||
></button>
|
|
@ -1,10 +0,0 @@
|
|||
<div class="selection-tools">
|
||||
<button class="primary select-all"><span class="icon-checkmark-white"></span><span> <%- t('bookmarks', 'Select all visible') %></span></button>
|
||||
<div class="close" title="<%- t('bookmarks', 'Cancel') %>"><span class="icon-close"></span></div>
|
||||
</div>
|
||||
<button class="delete">
|
||||
<span class="icon-delete"></span>
|
||||
<span><%- t('bookmarks', 'Delete') %></span>
|
||||
</button>
|
||||
<div class="tags">
|
||||
</div>
|
|
@ -1,5 +0,0 @@
|
|||
<div id="mobile-nav-slot"></div>
|
||||
<div id="bulk-actions-slot"></div>
|
||||
<div id="view-bookmarks-slot"></div>
|
||||
<div id="bookmark-detail-slot"></div>
|
||||
<div id="empty-bookmarks-slot"></div>
|
|
@ -1,2 +0,0 @@
|
|||
<h2><%- t('bookmarks', 'No bookmarks here.') %></h2>
|
||||
<p><%- t('bookmarks', 'There are no bookmarks available for this query. Try changing your filter or add some using the menu entry on the left.') %></p>
|
|
@ -1,39 +0,0 @@
|
|||
<button class="collapse"></button>
|
||||
<a href="#" draggable="false" class="icon-folder" title="<%- title %>"><%- title %></a>
|
||||
<div class="app-navigation-entry-edit">
|
||||
<div>
|
||||
<input type="text" value="<%- title %>" class="title" placeholder="<%- t('bookmarks', 'Title') %>">
|
||||
<input type="submit" value="" class="icon-close action cancel">
|
||||
<input type="submit" value="" class="icon-checkmark action submit">
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-navigation-entry-utils">
|
||||
<ul>
|
||||
<li class="app-navigation-entry-utils-menu-button">
|
||||
<button></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="app-navigation-entry-menu">
|
||||
<ul>
|
||||
<li>
|
||||
<button class="menu-edit">
|
||||
<span class="icon-rename"></span>
|
||||
<span><%- t('bookmarks', 'Rename') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-addsub">
|
||||
<span class="icon-add"></span>
|
||||
<span><%- t('bookmarks', 'Add subfolder') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-delete">
|
||||
<span class="icon-delete"></span>
|
||||
<span><%- t('bookmarks', 'Delete') %></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul class="children"></ul>
|
|
@ -1 +0,0 @@
|
|||
<div class="icon-loading"></div>
|
|
@ -1 +0,0 @@
|
|||
<a href="#" class="icon-menu toggle-menu" title="<%- t('bookmarks', 'Open main menu') %>"></a>
|
|
@ -1,22 +0,0 @@
|
|||
<div id="add-bookmark-slot"></div>
|
||||
<ul>
|
||||
<li data-id="all" class="all">
|
||||
<a href="#" class="icon-home"><%- t('bookmarks', 'All bookmarks') %></a>
|
||||
</li>
|
||||
<!--
|
||||
<li data-id="favorites" class="favorites">
|
||||
<a href="#"><span class="icon-favorite"></span><%- t('bookmarks', 'Favorites') %></a>
|
||||
</li>
|
||||
<li data-id="shared" class="shared">
|
||||
<a href="#"><span class="icon-share"></span><%- t('bookmarks', 'Shared') %></a>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
<div id="folders-slot"></div>
|
||||
<ul>
|
||||
<li data-id="untagged" class="untagged">
|
||||
<a href="#" class="icon-category-disabled"><%- t('bookmarks', 'Untagged') %></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="favorite-tags-slot"></div>
|
||||
<div id="settings-slot"></div>
|
|
@ -1,67 +0,0 @@
|
|||
<div id="app-settings-header">
|
||||
<button class="settings-button"><%- t('bookmarks', 'Settings') %></button>
|
||||
</div>
|
||||
<div id="app-settings-content">
|
||||
<h3><%- t('bookmarks', 'Bookmarklet') %></h3>
|
||||
<p>
|
||||
<%- t('bookmarks', 'Drag this to your browser bookmarks and click it, when you want to bookmark a webpage quickly:') %>
|
||||
</p>
|
||||
<a class="button bookmarklet" href=""
|
||||
><%- t('bookmarks', 'Add to {instanceName} ', {instanceName:oc_defaults.name}) %></a
|
||||
>
|
||||
<h3><%- t('bookmarks', 'Import & Export') %></h3>
|
||||
<form
|
||||
class="import-form"
|
||||
action="bookmark/import"
|
||||
method="post"
|
||||
target="upload_iframe"
|
||||
enctype="multipart/form-data"
|
||||
encoding="multipart/form-data"
|
||||
>
|
||||
<input type="file" class="import" name="bm_import" size="5" />
|
||||
<input type="hidden" name="requesttoken" value="<%- oc_requesttoken %>" />
|
||||
<button class="import-facade">
|
||||
<span class="icon-upload"></span> <%- t('bookmarks', 'Import') %>
|
||||
</button>
|
||||
</form>
|
||||
<iframe class="upload" name="upload_iframe" id="upload_iframe"></iframe>
|
||||
<button class="export">
|
||||
<span class="icon-download"></span> <%- t('bookmarks', 'Export') %>
|
||||
</button>
|
||||
<div class="import-status"></div>
|
||||
<h3><%- t('bookmarks', 'Sorting') %></h3>
|
||||
<form class="sort-form" action="settings/sort" method="post">
|
||||
<select id="sort" name="sorting">
|
||||
<option id="added" value="added">
|
||||
<%- t('bookmarks', 'Recently added') %></option
|
||||
>
|
||||
<option id="title" value="title">
|
||||
<%- t('bookmarks', 'Alphabetically') %></option
|
||||
>
|
||||
<option id="clickcount" value="clickcount">
|
||||
<%- t('bookmarks', 'Most visited') %></option
|
||||
>
|
||||
<option id="lastmodified" value="lastmodified">
|
||||
<%- t('bookmarks', 'Latest modified') %></option
|
||||
>
|
||||
</select>
|
||||
</form>
|
||||
<h3><%- t('bookmarks', 'View mode') %></h3>
|
||||
<select class="view-mode" name="view">
|
||||
<option id="grid" value="grid"> <%- t('bookmarks', 'Grid view') %></option>
|
||||
<option id="list" value="list"> <%- t('bookmarks', 'List view') %></option>
|
||||
</select>
|
||||
<h3><%- t('bookmarks', 'RSS Feed') %></h3>
|
||||
<p>
|
||||
<%- t('bookmarks', 'This is an RSS feed of the current result set with access restricted to you.') %>
|
||||
</p>
|
||||
<input type="text" readonly class="rss-link" />
|
||||
<h3><%- t('bookmarks', 'Clear data') %></h3>
|
||||
<p>
|
||||
<%- t('bookmarks', 'Permanently remove all bookmarks from your account. There is no going back!') %>
|
||||
</p>
|
||||
<button class="clear-data">
|
||||
<span class="icon-delete"></span> <%- t('bookmarks', 'Delete all bookmarks')
|
||||
%>
|
||||
</button>
|
||||
</div>
|
|
@ -1,37 +0,0 @@
|
|||
<a href="#" class="icon-tag" title="<%- name %>"><%- name %></a>
|
||||
<div class="app-navigation-entry-utils">
|
||||
<ul>
|
||||
<li class="app-navigation-entry-utils-counter"><%- count > 999 ? "999+" :count %></li>
|
||||
<li class="app-navigation-entry-utils-menu-button">
|
||||
<button></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="app-navigation-entry-menu">
|
||||
<ul>
|
||||
<li>
|
||||
<button class="menu-item-checkbox menu-filter-add">
|
||||
<span><input type="checkbox" name="select" class="checkbox" /><label for="select"></label></span>
|
||||
<span><%- t('bookmarks', 'Add to filter') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-item-checkbox menu-filter-remove">
|
||||
<span><input type="checkbox" name="select" checked class="checkbox" /><label for="select"></label></span>
|
||||
<span><%- t('bookmarks', 'Remove from filter') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-edit">
|
||||
<span class="icon-rename"></span>
|
||||
<span><%- t('bookmarks', 'Rename') %></span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="menu-delete">
|
||||
<span class="icon-delete"></span>
|
||||
<span><%- t('bookmarks', 'Delete') %></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||
<a href="#">
|
||||
<input type="text" value="<%- name %>">
|
||||
<div class="actions">
|
||||
<ul>
|
||||
<li class="action">
|
||||
<button class="submit icon-checkmark"></button>
|
||||
</li>
|
||||
<li class="action">
|
||||
<button class="cancel icon-close"></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
|
@ -1 +0,0 @@
|
|||
<a href="#" title="Other bookmarks with this tag"><%- name %></a>
|
|
@ -1,16 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Tag from '../models/Tag';
|
||||
|
||||
var _sync = Backbone.sync;
|
||||
Backbone.sync = function(method, model, options) {
|
||||
var overrideOptions = {
|
||||
headers: {
|
||||
requesttoken: oc_requesttoken
|
||||
}
|
||||
};
|
||||
if (method === 'update' && model instanceof Tag) {
|
||||
overrideOptions.url = model.urlRoot + '/' + model.previous('name');
|
||||
}
|
||||
return _sync(method, model, _.extend({}, options, overrideOptions));
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
import interact from 'interactjs';
|
||||
interact.dynamicDrop(true);
|
||||
interact.pointerMoveTolerance(20);
|
|
@ -1,18 +0,0 @@
|
|||
export default function isTouchDevice() {
|
||||
var prefixes = ' -webkit- -moz- -o- -ms- '.split(' ');
|
||||
var mq = function(query) {
|
||||
return window.matchMedia(query).matches;
|
||||
};
|
||||
|
||||
if (
|
||||
'ontouchstart' in window ||
|
||||
(window.DocumentTouch && document instanceof DocumentTouch)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
|
||||
// https://git.io/vznFH
|
||||
var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('');
|
||||
return mq(query);
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Bookmark from '../models/Bookmark';
|
||||
import templateString from '../templates/AddBookmark.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
template: _.template(templateString),
|
||||
className: 'add-bookmark',
|
||||
tagName: 'ul',
|
||||
events: {
|
||||
'click @ui.link': 'activate',
|
||||
'click @ui.button': 'submit',
|
||||
'keydown @ui.input': 'onKeydown',
|
||||
'blur @ui.input': 'deactivate'
|
||||
},
|
||||
ui: {
|
||||
link: '.link a',
|
||||
linkEntry: '.link',
|
||||
formEntry: '.form',
|
||||
input: 'input',
|
||||
button: 'button'
|
||||
},
|
||||
activate: function() {
|
||||
this.getUI('linkEntry').hide();
|
||||
this.getUI('formEntry').show();
|
||||
this.getUI('input').focus();
|
||||
},
|
||||
deactivate: function() {
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.getUI('linkEntry').show();
|
||||
that.getUI('formEntry').hide();
|
||||
}, 300);
|
||||
},
|
||||
onKeydown: function(e) {
|
||||
if (e.which != 13) return;
|
||||
// Enter
|
||||
this.submit();
|
||||
},
|
||||
submit: function(e) {
|
||||
var $input = this.getUI('input');
|
||||
if (this.pending || $input.val() === '') return;
|
||||
var url = $input.val();
|
||||
var bm = new Bookmark({ url: url });
|
||||
this.setPending(true);
|
||||
var that = this;
|
||||
bm.save(null, {
|
||||
success: function() {
|
||||
// needed in order for the route to be revaluated when it's already active
|
||||
Backbone.history.navigate('dummyroute');
|
||||
Backbone.history.navigate('all', { trigger: true });
|
||||
|
||||
// reset input field
|
||||
that.setPending(false);
|
||||
that.deactivate();
|
||||
that.getUI('input').val('');
|
||||
|
||||
// show new bookmark
|
||||
Radio.channel('details').trigger('show', bm);
|
||||
},
|
||||
error: function() {
|
||||
that.setPending(false);
|
||||
that.getUI('button').removeClass('icon-add');
|
||||
that.getUI('button').addClass('icon-error-color');
|
||||
}
|
||||
});
|
||||
},
|
||||
setPending: function(pending) {
|
||||
if (pending) {
|
||||
this.getUI('button').removeClass('icon-add');
|
||||
this.getUI('button').removeClass('icon-error-color');
|
||||
this.getUI('button').addClass('icon-loading-small');
|
||||
this.getUI('button').prop('disabled', true);
|
||||
} else {
|
||||
this.getUI('button').removeClass('icon-error-color');
|
||||
this.getUI('button').addClass('icon-add');
|
||||
this.getUI('button').removeClass('icon-loading-small');
|
||||
this.getUI('button').prop('disabled', false);
|
||||
}
|
||||
this.pending = pending;
|
||||
}
|
||||
});
|
|
@ -1,108 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import interact from 'interactjs';
|
||||
import templateStringDefault from '../templates/AddFolder.html';
|
||||
import Folder from '../models/Folder';
|
||||
import Folders from '../models/Folders';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
className: 'folders-item',
|
||||
tagName: 'li',
|
||||
template: _.template(templateStringDefault),
|
||||
events: {
|
||||
'click a': 'actionEdit',
|
||||
'click .action.submit': 'actionSubmit',
|
||||
'click .action.cancel': 'actionCancel',
|
||||
'keyup input.title': 'keyup'
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.parentFolder = options.parentFolder;
|
||||
this.collection = options.collection;
|
||||
this.listenTo(Radio.channel('documentClicked'), 'click', this.click);
|
||||
this.listenTo(this.parentFolder, 'addSubFolder', this.actionEdit);
|
||||
this.initInteractable();
|
||||
},
|
||||
initInteractable: function() {
|
||||
this.interactable = interact(this.el).dropzone({
|
||||
overlap: 'pointer',
|
||||
ondrop: this.onDrop.bind(this),
|
||||
ondropactivate: this.onDropActivate.bind(this),
|
||||
ondropdeactivate: this.onDropDeactivate.bind(this)
|
||||
});
|
||||
},
|
||||
onRender: function() {
|
||||
this.$el.addClass('add-folder');
|
||||
this.$el.removeClass('editing');
|
||||
if (this.editing) {
|
||||
this.$el.addClass('editing');
|
||||
this.$('input.title').focus();
|
||||
}
|
||||
},
|
||||
actionEdit: function(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (this.editing) {
|
||||
return;
|
||||
}
|
||||
this.editing = true;
|
||||
this.render();
|
||||
},
|
||||
actionSubmit: function(e) {
|
||||
var that = this;
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
var folder = new Folder();
|
||||
folder.set('title', this.$('input.title').val());
|
||||
folder.set('children', new Folders());
|
||||
folder.set(
|
||||
'parent_folder',
|
||||
this.parentFolder ? this.parentFolder.get('id') : -1
|
||||
);
|
||||
folder.once('sync', function() {
|
||||
that.collection.add(folder);
|
||||
});
|
||||
folder.save();
|
||||
this.actionCancel();
|
||||
},
|
||||
actionCancel: function(e) {
|
||||
this.editing = false;
|
||||
this.render();
|
||||
},
|
||||
click: function(e) {
|
||||
if ($.contains(this.el, e.target)) {
|
||||
return;
|
||||
}
|
||||
this.actionCancel();
|
||||
},
|
||||
keyup: function(e) {
|
||||
if (e.which === 13) {
|
||||
this.actionSubmit();
|
||||
}
|
||||
},
|
||||
onDropActivate: function(e) {
|
||||
if (this.parentFolder.get('id') !== '-1') return;
|
||||
if (
|
||||
!(e.draggable.model instanceof Folder) ||
|
||||
e.draggable.model.get('parent_folder') === this.parentFolder.get('id') ||
|
||||
e.draggable.model.get('id') === this.parentFolder.get('id')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.$el.addClass('droptarget-folder');
|
||||
},
|
||||
onDropDeactivate: function(e) {
|
||||
this.$el.removeClass('droptarget-folder');
|
||||
},
|
||||
onDrop: function(e) {
|
||||
if (e.draggable.model instanceof Folder) {
|
||||
this.parentFolder.trigger('dropFolder', e);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import SearchController from './SearchController';
|
||||
import NavigationView from './Navigation';
|
||||
import ContentView from './Content';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
el: '.app-bookmarks',
|
||||
template: _.noop,
|
||||
regions: {
|
||||
navigation: {
|
||||
el: '#navigation-slot',
|
||||
replaceElement: true
|
||||
},
|
||||
content: {
|
||||
el: '#app-content',
|
||||
replaceElement: true
|
||||
}
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.app = options.app;
|
||||
this.searchController = new SearchController();
|
||||
|
||||
$(window.document).click(function(e) {
|
||||
Radio.channel('documentClicked').trigger('click', e);
|
||||
});
|
||||
},
|
||||
onRender: function() {
|
||||
this.showChildView('navigation', new NavigationView({ app: this.app }));
|
||||
this.showChildView('content', new ContentView({ app: this.app }));
|
||||
}
|
||||
});
|
|
@ -1,173 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import interact from 'interactjs';
|
||||
import isTouchDevice from '../utils/IsTouchscreen';
|
||||
import Tags from '../models/Tags';
|
||||
import TagsNavigationView from './TagsNavigation';
|
||||
import templateString from '../templates/BookmarkCard.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
template: _.template(templateString),
|
||||
className: 'bookmark-card',
|
||||
ui: {
|
||||
link: 'h2 > a',
|
||||
checkbox: '.selectbox',
|
||||
actionsMenu: '.popovermenu',
|
||||
actionsToggle: '.toggle-actions'
|
||||
},
|
||||
regions: {
|
||||
tags: '.tags'
|
||||
},
|
||||
events: {
|
||||
click: 'open',
|
||||
'click @ui.link': 'clickLink',
|
||||
'click @ui.checkbox': 'select',
|
||||
'click @ui.actionsToggle': 'toggleActions',
|
||||
'click .menu-filter-add': 'select',
|
||||
'click .menu-filter-remove': 'select',
|
||||
'click .menu-delete': 'delete',
|
||||
'click .menu-details': 'open',
|
||||
'click .menu-move': 'move',
|
||||
contextmenu: 'preventRightClick'
|
||||
},
|
||||
initialize: function(opts) {
|
||||
this.app = opts.app;
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.listenTo(this.model, 'select', this.onSelect);
|
||||
this.listenTo(this.model, 'unselect', this.onUnselect);
|
||||
// when bulk selection is active, the cards not being dragged directly
|
||||
// get their events through the models
|
||||
this.listenTo(this.model, 'dragstart', this.onDragStart);
|
||||
this.listenTo(this.model, 'dragmove', this.onDragMove);
|
||||
this.listenTo(this.model, 'dragend', this.onDragEnd);
|
||||
|
||||
this.listenTo(this.model, 'dropped', this.onDropped);
|
||||
|
||||
this.listenTo(this.app.tags, 'sync', this.render);
|
||||
this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions);
|
||||
this.interactable = interact(this.el).draggable({
|
||||
onstart: this.onDragStart.bind(this),
|
||||
onend: this.onDragEnd.bind(this),
|
||||
onmove: this.onDragMove.bind(this),
|
||||
hold: isTouchDevice() ? 500 : 0
|
||||
});
|
||||
this.interactable.model = this.model;
|
||||
},
|
||||
onRender: function() {
|
||||
var that = this;
|
||||
this.$el.css(
|
||||
'background-image',
|
||||
'url(bookmark/' + this.model.get('id') + '/image)'
|
||||
);
|
||||
this.$el.css('background-color', this.model.getColor());
|
||||
|
||||
this.$el.prop('title', t('bookmarks', 'Open details'));
|
||||
|
||||
var tags = new Tags(
|
||||
this.model.get('tags').map(function(id) {
|
||||
return that.app.tags.findWhere({ name: id });
|
||||
})
|
||||
);
|
||||
this.showChildView('tags', new TagsNavigationView({ collection: tags }));
|
||||
this.$('.checkbox').prop('checked', this.$el.hasClass('active'));
|
||||
},
|
||||
clickLink: function(e) {
|
||||
if (e && e.target === this.getUI('actionsToggle')[0]) {
|
||||
return;
|
||||
}
|
||||
this.model.clickLink();
|
||||
},
|
||||
open: function(e) {
|
||||
if (
|
||||
e &&
|
||||
(this.getUI('actionsToggle')[0] === e.target ||
|
||||
this.getUI('link')[0] === e.target ||
|
||||
$.contains(this.$('.tags')[0], e.target))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.$el.closest('.selection-active').length) {
|
||||
this.select(e);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
Radio.channel('details').trigger('show', this.model);
|
||||
},
|
||||
toggleActions: function() {
|
||||
this.getUI('actionsMenu')
|
||||
.toggleClass('open')
|
||||
.toggleClass('closed');
|
||||
},
|
||||
closeActions: function(e) {
|
||||
if (e && this.getUI('actionsToggle')[0] === e.target) {
|
||||
return;
|
||||
}
|
||||
this.getUI('actionsMenu')
|
||||
.removeClass('open')
|
||||
.addClass('closed');
|
||||
},
|
||||
select: function(e) {
|
||||
e.stopPropagation();
|
||||
if (this.$el.hasClass('active')) {
|
||||
this.model.trigger('unselect', this.model);
|
||||
} else {
|
||||
this.model.trigger('select', this.model);
|
||||
}
|
||||
},
|
||||
onSelect: function() {
|
||||
this.$el.addClass('active');
|
||||
this.render();
|
||||
},
|
||||
onUnselect: function() {
|
||||
this.$el.removeClass('active');
|
||||
this.render();
|
||||
},
|
||||
delete: function() {
|
||||
this.model.destroy();
|
||||
},
|
||||
onDragStart: function(e, propagate) {
|
||||
this.$el.addClass('dragging');
|
||||
if (propagate !== false) {
|
||||
this.app.selectedBookmarks.forEach(function(bm) {
|
||||
bm.trigger('dragstart', e, false);
|
||||
});
|
||||
}
|
||||
},
|
||||
onDragMove: function(e, propagate) {
|
||||
this.$el.offset({ top: e.pageY + 20, left: e.pageX + 20 });
|
||||
if (propagate !== false) {
|
||||
this.app.selectedBookmarks.forEach(function(bm, i) {
|
||||
bm.trigger(
|
||||
'dragmove',
|
||||
{ pageY: e.pageY + 10 * (i + 1), pageX: e.pageX + 10 * (i + 1) },
|
||||
false
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
onDragEnd: function(e, propagate) {
|
||||
this.$el.removeClass('dragging');
|
||||
this.$el.css({ position: 'relative', top: 0, left: 0 });
|
||||
if (propagate !== false) {
|
||||
this.app.selectedBookmarks.forEach(function(bm) {
|
||||
bm.trigger('dragend', e, false);
|
||||
});
|
||||
}
|
||||
},
|
||||
onDropped: function() {
|
||||
var that = this;
|
||||
this.$el.hide();
|
||||
},
|
||||
onDestroy: function() {
|
||||
this.interactable.unset();
|
||||
},
|
||||
preventRightClick: function(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
|
@ -1,180 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Tag from '../models/Tag';
|
||||
import Tags from '../models/Tags';
|
||||
import TagsNavigationView from './TagsNavigation';
|
||||
import TagsSelectionView from './TagsSelection';
|
||||
import templateString from '../templates/BookmarkDetail.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
template: _.template(templateString),
|
||||
className: 'bookmark-detail',
|
||||
regions: {
|
||||
tags: {
|
||||
el: '.tags'
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
preview: '.preview',
|
||||
link: 'h2 > a',
|
||||
close: '> .close',
|
||||
edit: '.edit',
|
||||
status: '.status'
|
||||
},
|
||||
events: {
|
||||
'click @ui.link': 'clickLink',
|
||||
'click @ui.close': 'close',
|
||||
'click h1': 'edit',
|
||||
'click h2 .edit': 'edit',
|
||||
'click .description': 'edit',
|
||||
'click .submit': 'submit',
|
||||
'click .cancel': 'cancel'
|
||||
},
|
||||
initialize: function(opts) {
|
||||
this.doSlideIn = opts.slideIn;
|
||||
this.app = opts.app;
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.listenTo(this.model, 'destroy', this.onDestroy);
|
||||
this.listenTo(this.app.tags, 'sync', this.render);
|
||||
|
||||
var that = this;
|
||||
this.tags = new Tags(
|
||||
this.model.get('tags').map(function(id) {
|
||||
return that.app.tags.get(id);
|
||||
})
|
||||
);
|
||||
this.submitTagsTimeout = null;
|
||||
this.listenTo(this.tags, 'add remove', this.submitTags);
|
||||
this.listenTo(
|
||||
Radio.channel('documentClicked'),
|
||||
'click',
|
||||
this.onDocumentClicked
|
||||
);
|
||||
},
|
||||
onRender: function() {
|
||||
this.getUI('preview').css(
|
||||
'background-image',
|
||||
'url(bookmark/' + this.model.get('id') + '/image)'
|
||||
);
|
||||
this.getUI('preview').css('background-color', this.model.getColor());
|
||||
|
||||
this.showChildView(
|
||||
'tags',
|
||||
new TagsSelectionView({
|
||||
collection: this.app.tags,
|
||||
selected: this.tags,
|
||||
app: this.app
|
||||
})
|
||||
);
|
||||
|
||||
if (this.savingState === 'saving') {
|
||||
this.getUI('status')
|
||||
.removeClass('saved')
|
||||
.addClass('saving');
|
||||
}
|
||||
if (this.savingState === 'saved') {
|
||||
this.getUI('status')
|
||||
.addClass('saved')
|
||||
.removeClass('saving');
|
||||
}
|
||||
|
||||
if (this.doSlideIn) {
|
||||
this.slideIn();
|
||||
this.doSlideIn = false;
|
||||
}
|
||||
},
|
||||
clickLink: function() {
|
||||
this.model.clickLink();
|
||||
},
|
||||
close: function() {
|
||||
Radio.channel('details').trigger('close');
|
||||
},
|
||||
edit: function(e) {
|
||||
var that = this;
|
||||
e.preventDefault();
|
||||
|
||||
var $el = this.$(e.target).closest('[data-attribute]');
|
||||
|
||||
if ($el.prop('contenteditable') === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($el.data('attribute')) {
|
||||
case 'url':
|
||||
$el.text(this.model.get('url'));
|
||||
// fallthrough
|
||||
case 'title':
|
||||
$el.on('keydown', function(e) {
|
||||
// enter
|
||||
if (e.which === 13) {
|
||||
that.submit($el);
|
||||
}
|
||||
});
|
||||
case 'description':
|
||||
if ($el.hasClass('empty')) {
|
||||
$el.text('');
|
||||
$el.removeClass('empty');
|
||||
}
|
||||
break;
|
||||
}
|
||||
$el.prop('contenteditable', true);
|
||||
$el.one('blur', function() {
|
||||
that.submit($el);
|
||||
});
|
||||
$el.focus();
|
||||
},
|
||||
submitTags: function() {
|
||||
var that = this;
|
||||
clearTimeout(this.submitTagsTimeout);
|
||||
this.submitTagsTimeout = setTimeout(function() {
|
||||
that.savingState = 'saving';
|
||||
that.app.tags.add(that.tags.models);
|
||||
that.model.set({
|
||||
tags: that.tags.pluck('name')
|
||||
});
|
||||
that.model.once('sync', function() {
|
||||
that.savingState = 'saved';
|
||||
});
|
||||
that.model.save();
|
||||
}, 5000);
|
||||
},
|
||||
submit: function($el) {
|
||||
var that = this;
|
||||
if (this.savingState === 'saving') {
|
||||
return;
|
||||
}
|
||||
this.savingState = 'saving';
|
||||
this.model.set({
|
||||
[$el.data('attribute')]: $el[0].innerText
|
||||
});
|
||||
this.model.once('sync', function() {
|
||||
that.savingState = 'saved';
|
||||
});
|
||||
this.model.save();
|
||||
},
|
||||
onDestroy: function() {
|
||||
this.close();
|
||||
},
|
||||
onDocumentClicked: function(evt) {
|
||||
if (
|
||||
evt &&
|
||||
(this.el === evt.target ||
|
||||
$.contains(this.el, evt.target) ||
|
||||
!$.contains(document.body, evt.target))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
slideIn: function(cb) {
|
||||
this.$el.addClass('slide-in');
|
||||
if (cb) setTimeout(cb, 200);
|
||||
},
|
||||
slideOut: function(cb) {
|
||||
this.$el.addClass('slide-out');
|
||||
if (cb) setTimeout(cb, 200);
|
||||
}
|
||||
});
|
|
@ -1,54 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Bookmark from '../models/Bookmark';
|
||||
import Tags from '../models/Tags';
|
||||
import TagsSelectionView from './TagsSelection';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
template: false,
|
||||
el: '#bookmarklet_form',
|
||||
regions: {
|
||||
'tags': {
|
||||
el: '#tags',
|
||||
replaceElement: true
|
||||
},
|
||||
},
|
||||
events: {
|
||||
'click .submit': 'submit'
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.app = options.app;
|
||||
|
||||
$(window.document).click(function(e) {
|
||||
Radio.channel('documentClicked').trigger('click', e);
|
||||
});
|
||||
|
||||
this.app.tags.once('reset sync add remove', () => {
|
||||
this.selected = new Tags(
|
||||
this.$('#tags li')
|
||||
.map((e) => $(e).text())
|
||||
.map((tagName) => this.app.tags.findWhere({name: tagName}))
|
||||
);
|
||||
this.showChildView('tags', new TagsSelectionView({app: this.app, selected: this.selected}));
|
||||
});
|
||||
},
|
||||
submit: function(e) {
|
||||
e.preventDefault();
|
||||
this.$('#add_form_loading').css('visibility', 'visible');
|
||||
var bm = new Bookmark({
|
||||
title: this.$('.title').val(),
|
||||
url: this.$('.url_input').val(),
|
||||
description: this.$('.desc').val(),
|
||||
tags: this.selected.pluck('name')
|
||||
});
|
||||
bm.once('sync', () => setTimeout(() => window.close(), 1e3));
|
||||
bm.save({
|
||||
wait: true,
|
||||
error: () => OC.dialogs.alert(t('bookmarks', 'An error occurred while trying to save the bookmark.'),
|
||||
t('bookmarks', 'Error'), null, true)
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import BookmarkCardView from './BookmarkCard';
|
||||
import BookmarksDisplayView from './BookmarksDisplay';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.CollectionView.extend({
|
||||
className: 'bookmarks',
|
||||
initialize: function(opts) {
|
||||
this.app = opts.app;
|
||||
this.listenTo(Radio.channel('viewMode'), 'change', this.changeViewMode);
|
||||
this.listenTo(this.app.settings, 'change:viewMode', this.changeViewMode);
|
||||
},
|
||||
childViewOptions: function() {
|
||||
return { app: this.app };
|
||||
},
|
||||
childView: function() {
|
||||
return BookmarkCardView;
|
||||
},
|
||||
onRender: function() {
|
||||
this.addChildView(new BookmarksDisplayView({ app: this.app }), 0);
|
||||
this.addChildView(new EmptySpaceView(), this.collection.length + 1);
|
||||
this.addChildView(new EmptySpaceView(), this.collection.length + 1);
|
||||
this.addChildView(new EmptySpaceView(), this.collection.length + 1);
|
||||
this.addChildView(new EmptySpaceView(), this.collection.length + 1);
|
||||
this.addChildView(new EmptySpaceView(), this.collection.length + 1);
|
||||
},
|
||||
changeViewMode: function(mode) {
|
||||
if (typeof mode !== 'string') {
|
||||
mode = this.app.settings.get('viewMode');
|
||||
}
|
||||
if (mode === 'list') {
|
||||
this.$el.addClass('list-view');
|
||||
} else {
|
||||
this.$el.removeClass('list-view');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var EmptySpaceView = Marionette.View.extend({
|
||||
className: 'empty-space',
|
||||
render: function() {}
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import templateString from '../templates/BookmarksDisplay.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
template: _.template(templateString),
|
||||
className: 'bookmarks-display',
|
||||
initialize: function(opts) {
|
||||
this.app = opts.app;
|
||||
},
|
||||
events: {
|
||||
'click .action-listview': 'activateListView',
|
||||
'click .action-gridview': 'activateGridView'
|
||||
},
|
||||
activateGridView: function() {
|
||||
Radio.channel('viewMode').trigger('change', 'grid');
|
||||
},
|
||||
activateListView: function() {
|
||||
Radio.channel('viewMode').trigger('change', 'list');
|
||||
}
|
||||
});
|
|
@ -1,102 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Tags from '../models/Tags';
|
||||
import TagsSelectionView from './TagsSelection';
|
||||
import templateString from '../templates/BulkActions.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
className: 'bulk-actions',
|
||||
template: _.template(templateString),
|
||||
regions: {
|
||||
tags: {
|
||||
el: '.tags'
|
||||
}
|
||||
},
|
||||
events: {
|
||||
'click .delete': 'delete',
|
||||
'click .select-all': 'selectAll',
|
||||
'click .selection-tools .close': 'abort'
|
||||
},
|
||||
initialize: function(opts) {
|
||||
this.app = opts.app;
|
||||
this.all = this.app.bookmarks;
|
||||
this.selected = opts.selected;
|
||||
this.tags = new Tags();
|
||||
this.listenTo(this.tags, 'remove', this.onTagRemoved);
|
||||
this.listenTo(this.tags, 'add', this.onTagAdded);
|
||||
this.listenTo(this.selected, 'remove', this.onReduceSelection);
|
||||
this.listenTo(this.selected, 'add', this.onExtendSelection);
|
||||
},
|
||||
onRender: function() {
|
||||
this.showChildView(
|
||||
'tags',
|
||||
new TagsSelectionView({
|
||||
collection: this.app.tags,
|
||||
selected: this.tags,
|
||||
app: this.app
|
||||
})
|
||||
);
|
||||
},
|
||||
updateTags: function() {
|
||||
var that = this;
|
||||
this.triggeredByAlgo = true;
|
||||
this.tags.reset(
|
||||
_.intersection.apply(_, this.selected.pluck('tags')).map(function(name) {
|
||||
return that.app.tags.get(name);
|
||||
})
|
||||
);
|
||||
this.triggeredByAlgo = false;
|
||||
},
|
||||
onReduceSelection: function() {
|
||||
if (this.selected.length == 0) {
|
||||
this.$el.removeClass('active');
|
||||
}
|
||||
this.updateTags();
|
||||
},
|
||||
onExtendSelection: function() {
|
||||
if (this.selected.length == 1) {
|
||||
this.$el.addClass('active');
|
||||
}
|
||||
this.updateTags();
|
||||
},
|
||||
delete: function() {
|
||||
this.selected.slice().forEach(function(model) {
|
||||
model.trigger('unselect', model);
|
||||
model.destroy({
|
||||
error: function() {
|
||||
Backbone.history.navigate('all', { trigger: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
onTagAdded: function(tag) {
|
||||
if (this.triggeredByAlgo) return;
|
||||
this.selected.forEach(function(model) {
|
||||
var tags = model.get('tags');
|
||||
model.set('tags', _.union(tags, [tag.get('name')]));
|
||||
model.save();
|
||||
});
|
||||
},
|
||||
onTagRemoved: function(tag) {
|
||||
if (this.triggeredByAlgo) return;
|
||||
this.selected.forEach(function(model) {
|
||||
var tags = model.get('tags');
|
||||
model.set('tags', _.without(tags, tag.get('name')));
|
||||
model.save();
|
||||
});
|
||||
},
|
||||
selectAll: function() {
|
||||
this.all.forEach(function(model) {
|
||||
model.trigger('select', model);
|
||||
});
|
||||
},
|
||||
abort: function() {
|
||||
this.selected.models.slice().forEach(function(model) {
|
||||
model.trigger('unselect', model);
|
||||
});
|
||||
this.app.bookmarks.trigger('unselect');
|
||||
}
|
||||
});
|
|
@ -1,122 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Bookmarks from '../models/Bookmarks';
|
||||
import EmptyBookmarksView from './EmptyBookmarks';
|
||||
import MobileNavView from './MobileNav';
|
||||
import BulkActionsView from './BulkActions';
|
||||
import BookmarksView from './Bookmarks';
|
||||
import BookmarkDetailView from './BookmarkDetail';
|
||||
import templateString from '../templates/Content.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
template: _.template(templateString),
|
||||
id: 'app-content',
|
||||
regions: {
|
||||
mobileNav: {
|
||||
el: '#mobile-nav-slot',
|
||||
replaceElement: true
|
||||
},
|
||||
bulkActions: {
|
||||
el: '#bulk-actions-slot',
|
||||
replaceElement: true
|
||||
},
|
||||
viewBookmarks: {
|
||||
el: '#view-bookmarks-slot',
|
||||
replaceElement: true
|
||||
},
|
||||
emptyBookmarks: {
|
||||
el: '#empty-bookmarks-slot',
|
||||
replaceElement: true
|
||||
},
|
||||
bookmarkDetail: {
|
||||
el: '#bookmark-detail-slot',
|
||||
replaceElement: true
|
||||
}
|
||||
},
|
||||
initialize: function(options) {
|
||||
var that = this;
|
||||
this.app = options.app;
|
||||
this.bookmarks = this.app.bookmarks;
|
||||
this.selected = new Bookmarks();
|
||||
this.app.selectedBookmarks = this.selected;
|
||||
this.listenTo(
|
||||
this.bookmarks.loadingState,
|
||||
'change:fetching',
|
||||
this.infiniteScroll
|
||||
);
|
||||
this.listenTo(this.bookmarks, 'select', this.onSelect);
|
||||
this.listenTo(this.bookmarks, 'unselect', this.onUnselect);
|
||||
this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate);
|
||||
this.listenTo(Radio.channel('details'), 'show', this.onShowDetails);
|
||||
this.listenTo(Radio.channel('details'), 'close', this.onCloseDetails);
|
||||
document.addEventListener('scroll', function() {
|
||||
that.infiniteScroll();
|
||||
});
|
||||
},
|
||||
onRender: function() {
|
||||
this.showChildView('mobileNav', new MobileNavView());
|
||||
this.showChildView(
|
||||
'viewBookmarks',
|
||||
new BookmarksView({ collection: this.bookmarks, app: this.app })
|
||||
);
|
||||
this.showChildView(
|
||||
'emptyBookmarks',
|
||||
new EmptyBookmarksView({ app: this.app })
|
||||
);
|
||||
},
|
||||
infiniteScroll: function(e) {
|
||||
if (
|
||||
document.body.scrollHeight < window.scrollY + window.innerHeight + 500 &&
|
||||
this.bookmarks.loadingState.get('page') !== 0
|
||||
) {
|
||||
this.bookmarks.fetchPage();
|
||||
}
|
||||
},
|
||||
onSelect: function(model) {
|
||||
if (this.selected.length == 0) {
|
||||
this.$el.addClass('selection-active');
|
||||
Radio.channel('details').trigger('close');
|
||||
this.showChildView(
|
||||
'bulkActions',
|
||||
new BulkActionsView({ selected: this.selected, app: this.app })
|
||||
);
|
||||
}
|
||||
this.selected.add(model);
|
||||
},
|
||||
onUnselect: function(model) {
|
||||
if (this.selected.length <= 1) {
|
||||
this.$el.removeClass('selection-active');
|
||||
this.detachChildView('bulkActions');
|
||||
}
|
||||
this.selected.remove(model);
|
||||
},
|
||||
onShowDetails: function(model) {
|
||||
var view = this.getChildView('bookmarkDetail');
|
||||
// toggle details when the same card is clicked twice
|
||||
if (view && view.model.id === model.id) {
|
||||
Radio.channel('details').trigger('close');
|
||||
} else {
|
||||
var oldView;
|
||||
if ((oldView = this.detachChildView('bookmarkDetail'))) {
|
||||
oldView.destroy();
|
||||
}
|
||||
var newView = new BookmarkDetailView({
|
||||
model: model,
|
||||
app: this.app,
|
||||
slideIn: !view
|
||||
});
|
||||
this.showChildView('bookmarkDetail', newView);
|
||||
}
|
||||
},
|
||||
onCloseDetails: function(evt) {
|
||||
var that = this;
|
||||
var view = this.getChildView('bookmarkDetail');
|
||||
if (!view) return;
|
||||
that.getChildView('bookmarkDetail').slideOut(function() {
|
||||
that.detachChildView('bookmarkDetail').destroy();
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import templateStringEmpty from '../templates/EmptyBookmarks.html';
|
||||
import templateStringLoading from '../templates/LoadingBookmarks.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
getTemplate: function() {
|
||||
if (this.app.bookmarks.loadingState.get('fetching')) {
|
||||
return _.template(templateStringLoading);
|
||||
} else if (this.app.bookmarks.length === 0) {
|
||||
return _.template(templateStringEmpty);
|
||||
} else {
|
||||
return _.template('');
|
||||
}
|
||||
},
|
||||
className: 'bookmarks-empty',
|
||||
initialize: function(options) {
|
||||
this.app = options.app;
|
||||
this.listenTo(this.app.bookmarks.loadingState, 'change:fetching', this.render);
|
||||
}
|
||||
});
|
|
@ -1,325 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import interact from 'interactjs';
|
||||
import isTouchDevice from '../utils/IsTouchscreen';
|
||||
import templateString from '../templates/Folder.html';
|
||||
import FoldersView from './Folders';
|
||||
import Folder from '../models/Folder';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
className: 'folders-item',
|
||||
tagName: 'li',
|
||||
template: _.template(templateString),
|
||||
regions: {
|
||||
children: {
|
||||
el: '.children',
|
||||
replaceElement: true
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
actionsMenu: '.app-navigation-entry-menu',
|
||||
actionsToggle: '.app-navigation-entry-utils-menu-button'
|
||||
},
|
||||
events: {
|
||||
'click > a': 'select',
|
||||
'click > .collapse': 'toggleChildren',
|
||||
'click @ui.actionsToggle': 'toggleActions',
|
||||
'click > .app-navigation-entry-menu .menu-delete': 'actionDelete',
|
||||
'click > .app-navigation-entry-menu .menu-edit': 'actionEdit',
|
||||
'click > .app-navigation-entry-menu .menu-addsub': 'actionAddSubFolder',
|
||||
'click .action.submit': 'actionSubmit',
|
||||
'click .action.cancel': 'actionCancel',
|
||||
'keyup input.title': 'onKeyup',
|
||||
mouseover: 'onMouseOver',
|
||||
mouseout: 'onMouseOut'
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.app = options.app;
|
||||
this.selectedFolder = options.selectedFolder;
|
||||
this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate);
|
||||
this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions);
|
||||
this.listenTo(this.model, 'dropFolder', this.onDropFolder);
|
||||
this.listenTo(this.model, 'dropped', this.onDropped);
|
||||
this.initInteractable();
|
||||
},
|
||||
initInteractable: function() {
|
||||
this.interactable = interact(this.el)
|
||||
.dropzone({
|
||||
overlap: 'pointer',
|
||||
ignoreFrom: '.folders, input',
|
||||
ondrop: this.onDrop.bind(this),
|
||||
ondropactivate: this.onDropActivate.bind(this),
|
||||
ondropdeactivate: this.onDropDeactivate.bind(this)
|
||||
})
|
||||
.draggable({
|
||||
onstart: this.onDragStart.bind(this),
|
||||
onend: this.onDragEnd.bind(this),
|
||||
onmove: this.onDragMove.bind(this),
|
||||
hold: isTouchDevice() ? 500 : 0
|
||||
});
|
||||
this.interactable.model = this.model;
|
||||
},
|
||||
onRender: function() {
|
||||
if (this.model.get('children') && this.model.get('children').length) {
|
||||
this.$el.addClass('collapsible');
|
||||
} else {
|
||||
this.$el.removeClass('collapsible');
|
||||
this.$('> .collapse').hide();
|
||||
}
|
||||
this.showChildView(
|
||||
'children',
|
||||
new FoldersView({
|
||||
collection: this.model.get('children'),
|
||||
parentFolder: this.model,
|
||||
selectedFolder: this.selectedFolder,
|
||||
app: this.app
|
||||
})
|
||||
);
|
||||
|
||||
this.$el.removeClass('active');
|
||||
if (this.selectedFolder == this.model.get('id')) {
|
||||
this.$el.addClass('active');
|
||||
}
|
||||
if (this.model.contains(this.selectedFolder)) {
|
||||
this.showChildren();
|
||||
}
|
||||
|
||||
this.$el.removeClass('editing');
|
||||
if (this.editing) {
|
||||
this.$el.addClass('editing');
|
||||
this.$('input').focus();
|
||||
}
|
||||
},
|
||||
select: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.editing) return;
|
||||
this.triggerRoute();
|
||||
},
|
||||
toggleChildren: function(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
if (this.editing) return;
|
||||
this.$el.toggleClass('open');
|
||||
},
|
||||
showChildren: function(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.$el.addClass('open');
|
||||
},
|
||||
onNavigate: function(category, folderId) {
|
||||
if (category !== 'folder') {
|
||||
return;
|
||||
}
|
||||
this.selectedFolder = folderId;
|
||||
this.render();
|
||||
},
|
||||
triggerRoute: function() {
|
||||
Backbone.history.navigate('folder/' + this.model.get('id'), {
|
||||
trigger: true
|
||||
});
|
||||
},
|
||||
toggleActions: function(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.getUI('actionsMenu').toggleClass('open');
|
||||
},
|
||||
closeActions: function(e) {
|
||||
if (this.editing || $.contains(this.getUI('actionsToggle')[0], e.target))
|
||||
return;
|
||||
this.getUI('actionsMenu').removeClass('open');
|
||||
},
|
||||
actionDelete: function(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.model.destroy();
|
||||
},
|
||||
actionEdit: function(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.editing = true;
|
||||
this.render();
|
||||
},
|
||||
onKeyup: function(e) {
|
||||
if (e.which === 13) {
|
||||
this.actionSubmit();
|
||||
}
|
||||
},
|
||||
actionSubmit: function(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.model.set('title', this.$('input.title').val());
|
||||
this.model.save();
|
||||
this.actionCancel();
|
||||
},
|
||||
actionCancel: function(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.editing = false;
|
||||
this.render();
|
||||
},
|
||||
actionAddSubFolder: function(e) {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
this.model.trigger('addSubFolder'); // communicate to AddFolderView
|
||||
this.$el.addClass('collapsible');
|
||||
this.toggleActions();
|
||||
this.showChildren();
|
||||
},
|
||||
onDropActivate: function(e) {
|
||||
if (e.draggable.model instanceof Folder) {
|
||||
this.folderBeingDragged = e.draggable.model;
|
||||
this.$el.addClass('droptarget-folder');
|
||||
return;
|
||||
}
|
||||
if (this.$el.hasClass('active')) return;
|
||||
this.$el.addClass('droptarget-bookmark');
|
||||
},
|
||||
onMouseOver: function(e) {
|
||||
if (
|
||||
this.$el.hasClass('droptarget-bookmark') &&
|
||||
this.model.get('children').length
|
||||
) {
|
||||
this.showChildren();
|
||||
}
|
||||
if (this.folderBeingDragged) {
|
||||
this.showChildren();
|
||||
}
|
||||
},
|
||||
onMouseOut: function(e) {
|
||||
if (
|
||||
this.$el.hasClass('droptarget-bookmark') &&
|
||||
this.model.get('children').length
|
||||
) {
|
||||
this.toggleChildren();
|
||||
}
|
||||
|
||||
// Only hide children if this is a folder AND the folder being dragged is not within this one
|
||||
if (
|
||||
this.folderBeingDragged &&
|
||||
!this.model.contains(this.folderBeingDragged.get('id'))
|
||||
) {
|
||||
this.toggleChildren();
|
||||
}
|
||||
},
|
||||
onDropDeactivate: function(e) {
|
||||
// TODO: Wait for 'sync' til we remove this
|
||||
this.folderBeingDragged = false;
|
||||
this.$el.removeClass('droptarget-bookmark');
|
||||
this.$el.removeClass('droptarget-folder');
|
||||
},
|
||||
onDrop: function(e) {
|
||||
if (!(e.draggable.model instanceof Folder)) {
|
||||
this.onDropBookmark(e);
|
||||
} else {
|
||||
this.onDropFolder(e);
|
||||
}
|
||||
},
|
||||
onDropBookmark: function(e) {
|
||||
var that = this;
|
||||
e.draggable.model.trigger('dropped');
|
||||
if (this.app.selectedBookmarks.length) {
|
||||
this.app.selectedBookmarks.models.slice().forEach(function(bm, i) {
|
||||
bm.trigger('unselect');
|
||||
that.moveBookmark(bm);
|
||||
// quiver only once
|
||||
if (i === that.app.selectedBookmarks.length - 1) {
|
||||
bm.once('sync', function() {
|
||||
that.quiver();
|
||||
});
|
||||
}
|
||||
});
|
||||
this.app.selectedBookmarks.reset();
|
||||
return;
|
||||
}
|
||||
this.moveBookmark(e.draggable.model);
|
||||
e.draggable.model.once('sync', function() {
|
||||
that.quiver();
|
||||
});
|
||||
},
|
||||
onDropFolder: function(e) {
|
||||
var that = this;
|
||||
e.draggable.model.trigger('dropped');
|
||||
e.draggable.model.once('sync', function() {
|
||||
that.quiver(function() {
|
||||
that.app.folders.fetch({ reset: true });
|
||||
});
|
||||
});
|
||||
this.moveFolder(e.draggable.model);
|
||||
},
|
||||
quiver: function(cb) {
|
||||
var that = this;
|
||||
that.$('> a').addClass('quiver-vertically');
|
||||
setTimeout(function() {
|
||||
that.$('> a').removeClass('quiver-vertically');
|
||||
cb && cb();
|
||||
}, 600);
|
||||
},
|
||||
moveFolder: function(folder) {
|
||||
folder.set('parent_folder', this.model.get('id'));
|
||||
folder.save();
|
||||
},
|
||||
moveBookmark: function(bm) {
|
||||
var that = this;
|
||||
var folders = bm.get('folders'),
|
||||
isInsideFolder =
|
||||
'undefined' !==
|
||||
typeof this.app.bookmarks.loadingState.get('query').folder;
|
||||
if (isInsideFolder) {
|
||||
if (
|
||||
this.app.bookmarks.loadingState.get('query').folder ===
|
||||
this.model.get('id')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
folders = _.without(
|
||||
folders,
|
||||
this.app.bookmarks.loadingState.get('query').folder
|
||||
);
|
||||
}
|
||||
folders.push(this.model.get('id'));
|
||||
bm.set('folders', folders);
|
||||
if (isInsideFolder) {
|
||||
bm.once('sync', function() {
|
||||
that.app.bookmarks.remove(bm);
|
||||
});
|
||||
}
|
||||
bm.save();
|
||||
},
|
||||
onDragStart: function(e) {
|
||||
this.$el.addClass('dragging');
|
||||
},
|
||||
onDragMove: function(e) {
|
||||
this.$el.offset({ top: e.pageY + 5, left: e.pageX });
|
||||
},
|
||||
onDragEnd: function(e) {
|
||||
this.$el.removeClass('dragging');
|
||||
this.$el.css({ position: 'relative', top: 0, left: 0 });
|
||||
},
|
||||
onDropped: function() {
|
||||
var that = this;
|
||||
this.$el.hide();
|
||||
},
|
||||
onDestroy: function() {
|
||||
this.interactable.unset();
|
||||
}
|
||||
});
|
|
@ -1,71 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import interact from 'interactjs';
|
||||
import Folder from '../models/Folder';
|
||||
import Folders from '../models/Folders';
|
||||
import FolderView from './Folder';
|
||||
import AddFolderView from './AddFolder';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.CollectionView.extend({
|
||||
childView: FolderView,
|
||||
tagName: 'ul',
|
||||
className: 'folders',
|
||||
initialize: function(options) {
|
||||
this.app = options.app;
|
||||
this.parentFolder = options.parentFolder;
|
||||
this.selectedFolder = options.selectedFolder;
|
||||
if (!this.parentFolder) {
|
||||
this.parentFolder = new Folder({
|
||||
id: '-1',
|
||||
title: t('bookmarks', 'Uncategorized')
|
||||
});
|
||||
this.isRootFolder = true;
|
||||
}
|
||||
},
|
||||
childViewOptions: function() {
|
||||
return {
|
||||
selectedFolder: this.selectedFolder,
|
||||
app: this.app
|
||||
};
|
||||
},
|
||||
onRender: function() {
|
||||
var length = this.collection.length;
|
||||
if (this.isRootFolder) {
|
||||
this.addChildView(
|
||||
new RootFolderView({ app: this.app, model: this.parentFolder }),
|
||||
0
|
||||
);
|
||||
length++;
|
||||
}
|
||||
this.addChildView(
|
||||
new AddFolderView({
|
||||
parentFolder: this.parentFolder,
|
||||
collection: this.collection
|
||||
}),
|
||||
length
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var RootFolderView = FolderView.extend({
|
||||
initInteractable: function() {
|
||||
this.interactable = interact(this.el).dropzone({
|
||||
ondrop: this.onDrop.bind(this),
|
||||
ondropactivate: this.onDropActivate.bind(this),
|
||||
ondropdeactivate: this.onDropDeactivate.bind(this)
|
||||
});
|
||||
this.interactable.model = this.model;
|
||||
},
|
||||
onRender: function() {
|
||||
this.$el.removeClass('active');
|
||||
if (this.selectedFolder == this.model.get('id')) {
|
||||
this.$el.addClass('active');
|
||||
}
|
||||
this.$('.app-navigation-entry-utils').hide();
|
||||
this.$('.collapse').hide();
|
||||
},
|
||||
onMouseOver: function() {},
|
||||
onMouseOut: function() {}
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import templateString from '../templates/MobileNav.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
className: 'mobile-nav',
|
||||
template: _.template(templateString),
|
||||
events: {
|
||||
'click .toggle-menu': 'toggleMenu'
|
||||
},
|
||||
toggleMenu: function(e) {
|
||||
e.preventDefault();
|
||||
$('body').toggleClass('mobile-nav-open');
|
||||
}
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import TagsManagementView from './TagsManagement';
|
||||
import FoldersView from './Folders';
|
||||
import AddBookmarkView from './AddBookmark';
|
||||
import SettingsView from './Settings';
|
||||
import Folder from '../models/Folder';
|
||||
import interact from 'interactjs';
|
||||
import templateString from '../templates/Navigation.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
className: 'navigation',
|
||||
id: 'app-navigation',
|
||||
tagName: 'div',
|
||||
template: _.template(templateString),
|
||||
events: {
|
||||
'click .all': 'onClick',
|
||||
'click .untagged': 'onClick',
|
||||
'click .favorites': 'onClick',
|
||||
'click .shared': 'onClick',
|
||||
'click .folders': 'onClick',
|
||||
'click .tags': 'onClick'
|
||||
},
|
||||
regions: {
|
||||
addBookmarks: {
|
||||
el: '#add-bookmark-slot',
|
||||
replaceElement: true
|
||||
},
|
||||
folders: {
|
||||
el: '#folders-slot',
|
||||
replaceElement: true
|
||||
},
|
||||
tags: {
|
||||
el: '#favorite-tags-slot',
|
||||
replaceElement: true
|
||||
},
|
||||
settings: {
|
||||
el: '#settings-slot',
|
||||
replaceElement: true
|
||||
}
|
||||
},
|
||||
initialize: function(opt) {
|
||||
this.app = opt.app;
|
||||
this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate, this);
|
||||
},
|
||||
onRender: function() {
|
||||
this.showChildView('addBookmarks', new AddBookmarkView());
|
||||
this.showChildView(
|
||||
'folders',
|
||||
new FoldersView({
|
||||
collection: this.app.folders,
|
||||
app: this.app,
|
||||
selectedFolder: this.selectedFolder
|
||||
})
|
||||
);
|
||||
this.showChildView(
|
||||
'tags',
|
||||
new TagsManagementView({ collection: this.app.tags })
|
||||
);
|
||||
this.showChildView(
|
||||
'settings',
|
||||
new SettingsView({ app: this.app, model: this.app.settings })
|
||||
);
|
||||
},
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
var $li = this.$(e.target).closest('li');
|
||||
if ($li.hasClass('collapsible')) {
|
||||
this.$('li')
|
||||
.removeClass('open')
|
||||
.removeClass('active');
|
||||
$li.addClass('active');
|
||||
$li.toggleClass('open');
|
||||
return;
|
||||
}
|
||||
Backbone.history.navigate(e.target.parentNode.dataset.id, {
|
||||
trigger: true
|
||||
});
|
||||
},
|
||||
onNavigate: function(category, id) {
|
||||
$('.active', this.$el).removeClass('active');
|
||||
var $li = this.$('[data-id=' + category + ']');
|
||||
if (category && $li.length) {
|
||||
$li.addClass('active');
|
||||
if ($li.hasClass('collapsible')) {
|
||||
this.$('li').removeClass('open');
|
||||
$li.addClass('open');
|
||||
}
|
||||
}
|
||||
if (category === 'folder') {
|
||||
this.selectedFolder = id;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,38 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
el: '#searchbox',
|
||||
initialize: function() {
|
||||
var that = this;
|
||||
// register a dummy search plugin
|
||||
new OCA.Search(
|
||||
function(query) {
|
||||
that.submit(query);
|
||||
},
|
||||
function() {
|
||||
that.submit('');
|
||||
}
|
||||
);
|
||||
this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate, this);
|
||||
},
|
||||
events: {
|
||||
keydown: 'onKeydown'
|
||||
},
|
||||
onRender: function() {
|
||||
this.$el.show();
|
||||
},
|
||||
onNavigate: function(route, query) {
|
||||
if (route === 'search/:query') this.$el.val(decodeURIComponent(query));
|
||||
},
|
||||
submit: function(query) {
|
||||
if (query !== '') {
|
||||
query = encodeURIComponent(query);
|
||||
Backbone.history.navigate('search/' + query, { trigger: true });
|
||||
} else {
|
||||
Backbone.history.navigate('all', { trigger: true });
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,200 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Settings from '../models/Settings';
|
||||
import templateString from '../templates/Settings.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
className: 'settings',
|
||||
id: 'app-settings',
|
||||
template: _.template(templateString),
|
||||
ui: {
|
||||
content: '#app-settings-content',
|
||||
bookmarklet: '.bookmarklet',
|
||||
import: '.import',
|
||||
form: '.import-form',
|
||||
iframe: '.upload',
|
||||
status: '.import-status',
|
||||
sort: '#sort',
|
||||
title: '#title',
|
||||
added: '#added',
|
||||
clickcount: '#clickcount',
|
||||
lastmodified: '#lastmodified',
|
||||
rsslink: '.rss-link',
|
||||
viewMode: '.view-mode',
|
||||
list: '#list',
|
||||
grid: '#grid',
|
||||
clearData: '.clear-data'
|
||||
},
|
||||
events: {
|
||||
'click .settings-button': 'open',
|
||||
'click @ui.bookmarklet': 'bookmarkletClick',
|
||||
'click .import-facade': 'importTrigger',
|
||||
'change @ui.import': 'importSubmit',
|
||||
'load @ui.iframe': 'importResult',
|
||||
'click .export': 'exportTrigger',
|
||||
'change @ui.sort': 'setSorting',
|
||||
'focus @ui.rsslink': 'clickRssLink',
|
||||
'change @ui.viewMode': 'changeViewMode',
|
||||
'click @ui.clearData': 'deleteAllBookmarks'
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.app = options.app;
|
||||
this.listenTo(this.model, 'change:sorting', this.getSorting);
|
||||
this.listenTo(this.model, 'change:viewMode', this.getViewMode);
|
||||
this.listenTo(this.app.bookmarks.loadingState, 'change:query', this.render);
|
||||
},
|
||||
onRender: function() {
|
||||
const bookmarkletUrl =
|
||||
window.location.origin +
|
||||
OC.getRootPath() +
|
||||
'/index.php/apps/bookmarks/bookmarklet';
|
||||
const bookmarkletSrc = `javascript:(function(){var a=window,b=document,c=encodeURIComponent,e=c(document.title),d=a.open('${bookmarkletUrl}?output=popup&url='+c(b.location)+'&title='+e,'bkmk_popup','left='+((a.screenX||a.screenLeft)+10)+',top='+((a.screenY||a.screenTop)+10)+',height=500px,width=550px,resizable=1,alwaysRaised=1');a.setTimeout(function(){d.focus()},300);})();`;
|
||||
this.getUI('bookmarklet').prop('href', bookmarkletSrc);
|
||||
|
||||
const rssURL =
|
||||
window.location.origin +
|
||||
OC.getRootPath() +
|
||||
'/index.php/apps/bookmarks/public/rest/v2/bookmark?' +
|
||||
$.param(
|
||||
Object.assign({}, this.app.bookmarks.loadingState.get('query'), {
|
||||
format: 'rss',
|
||||
page: -1
|
||||
})
|
||||
);
|
||||
this.getUI('rsslink').val(rssURL);
|
||||
},
|
||||
open: function(e) {
|
||||
e.preventDefault();
|
||||
this.getUI('content').slideToggle();
|
||||
},
|
||||
bookmarkletClick: function(e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
importTrigger: function(e) {
|
||||
e.preventDefault();
|
||||
this.getUI('import').click();
|
||||
},
|
||||
importSubmit: function(e) {
|
||||
var that = this;
|
||||
e.preventDefault();
|
||||
if (typeof window.fetch !== 'undefined') {
|
||||
// If we have fetch() do a little hapiness dance and go!
|
||||
var data = new FormData();
|
||||
data.append('bm_import', this.getUI('import')[0].files[0]);
|
||||
fetch(this.getUI('form').attr('action'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
requesttoken: oc_requesttoken
|
||||
},
|
||||
body: data,
|
||||
mode: 'same-origin',
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(function(res) {
|
||||
if (!res.ok) {
|
||||
if (res.status === 413) {
|
||||
return { status: 'error', data: ['Selected file is too large'] };
|
||||
}
|
||||
return { status: 'error', data: [res.statusText] };
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(function(json) {
|
||||
that.importResult(JSON.stringify(json));
|
||||
})
|
||||
.catch(function(e) {
|
||||
that.importResult(
|
||||
JSON.stringify({ status: 'error', data: [e.message] })
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// If we don't have fetch() ask grandpa iframe to send it
|
||||
this.getUI('iframe').load(function() {
|
||||
that.importResult(
|
||||
that
|
||||
.getUI('iframe')
|
||||
.contents()
|
||||
.text()
|
||||
);
|
||||
});
|
||||
this.getUI('form').submit();
|
||||
}
|
||||
this.$('.import-facade .icon-upload')
|
||||
.removeClass('icon-upload')
|
||||
.addClass('icon-loading-small');
|
||||
},
|
||||
importResult: function(data) {
|
||||
this.$('.import-facade .icon-upload')
|
||||
.addClass('icon-upload')
|
||||
.removeClass('icon-loading-small');
|
||||
try {
|
||||
data = $.parseJSON(data);
|
||||
} catch (e) {
|
||||
this.getUI('status').text(
|
||||
t('bookmarks', 'Error parsing the import result')
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (data.status == 'error') {
|
||||
var list = $('<ul></ul>').addClass('setting_error_list');
|
||||
console.log(data);
|
||||
$.each(data.data, function(index, item) {
|
||||
list.append($('<li></li>').text(item));
|
||||
});
|
||||
this.getUI('status').html(list);
|
||||
return;
|
||||
}
|
||||
this.getUI('status').text(t('bookmarks', 'Import completed successfully.'));
|
||||
Backbone.history.navigate('', { trigger: true }); // reload app
|
||||
this.app.folders.fetch();
|
||||
},
|
||||
exportTrigger: function() {
|
||||
window.location =
|
||||
'bookmark/export?requesttoken=' + encodeURIComponent(oc_requesttoken);
|
||||
},
|
||||
getSorting: function() {
|
||||
this.getUI(this.model.get('sorting')).prop('selected', true);
|
||||
},
|
||||
setSorting: function(e) {
|
||||
e.preventDefault();
|
||||
var select = document.getElementById('sort');
|
||||
var value = select.options[select.selectedIndex].value;
|
||||
this.model.setSorting(value);
|
||||
},
|
||||
getViewMode: function() {
|
||||
this.getUI(this.model.get('viewMode')).prop('selected', true);
|
||||
},
|
||||
changeViewMode: function() {
|
||||
this.model.setViewMode(this.getUI('viewMode').val());
|
||||
},
|
||||
clickRssLink: function() {
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.getUI('rsslink').select();
|
||||
}, 100);
|
||||
},
|
||||
deleteAllBookmarks: function() {
|
||||
var app = this.app;
|
||||
if (
|
||||
!confirm(
|
||||
t('bookmarks', 'Do you really want to delete all your bookmarks?')
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$.ajax({
|
||||
method: 'DELETE',
|
||||
url: 'bookmark',
|
||||
headers: {
|
||||
requesttoken: oc_requesttoken
|
||||
},
|
||||
success: function() {
|
||||
Backbone.history.navigate('dummy', { trigger: true });
|
||||
Backbone.history.navigate('all', { trigger: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import Tags from '../models/Tags';
|
||||
import TagView from './TagsManagementTag';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.CollectionView.extend({
|
||||
childView: TagView,
|
||||
tagName: 'ul',
|
||||
className: 'tags-management',
|
||||
initialize: function(options) {
|
||||
this.selected = new Tags();
|
||||
this.selected.comparator = 'name';
|
||||
|
||||
this.listenTo(this.collection, 'select', this.onSelect);
|
||||
this.listenTo(this.collection, 'unselect', this.onUnselect);
|
||||
this.listenTo(this.collection, 'reset', this.onReset);
|
||||
this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate);
|
||||
this.lastRouteTags = []; // for the below hack
|
||||
},
|
||||
onNavigate: function(category, tags) {
|
||||
// reset selection (needs slice, since we pull the models out from under the loop otherwise)
|
||||
this.selected.slice().forEach(function(t) {
|
||||
t.trigger('unselect', t, true);
|
||||
});
|
||||
if (category !== 'tags') {
|
||||
this.lastRouteTags = []; // for the below hack
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
// select all tags passed by router
|
||||
tags.forEach(function(tagName) {
|
||||
var tag = that.collection.findWhere({ name: tagName });
|
||||
if (!tag) return;
|
||||
|
||||
tag.trigger('select', tag, true);
|
||||
});
|
||||
|
||||
// hack!
|
||||
// this is for when the route is triggered before the tags are loaded
|
||||
this.lastRouteTags = tags;
|
||||
},
|
||||
onReset: function() {
|
||||
var that = this;
|
||||
this.collection.forEach(function(tag) {
|
||||
if (~that.lastRouteTags.indexOf(tag.get('name'))) {
|
||||
// wait for the tag view to render, so it can receive the event
|
||||
setTimeout(function() {
|
||||
tag.trigger('select', tag, true);
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
},
|
||||
onSelect: function(model, silentRoute) {
|
||||
this.selected.add(model);
|
||||
if (!silentRoute) this.triggerRoute();
|
||||
},
|
||||
onUnselect: function(model, silentRoute) {
|
||||
this.selected.remove(model);
|
||||
if (!silentRoute) this.triggerRoute();
|
||||
},
|
||||
triggerRoute: function() {
|
||||
Backbone.history.navigate(
|
||||
'tags/' +
|
||||
this.selected
|
||||
.pluck('name')
|
||||
.map(encodeURIComponent)
|
||||
.join(','),
|
||||
{ trigger: true }
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import templateStringDefault from '../templates/TagsManagementTag_default.html';
|
||||
import templateStringEditing from '../templates/TagsManagementTag_editing.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
className: 'tag-man-item',
|
||||
tagName: 'li',
|
||||
getTemplate: function() {
|
||||
if (this.editing) {
|
||||
return this.templateEditing;
|
||||
}
|
||||
return this.templateDefault;
|
||||
},
|
||||
templateDefault: _.template(templateStringDefault),
|
||||
templateEditing: _.template(templateStringEditing),
|
||||
ui: {
|
||||
actionsMenu: '.app-navigation-entry-menu',
|
||||
actionsToggle: '.app-navigation-entry-utils-menu-button'
|
||||
},
|
||||
events: {
|
||||
click: 'selectSimple',
|
||||
'click @ui.actionsToggle': 'toggleActions',
|
||||
'click .menu-filter-add': 'actionSelect',
|
||||
'click .menu-filter-remove': 'actionUnselect',
|
||||
'click .menu-delete': 'actionDelete',
|
||||
'click .menu-edit': 'actionEdit',
|
||||
'click .action .submit': 'actionSubmit',
|
||||
'click .action .cancel': 'actionCancel'
|
||||
},
|
||||
initialize: function() {
|
||||
this.listenTo(this.model, 'select', this.onSelect);
|
||||
this.listenTo(this.model, 'unselect', this.onUnselect);
|
||||
this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions);
|
||||
},
|
||||
onRender: function() {
|
||||
if (this.selected) {
|
||||
this.$el.addClass('active');
|
||||
} else {
|
||||
this.$el.removeClass('active');
|
||||
}
|
||||
if (this.editing) {
|
||||
this.$('input').focus();
|
||||
}
|
||||
},
|
||||
onSelect: function() {
|
||||
this.selected = true;
|
||||
this.render();
|
||||
},
|
||||
onUnselect: function() {
|
||||
this.selected = false;
|
||||
this.render();
|
||||
},
|
||||
selectSimple: function(e) {
|
||||
e.preventDefault();
|
||||
if (e && !~[this.el, this.$('a')[0], this.$('span')[0]].indexOf(e.target)) {
|
||||
return;
|
||||
}
|
||||
if (this.editing) return;
|
||||
Backbone.history.navigate(
|
||||
'tags/' + encodeURIComponent(this.model.get('name')),
|
||||
{ trigger: true }
|
||||
);
|
||||
},
|
||||
toggleActions: function() {
|
||||
this.getUI('actionsMenu').toggleClass('open');
|
||||
},
|
||||
closeActions: function(e) {
|
||||
if (this.editing || $.contains(this.getUI('actionsToggle')[0], e.target))
|
||||
return;
|
||||
this.getUI('actionsMenu').removeClass('open');
|
||||
},
|
||||
actionSelect: function(e) {
|
||||
this.model.trigger('select', this.model);
|
||||
},
|
||||
actionUnselect: function(e) {
|
||||
this.model.trigger('unselect', this.model);
|
||||
},
|
||||
actionDelete: function() {
|
||||
this.model.destroy();
|
||||
},
|
||||
actionEdit: function() {
|
||||
this.editing = true;
|
||||
this.render();
|
||||
},
|
||||
actionSubmit: function() {
|
||||
this.model.set('name', this.$('input').val());
|
||||
this.model.save();
|
||||
this.actionCancel();
|
||||
},
|
||||
actionCancel: function() {
|
||||
this.editing = false;
|
||||
this.render();
|
||||
}
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
import Backbone from 'backbone';
|
||||
import TagView from '../views/TagsNavigationTag';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.CollectionView.extend({
|
||||
tagName: 'ul',
|
||||
childView: TagView
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import templateString from '../templates/TagsNavigationTag.html';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
className: 'tag-nav-item',
|
||||
tagName: 'li',
|
||||
template: _.template(templateString),
|
||||
events: {
|
||||
'click': 'open'
|
||||
},
|
||||
initialize: function() {
|
||||
this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate, this);
|
||||
},
|
||||
open: function(e) {
|
||||
e.preventDefault();
|
||||
Backbone.history.navigate('tags/' + encodeURIComponent(this.model.get('name')), {trigger: true});
|
||||
},
|
||||
onNavigate: function(category, tags) {
|
||||
this.$el.removeClass('active');
|
||||
if (category === 'tags' && ~tags.indexOf(this.model.get('name'))) {
|
||||
this.$el.addClass('active');
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,63 +0,0 @@
|
|||
import _ from 'underscore';
|
||||
import Backbone from 'backbone';
|
||||
import Tag from '../models/Tag';
|
||||
import Tags from '../models/Tags';
|
||||
|
||||
const Marionette = Backbone.Marionette;
|
||||
const Radio = Backbone.Radio;
|
||||
|
||||
export default Marionette.View.extend({
|
||||
tagName: 'select',
|
||||
template: _.template(''),
|
||||
className: 'tags-selection',
|
||||
events: {
|
||||
'select2:select': 'onAddByUser',
|
||||
'select2:unselect': 'onRemoveByUser'
|
||||
},
|
||||
initialize: function(options) {
|
||||
this.app = options.app;
|
||||
this.selected = options.selected || new Tags();
|
||||
this.selected.comparator = 'name';
|
||||
|
||||
this.listenTo(this.selected, 'add', this.onChangeByAlgo);
|
||||
this.listenTo(this.selected, 'remove', this.onChangeByAlgo);
|
||||
this.listenTo(this.selected, 'reset', this.onChangeByAlgo);
|
||||
},
|
||||
onAttach: function() {
|
||||
if (this.$el.hasClass('select2-hidden-accessible')) {
|
||||
this.$el.select2('destroy');
|
||||
}
|
||||
this.$el
|
||||
.select2({
|
||||
placeholder: t('bookmarks', 'Set tags'),
|
||||
width: '100%',
|
||||
tags: true,
|
||||
multiple: true,
|
||||
tokenSeparators: [','],
|
||||
data: this.app.tags.pluck('name').map(function(name) {
|
||||
return { id: name, text: name };
|
||||
})
|
||||
})
|
||||
.val(this.selected.pluck('name'))
|
||||
.trigger('change');
|
||||
},
|
||||
onDetach: function() {
|
||||
this.$el.select2('destroy');
|
||||
},
|
||||
onAddByUser: function(e) {
|
||||
var that = this;
|
||||
var tag =
|
||||
this.app.tags.get(e.params.data.text) ||
|
||||
new Tag({ name: e.params.data.text, count: 1 });
|
||||
this.selected.add(tag);
|
||||
setTimeout(function() {
|
||||
that.$el.select2('open');
|
||||
}, 150);
|
||||
},
|
||||
onRemoveByUser: function(e) {
|
||||
this.selected.remove(e.params.data.text);
|
||||
},
|
||||
onChangeByAlgo: function(e) {
|
||||
this.$el.val(this.selected.pluck('name')).trigger('change');
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
|
@ -3,7 +3,13 @@
|
|||
"version": "2.0.3",
|
||||
"main": "js/index.js",
|
||||
"scripts": {
|
||||
"build": "webpack"
|
||||
"dev": "webpack --config webpack.dev.js",
|
||||
"watch": "webpack --progress --watch --config webpack.dev.js",
|
||||
"build": "webpack --progress --hide-modules --config webpack.prod.js",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"lint:fix": "eslint --ext .js,.vue src --fix",
|
||||
"stylelint": "stylelint src",
|
||||
"stylelint:fix": "stylelint src --fix"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -13,16 +19,47 @@
|
|||
"url": "https://github.com/nextcloud/bookmarks/issues"
|
||||
},
|
||||
"homepage": "https://github.com/nextcloud/bookmarks#readme",
|
||||
"devDependencies": {
|
||||
"html-loader": "^0.5.5",
|
||||
"webpack": "^3.12.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"backbone": "^1.3.3",
|
||||
"backbone.marionette": "^3.5.1",
|
||||
"interactjs": "^1.5.3",
|
||||
"jquery": "^3.3.1",
|
||||
"select2": "^4.0.6",
|
||||
"underscore": "^1.9.1"
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"nextcloud-axios": "^0.2.0",
|
||||
"nextcloud-server": "^0.15.10",
|
||||
"nextcloud-vue": "^0.11.5",
|
||||
"uuid": "^3.3.2",
|
||||
"vue": "^2.6.10",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
"vue-router": "^3.0.7",
|
||||
"vuex": "^3.1.1",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"babel-eslint": "^10.0.2",
|
||||
"babel-loader": "^8.0.6",
|
||||
"css-loader": "^3.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-import-resolver-webpack": "^0.11.1",
|
||||
"eslint-loader": "^2.2.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-node": "^9.1.0",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.3",
|
||||
"file-loader": "^4.1.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"stylelint": "^8.4.0",
|
||||
"stylelint-config-recommended-scss": "^3.3.0",
|
||||
"stylelint-scss": "^3.9.2",
|
||||
"stylelint-webpack-plugin": "^0.10.5",
|
||||
"vue-loader": "^15.7.1",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"webpack": "^4.37.0",
|
||||
"webpack-cli": "^3.3.5",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
import AppGlobal from './mixins/AppGlobal'
|
||||
import store from './store'
|
||||
import axios from 'nextcloud-axios'
|
||||
|
||||
export default {
|
||||
t: AppGlobal.methods.t,
|
||||
|
||||
url(url) {
|
||||
url = `/apps/bookmarks/public/rest/v2${url}`
|
||||
return OC.generateUrl(url)
|
||||
},
|
||||
|
||||
handleSyncError(message) {
|
||||
OC.Notification.showTemporary(message + ' ' + this.t('bookmarks', 'See JavaScript console for details.'))
|
||||
},
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<AppContentList>
|
||||
<BookmarksListItem
|
||||
v-for="bookmark in bookmarks"
|
||||
:key="bookmark.id"
|
||||
:bookmark="bookmark"
|
||||
@delete-bookmark="$emit('delete-bookmark')"
|
||||
/>
|
||||
</AppContentList>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { AppContentList } from 'nextcloud-vue';
|
||||
import BookmarksListItem from './BookmarksListItem';
|
||||
|
||||
export default {
|
||||
name: 'NavigationList',
|
||||
components: {
|
||||
BookmarksListItem,
|
||||
AppContentList
|
||||
},
|
||||
props: {
|
||||
bookmarks: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
created() {}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div>
|
||||
{{ bookmark.title }}
|
||||
<Action>
|
||||
<ActionButton icon="icon-edit" @click="$emit('edit-bookmark')"
|
||||
>Edit</ActionButton
|
||||
>
|
||||
<ActionButton icon="icon-delete" @click="$emit('delete-bookmark')"
|
||||
>Delete</ActionButton
|
||||
>
|
||||
</Action>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { Action, ActionButton } from 'nextcloud-vue';
|
||||
|
||||
export default {
|
||||
name: 'NavigationList',
|
||||
components: {
|
||||
Action,
|
||||
ActionButton
|
||||
},
|
||||
props: {
|
||||
bookmark: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
created() {}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<Content app-name="bookmarks">
|
||||
<AppNavigation>
|
||||
<AppNavigationNew
|
||||
:text="t('bookmarks', 'New Bookmark')"
|
||||
:disabled="false"
|
||||
button-id="bookmarks-new"
|
||||
button-class="['icon-add', {loading: loading.create}]"
|
||||
@click="onNewBookmark"
|
||||
/>
|
||||
<NavigationList
|
||||
v-show="!loading.folders && !loading.tags"
|
||||
:folders="folders"
|
||||
:tags="tags"
|
||||
@select-folder="onSelectFolder"
|
||||
@select-tag="onSelectTag"
|
||||
/>
|
||||
<Settings @reload="reload" />
|
||||
</AppNavigation>
|
||||
<BookmarksList
|
||||
:loading="loading.bookmarks"
|
||||
:bookmarks="bookmarks"
|
||||
@load-next="onNextPage"
|
||||
@delete-bookmark="onDeleteBookmark"
|
||||
/>
|
||||
</Content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Content, AppNavigation, AppNavigationNew } from 'nextcloud-vue';
|
||||
// import Settings from './Settings';
|
||||
// import NavigationList from './NavigationList';
|
||||
import BookmarksList from './BookmarksList';
|
||||
import { actions } from '../store';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
Content,
|
||||
AppNavigation,
|
||||
AppNavigationNew,
|
||||
// NavigationList,
|
||||
// Settings,
|
||||
BookmarksList
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
loading: {
|
||||
folders: true,
|
||||
tags: true,
|
||||
bookmarks: true,
|
||||
create: false
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
bookmarks() {
|
||||
return this.$store.bookmarks;
|
||||
},
|
||||
folders() {
|
||||
return this.$store.folders;
|
||||
},
|
||||
tags() {
|
||||
return this.$store.tags;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
search(from, to) {
|
||||
this.$store.dispatch(actions.FILTER_BY_SEARCH, to);
|
||||
},
|
||||
|
||||
tags(from, to) {
|
||||
this.$store.dispatch(actions.FILTER_BY_TAGS, to);
|
||||
},
|
||||
|
||||
folderId(from, to) {
|
||||
this.$store.dispatch(actions.FILTER_BY_FOLDER, to);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onNextPage() {
|
||||
this.$store.dispatch(actions.FETCH_PAGE);
|
||||
},
|
||||
|
||||
onNewBookmark(e) {
|
||||
console.debug(e);
|
||||
},
|
||||
|
||||
onSelectTag(tag) {
|
||||
this.$router.push({ name: 'tag', tags: tag });
|
||||
},
|
||||
|
||||
onSelectFolder(folderId) {
|
||||
this.$router.push({ name: 'folder', folderId });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import '@babel/polyfill/noConflict';
|
||||
|
||||
import Vue from 'vue';
|
||||
import App from './App';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import AppGlobal from './mixins/AppGlobal';
|
||||
|
||||
Vue.mixin(AppGlobal);
|
||||
|
||||
export default new Vue({
|
||||
el: '#content',
|
||||
store,
|
||||
router,
|
||||
render: h => h(App)
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
methods: {
|
||||
t,
|
||||
n,
|
||||
OC,
|
||||
OCA
|
||||
}
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import ViewPrivate from './components/ViewPrivate';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
base: OC.generateUrl('/apps/bookmarks'),
|
||||
linkActiveClass: 'active',
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: ViewPrivate
|
||||
},
|
||||
{
|
||||
path: '/search/:search',
|
||||
name: 'home',
|
||||
component: ViewPrivate,
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/folder/:folderId',
|
||||
name: 'folder',
|
||||
component: ViewPrivate
|
||||
},
|
||||
{
|
||||
path: '/tags/:tags',
|
||||
name: 'tags',
|
||||
components: ViewPrivate,
|
||||
props: true
|
||||
}
|
||||
]
|
||||
});
|
|
@ -0,0 +1,158 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import axios from 'nextcloud-axios';
|
||||
import AppGlobal from './mixins/AppGlobal';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const BATCH_SIZE = 42;
|
||||
|
||||
export const mutations = {
|
||||
ADD: 'ADD',
|
||||
REMOVE: 'REMOVE',
|
||||
REMOVE_ALL: 'REMOVE_ALL',
|
||||
SET_SIDEBAR_OPEN: 'SET_SIDEBAR_OPEN',
|
||||
INCREMENT_PAGE: 'INCREMENT_PAGE',
|
||||
SET_QUERY: 'SET_QUERY',
|
||||
SET_SORTBY: 'SET_SORTBY',
|
||||
SET_FETCHING: 'SET_FETCHING',
|
||||
SET_REACHED_END: 'SET_REACHED_END',
|
||||
SET_ERROR: 'SET_ERROR'
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
ADD_ALL: 'ADD_ALL',
|
||||
FILTER_BY_TAGS: 'FILTER_BY_TAGS',
|
||||
FILTER_BY_FOLDER: 'FILTER_BY_FOLDER',
|
||||
FILTER_BY_SEARCH: 'FILTER_BY_SEARCH'
|
||||
};
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
fetchState: {
|
||||
page: 0,
|
||||
query: {},
|
||||
fetching: false,
|
||||
reachedEnd: false,
|
||||
sortby: 'lastmodified'
|
||||
},
|
||||
bookmarks: [],
|
||||
bookmarksById: {},
|
||||
sidebarOpen: false,
|
||||
page: 0
|
||||
},
|
||||
|
||||
getters: {
|
||||
getNote: state => id => {
|
||||
if (state.bookmarksById[id] === undefined) {
|
||||
return null;
|
||||
}
|
||||
return state.bookmarksById[id];
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
[mutations.SET_ERROR](state, error) {
|
||||
state.error = error;
|
||||
},
|
||||
|
||||
[mutations.ADD](state, bookmark) {
|
||||
const existingBookmark = state.bookmarksById[bookmark.id];
|
||||
if (!existingBookmark) {
|
||||
state.bookmarks.push(bookmark);
|
||||
Vue.set(state.bookmarksById, bookmark.id, bookmark);
|
||||
}
|
||||
},
|
||||
|
||||
[mutations.REMOVE](state, id) {
|
||||
const index = state.bookmarks.findIndex(bookmark => bookmark.id === id);
|
||||
if (index !== -1) {
|
||||
state.bookmarks.splice(index, 1);
|
||||
Vue.delete(state.bookmarksById, id);
|
||||
}
|
||||
},
|
||||
|
||||
[mutations.REMOVE_ALL](state) {
|
||||
state.notes = [];
|
||||
state.bookmarksById = {};
|
||||
},
|
||||
|
||||
[mutations.SET_SIDEBAR_OPEN](state, open) {
|
||||
state.sidebarOpen = open;
|
||||
},
|
||||
|
||||
[mutations.INCREMENT_PAGE](state) {
|
||||
Vue.set(state.fetchState, 'page', state.fetchState.page + 1);
|
||||
},
|
||||
|
||||
[mutations.SET_QUERY](state, query) {
|
||||
Vue.set(state.fetchState, 'page', 0);
|
||||
Vue.set(state.fetchState, 'reachedEnd', false);
|
||||
Vue.set(state.fetchState, 'query', query);
|
||||
},
|
||||
|
||||
[mutations.FETCH_START](state) {
|
||||
Vue.set(state.fetchState, 'fetching', true);
|
||||
},
|
||||
|
||||
[mutations.FETCH_END](state) {
|
||||
Vue.set(state.fetchState, 'fetching', false);
|
||||
},
|
||||
|
||||
[mutations.REACHED_END](state) {
|
||||
Vue.set(state.fetchState, 'reachedEnd', true);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
[actions.ADD_ALL]({ commit }, bookmarks) {
|
||||
for (const bookmark of bookmarks) {
|
||||
commit(mutations.ADD, bookmark);
|
||||
}
|
||||
},
|
||||
[actions.FILTER_BY_SEARCH]({ dispatch, commit }, search) {
|
||||
commit(mutations.SET_QUERY, { search: search.split(' ') });
|
||||
return dispatch(actions.FETCH_PAGE);
|
||||
},
|
||||
|
||||
[actions.FILTER_BY_TAGS]({ dispatch, commit }, tags) {
|
||||
commit(mutations.SET_QUERY, { tags });
|
||||
return dispatch(actions.FETCH_PAGE);
|
||||
},
|
||||
[actions.FILTER_BY_FOLDER]({ dispatch, commit }, folder) {
|
||||
commit(mutations.SET_QUERY, { folder });
|
||||
return dispatch(actions.FETCH_PAGE);
|
||||
},
|
||||
[actions.FETCH_PAGE]({ dispatch, commit, state }) {
|
||||
if (state.fetchState.fetching) return;
|
||||
commit(mutations.FETCH_START);
|
||||
return axios
|
||||
.get(url('/bookmark'), {
|
||||
...state.fetchState.query,
|
||||
limit: BATCH_SIZE,
|
||||
page: state.fetchQuery.page
|
||||
})
|
||||
.then(response => {
|
||||
const bookmarks = response.data;
|
||||
commit(mutations.INCREMENT_PAGE);
|
||||
return dispatch(actions.ADD_ALL, bookmarks);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
commit(
|
||||
mutations.SET_ERROR,
|
||||
AppGlobal.t('bookmarks', 'Failed to fetch bookmarks.')
|
||||
);
|
||||
throw err;
|
||||
})
|
||||
.finally(() => {
|
||||
commit(mutations.FETCH_END);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function url(url) {
|
||||
url = `/apps/bookmarks/public/rest/v2${url}`;
|
||||
return OC.generateUrl(url);
|
||||
}
|
|
@ -1,11 +1,5 @@
|
|||
<?php
|
||||
script('bookmarks', 'dist/main.bundle');
|
||||
style('bookmarks', 'bookmarks');
|
||||
|
||||
style('bookmarks', 'select2');
|
||||
script('bookmarks', 'bookmarks');
|
||||
style('bookmarks', 'style');
|
||||
?>
|
||||
|
||||
<div id="navigation-slot">
|
||||
</div>
|
||||
<div id="app-content">
|
||||
</div>
|
||||
<div id="vue-content"></div>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
const StyleLintPlugin = require('stylelint-webpack-plugin')
|
||||
|
||||
module.exports = {
|
||||
entry: path.join(__dirname, 'src', 'main.js'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, './js'),
|
||||
publicPath: '/js/',
|
||||
filename: 'bookmarks.js',
|
||||
chunkFilename: 'chunks/[name].js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['vue-style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'sass-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
use: 'eslint-loader',
|
||||
enforce: 'pre'
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
new StyleLintPlugin(),
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['*', '.js', '.vue', '.json']
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
devtool: '#cheap-source-map',
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
const merge = require('webpack-merge')
|
||||
const common = require('./webpack.common.js')
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'production',
|
||||
devtool: '#source-map'
|
||||
})
|
Loading…
Reference in New Issue