Initial pass at a vue rewrite

This commit is contained in:
Marcel Klehr 2019-07-26 20:16:37 +02:00
parent c6767cf077
commit 0fbe1e82e8
72 changed files with 10135 additions and 3770 deletions

92
.eslintrc.js Normal file
View File

@ -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
}
}
]
}
};

5
.prettierrc.json Normal file
View File

@ -0,0 +1,5 @@
{
"semi": true,
"singleQuote": true,
"useTabs": true
}

26
.stylelint.js Normal file
View File

@ -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']
}

View File

@ -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, $);

View File

@ -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}));
},
});

View File

@ -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();
}
});

View File

@ -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;
}
});

View File

@ -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();
});

View File

@ -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();
});

View File

@ -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';
}
});

View File

@ -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);
}
}
});
}
});

View File

@ -1,4 +0,0 @@
import Backbone from 'backbone';
import { Folder } from './Folders';
export default Folder;

View File

@ -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;

View File

@ -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 });
}
});
}
});

View File

@ -1,6 +0,0 @@
import Backbone from 'backbone';
export default Backbone.Model.extend({
idAttribute: 'name',
urlRoot: 'tag'
});

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1 +0,0 @@
<div class="icon-loading"></div>

View File

@ -1 +0,0 @@
<a href="#" class="icon-menu toggle-menu" title="<%- t('bookmarks', 'Open main menu') %>"></a>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1 +0,0 @@
<a href="#" title="Other bookmarks with this tag"><%- name %></a>

View File

@ -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));
};

View File

@ -1,3 +0,0 @@
import interact from 'interactjs';
interact.dynamicDrop(true);
interact.pointerMoveTolerance(20);

View File

@ -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);
}

View File

@ -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;
}
});

View File

@ -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);
}
}
});

View File

@ -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 }));
}
});

View File

@ -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();
}
});

View File

@ -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);
}
});

View File

@ -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)
});
}
});

View File

@ -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() {}
});

View File

@ -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');
}
});

View File

@ -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');
}
});

View File

@ -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();
});
}
});

View File

@ -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);
}
});

View File

@ -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();
}
});

View File

@ -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() {}
});

View File

@ -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');
}
});

View File

@ -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();
}
}
});

View File

@ -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 });
}
}
});

View File

@ -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 });
}
});
}
});

View File

@ -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 }
);
}
});

View File

@ -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();
}
});

View File

@ -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
});

View File

@ -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');
}
}
});

View File

@ -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');
}
});

10426
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

9
src/App.vue Normal file
View File

@ -0,0 +1,9 @@
<template>
<router-view />
</template>
<script>
export default {
name: 'App'
};
</script>

19
src/BookmarksService.js Normal file
View File

@ -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.'))
},
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

37
src/main.js Normal file
View File

@ -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)
});

8
src/mixins/AppGlobal.js Normal file
View File

@ -0,0 +1,8 @@
export default {
methods: {
t,
n,
OC,
OCA
}
};

35
src/router.js Normal file
View File

@ -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
}
]
});

158
src/store.js Normal file
View File

@ -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);
}

View File

@ -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>

55
webpack.common.js Normal file
View File

@ -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']
}
}

7
webpack.dev.js Normal file
View File

@ -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',
})

7
webpack.prod.js Normal file
View File

@ -0,0 +1,7 @@
const merge = require('webpack-merge')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production',
devtool: '#source-map'
})