From f2221efe7a1f3625e76b2b5f317276216238a7b8 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 7 Sep 2017 18:48:13 +0200 Subject: [PATCH 001/110] First try of backbone/marionette framework --- css/bookmarks.css | 450 ++--------------------------------------- js/bookmarks.js | 491 ++++++--------------------------------------- templates/main.php | 3 + 3 files changed, 78 insertions(+), 866 deletions(-) diff --git a/css/bookmarks.css b/css/bookmarks.css index 7e553dc6b..b56f3b721 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -1,438 +1,14 @@ -input.disabled, input.disabled:hover, input.disabled:focus { - cursor: not-allowed; - background-color: #ddd; - color: gray; -} -#settingsbtn { background: transparent; color:#666; - border-bottom: 1px solid #ddd; border-top: 1px solid #fff; } -#settingsbtn:hover { background:#dbdbdb !important; color:#999; - border-bottom: 1px solid #CCCCCC; border-top: 1px solid #D4D4D4; } -#settingsbtn img { width: 18px; height: 18px; margin: 10px; } - - -.bookmarks_headline { - font-size: large; - font-weight: bold; - margin-left: 2em; - padding: 2.5ex 0.5ex; -} - -.bookmarks_menu { - margin-left: 1.5em; - padding: 0.5ex; -} - -.bookmarks_list, #firstrun { - overflow: auto; - -moz-box-sizing: border-box; - box-sizing: border-box; - height: 100%; -} - -#add_form { - padding-bottom: 2em; -} - -#add_form_loading { - visibility: hidden; - float: right; - margin: 8px; -} - -#add_form input { - margin: 0.3em; -} -#add_url { - width: 98%; -} -.bookmarks_addBml { - text-decoration: underline; -} -#bookmark_add_submit { - background-color: transparent; - border: none; - position: absolute; - right: 0; - top: 0; - opacity: .5; - padding: 17px 14px; -} - -.bookmarks_label { - width: 7em; - display: inline-block; - text-align: right; -} - -.bookmarks_input { - width: 8em; -} - -.bookmark_actions { - position: absolute; - right: 1em; - top: 12px; - display: none; -} -.bookmark_actions span { margin: 0 0.4em; } -.bookmark_actions img { opacity: 0.3;} -.bookmark_actions img:hover { opacity: 1; cursor: pointer; } -.bookmark_edit img {opacity:0.3;} -.bookmark_edit img:hover {cursor: pointer; opacity: 1;} - -.bookmark_single { - position: relative; - padding: 0.2em 1em; - min-height: 3em; - border-bottom: 1px solid #DDD; - -webkit-transition:background-color 500ms; -moz-transition:background-color 500ms; -o-transition:background-color 500ms; transition:background-color 500ms; -} - -.bookmark_single:hover { - background-color:#f8f8f8; -} - -.bookmark_single:hover .bookmark_actions { - display: block; -} - -.bookmark_title { - font-weight: bold; - display: block; - margin-right: 0.8em; - margin-top:0.7em; -} - -.bookmark_title a { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - display: inline-block; - max-width: 74%; -} - -.bookmark_single:hover .bookmark_title .bookmark_edit_btn { - display:inline-block; -} - -.bm_view_list .bookmark_tags { - position: absolute; - top: 0.5em; - right: 6em; - text-align: right; -} - -#app-navigation > ul > li { - display: block !important; -} - -.bookmark_tags { - left:0%; - position: relative; - width: 75%; -} -.bookmark_tag { - display: inline-block; - color: white; - margin: 0.2em 0.2em; - padding: 0 0.4em; - background-color: #1D2D44; - border-radius: 0.4em; - opacity: 0.2; -} -.bookmark_tag:hover { opacity: 0.5; } - -.loading_meta { - display: none; - margin-left: 5px; -} - -.addBm { - background: none repeat scroll 0 0 #F8F8F8; - border-radius: 0.5em 0.5em 0.5em 0.5em; - color: #555555; - margin: 1em; - padding: 0.5em 1em; - text-shadow: 0 1px 0 #FFFFFF; - width: 500px; - -} -.bm_desc { - width:90%; -} -.addBm h1 { - font-weight: bold; - border-bottom: 1px solid #BABABA; -} - - -.addBm fieldset > ul > li { - margin-top: 1em; -} -.addBm label { - display:block; - width:100%; -} -.addBm .is_public_label { - display: inline; - -} -.addBm fieldset input[type="text"], .addBm textarea { - width:100%; -} -.addBm textarea{ - min-width:250px; -} -.addBm .close_btn -{ - height: 18px; - margin: -20px 0 0; - padding: 1px; - float:right; - width: 19px; -} -.addBm .submit { - float: right; -} - -.addBm ul.tagit { background:white; } - -.addBm input.ui-autocomplete-input, #tag_filter input.ui-autocomplete-input, .bookmark_form_tags input.ui-autocomplete-input{ - box-shadow:none; - -} - -ul.tagit li.tagit-new { - padding-left: 0em; -} - -#tag_select_label{ - display: block; - text-align: center; - margin-top: 10px; - cursor: default; -} - -li.tag_list { - padding-left: 3px; - padding-right: 3px; - overflow: hidden; -} - -li.tag_list a { - display: inline !important; - padding: 5px 11px 5px 11px !important; - border: 1px solid transparent; -} - -li.tag_list li:hover > a { - background: none repeat scroll 0 0 #dee7f8 !important; - border: 1px solid #cad8f3; - border-radius: 6px; -} - -.tag_list em , .share_list em{ - float:right; - display:block; -} - -.tags_actions { - margin-left: 0.5em; - display: inline; - line-height: 44px; -} - -.tags_actions > span { - display:none; - float:right; -} -li:hover > .tags_actions > span { - display:inline; -} -li:hover > .tags_actions > em { display : none; } - -#tag_filter li, #tag_filter li:hover { - background: none repeat scroll 0 0 #F8F8F8; -} -#tag_filter .tagit { - display: inline-block; - margin: 0.3em; -} -#tag_filter ul.tagit > li.tagit-new { - padding:0; -} -#tag_filter ul.tagit li.tagit-choice { - background: none repeat scroll 0 0 #DEE7F8; - padding: 0.2em 18px 0.2em 0.5em; - width: auto; -} - -#tag_filter a { - display: inline; - float: none; - margin: 0; - padding: 0; -} - -#tag_filter ul li:hover > a { - background-color: transparent; -} - -#tag_filter ul.tagit li.tagit-choice .close{ - margin-top: -8px; - cursor: pointer; - margin-top: -8px; - top: 50%; - padding-left: 0px !important; -} - -#tag_filter ul.tagit li.tagit-choice .close span{ - text-align: right; -} - -ul.tagit li.tagit-choice .close .text-icon { - display: block !important; -} - -.bookmark_desc{ - padding-left:0.25em; - padding-bottom:0.25em; - white-space:nowrap; - text-overflow:ellipsis; - overflow:hidden; - display:block; - font-weight:500; - /* z-index: -10; */ -} -.bookmark_date, .bookmark_submit { - font-size:small; - position: absolute; - right: 6em; - color:gray; - /* margin-top:2.5em; */ - margin-right: 0.2em; - top:0.8em; -} - -/* -.bm_view_img .bookmark_single:hover .bookmark_url{ - display:block; -} -.bm_view_img .bookmark_single .bookmark_url{ - display:block; -} -*/ -.bookmark_single:hover .bookmark_url { display: inline; } -.bookmark_single:hover .bookmark_url a[href]{ color:gray; } - -.bookmark_url { display: none; } -.bookmark_form_title { - float: left; - width:25%; -} - -.bookmark_form_url { - float:left; - width:25%; -} - -.bookmark_form_tags { - width:30%; - float:left; -} - - -.bookmark_form_desc { - width:50%; - padding-top:100px; -} - -.bookmark_form_desc textarea { - margin-top:-3.4em; -} - -.bookmark_form_title input, .bookmark_form_url input { - width: 89%; -} -.bookmark_form_desc textarea { - width: 94.5%; -} - -.bookmark_single .bookmark_edit_btn { - visibility:hidden; - width: 16px; -} - -.bookmark_single:hover .bookmark_edit_btn { - visibility: visible; -} - -.bookmark_single:focus .bookmark_edit_btn { - visibility: visible; - opacity:1; -} - -.bookmark_single_form { - padding: 20px 8px; - border-bottom: 1px solid #DDD; -} - -.bookmark_single_form .tagit{ - /* width: 80%; */ - box-shadow: 0 1px 1px #FFFFFF, 0 1px 0 #BBBBBB inset; - /* margin: 3px; */ - /* height:6.4em; */ -} -.bookmark_form_submit { - margin-left: 4px; -} -.bookmark_form_submit .reset { - float: right; -} - -.bm_view_img .bookmark_actions { - bottom: 0.7em; - display: block; - position: absolute; - right: 1em; - top: auto; -} - -#bm_setting_panel { - background-color: #eee; - padding: 1em; - height: 100%; - /* display:none; */ -} -/* .open #bm_setting_panel { display:block; } */ - -#bm_setting_panel legend{ - margin-top: 0.5em; -} - -#bookmarklet_hint { - margin-bottom: 6px; -} - -#bm_import { - opacity: 0; - position: absolute; - width: 90px; -} - -#bm_import:hover ~ button#bm_import_submit { - background-color: rgba(250, 250, 250, 0.9); - color: #333; -} - -#import_bookmark { - margin-top: 10px; -} - -#import_bookmark .personalblock { - margin-top: 1em; -} -#app-navigation a.bookmarklet { - margin-top: 5px; - padding: 0 5px 2px; +.bookmarks { + padding: .5cm; +} +.bookmark-card { + width: 5cm; + height: 5cm; + float: left; + margin: 0 .5cm .5cm 0; +} +.bookmark-card h1 { + font-weight: bold; + font-family: sans-serif; + font-size: .3cm; } diff --git a/js/bookmarks.js b/js/bookmarks.js index 5bbff687c..219f2fffd 100644 --- a/js/bookmarks.js +++ b/js/bookmarks.js @@ -1,429 +1,62 @@ -var bookmarksPage = 0; -var bookmarksLoading = false; -var dialog; -var bookmarksSorting = 'bookmarks_sorting_recent'; -var fullTags = []; -var ajaxCallCount = 0; - -$(document).ready(function () { - getTags(); - watchUrlField(); - $('#bm_import').change(attachSettingEvent); - $('#add_url').on('keydown keyup change click', watchUrlField); - $('#app-settings').on('click keydown', toggleSettings); - $('#bm_export').click(exportBm); - $('#emptycontent-setting').click(function () { - if (!$('#app-settings').hasClass('open')) { - $('#app-settings').click(); - } - }); - $('.bookmarks_list').scroll(updateOnBottom).empty(); - $('#tag_filter input').tagit({ - allowSpaces: true, - availableTags: fullTags, - onTagFinishRemoved: filterTagsChanged, - placeholderText: t('bookmarks', 'Filter by tag') - }).tagit('option', 'onTagAdded', filterTagsChanged); - getBookmarks(); -}); - -function getTags() { - jQuery.ajax({ - url: 'tag', - success: function (result) { - fullTags = result; - }, - async: false - }); -} - -var formatString = (function () { - var replacer = function (context) { - return function (s, name) { - return context[name]; - }; - }; - - return function (input, context) { - return input.replace(/\{(\w+)\}/g, replacer(context)); - }; -})(); - -function increaseAjaxCallCount() { - ajaxCallCount++; - if (ajaxCallCount - 1 === 0) { - updateLoadingAnimation(); - } -} - -function decreaseAjaxCallCount() { - if (ajaxCallCount > 0) { - ajaxCallCount--; - updateLoadingAnimation(); - } -} - -function updateLoadingAnimation() { - if (ajaxCallCount === 0) { - $('#bookmark_add_submit').removeClass('icon-loading-small'); - $('#bookmark_add_submit').addClass('icon-add'); - } else { - $('#bookmark_add_submit').removeClass('icon-add'); - $('#bookmark_add_submit').addClass('icon-loading-small'); - } -} - -function watchClickInSetting(e) { - if ($('#app-settings').find($(e.target)).length === 0) { - toggleSettings(); - } -} - -function checkURL(url) { - if (url.substring(0, 3) === "htt") { - return url; - } - return "http://" + url; -} - -function toggleSettings() { - if ($('#app-settings').hasClass('open')) { //Close - $('#app-settings').switchClass("open", ""); - $('body').unbind('click', watchClickInSetting); - } - else { - $('#app-settings').switchClass("", "open"); - $('body').bind('click', watchClickInSetting); - } -} -function addFilterTag(event) { - event.preventDefault(); - $('#tag_filter input').tagit('createTag', $(this).text()); -} - -function updateTagsList(tag) { - var html = tmpl("tag_tmpl", tag); - $('.tag_list').append(html); -} - -function filterTagsChanged() -{ - $('#bookmarkFilterTag').val($('#tag_filter input').val()); - $('.bookmarks_list').empty(); - bookmarksPage = 0; - getBookmarks(); -} -function getBookmarks() { - if (bookmarksLoading) { - //have patience :) - return; - } - increaseAjaxCallCount(); - bookmarksLoading = true; - //Update Rel Tags if first page - if (bookmarksPage === 0) { - - $.ajax({ - type: 'GET', - url: 'bookmark', - data: {type: 'rel_tags', tag: $('#bookmarkFilterTag').val(), page: bookmarksPage, sort: bookmarksSorting}, - success: function (tags) { - $('.tag_list').empty(); - for (var i in tags.data) { - updateTagsList(tags.data[i]); - } - $('.tag_list .tag_edit').click(renameTag); - $('.tag_list .tag_delete').click(deleteTag); - $('.tag_list a.tag').click(addFilterTag); - - - } - }); - } - $.ajax({ - type: 'GET', - url: 'bookmark', - data: {type: 'bookmark', tag: $('#bookmarkFilterTag').val(), page: bookmarksPage, conjunction: 'and', sort: bookmarksSorting}, - complete: function () { - decreaseAjaxCallCount(); - }, - success: function (bookmarks) { - if (bookmarks.data.length) { - bookmarksPage += 1; - } - $('.bookmark_link').unbind('click', recordClick); - $('.bookmark_delete').unbind('click', delBookmark); - $('.bookmark_edit').unbind('click', editBookmark); - - for (var i in bookmarks.data) { - updateBookmarksList(bookmarks.data[i]); - } - checkEmpty(); - - $('.bookmark_link').click(recordClick); - $('.bookmark_delete').click(delBookmark); - $('.bookmark_edit').click(editBookmark); - - bookmarksLoading = false; - if (bookmarks.data.length) { - updateOnBottom(); - } - } - }); -} - -function watchUrlField() { - var form = $('#add_form'); - var el = $('#add_url'); - var button = $('#bookmark_add_submit'); - form.unbind('submit'); - if (!acceptUrl(el.val())) { - form.bind('submit', function (e) { - e.preventDefault(); - }); - button.addClass('disabled'); - } - else { - button.removeClass('disabled'); - form.bind('submit', addBookmark); - } -} - -function acceptUrl(url) { - return url.replace(/^\s+/g, '').replace(/\s+$/g, '') !== ''; -} - -function addBookmark(event) { - event.preventDefault(); - var url = $('#add_url').val(); - //If trim is empty - if (!acceptUrl(url)) { - return; - } - - $('#add_url').val(''); - var bookmark = {url: url, description: '', title: '', from_own: 0, added_date: new Date()}; - increaseAjaxCallCount(); - $.ajax({ - type: 'POST', - url: 'bookmark', - data: bookmark, - complete: function () { - decreaseAjaxCallCount(); - }, - success: function (data) { - if (data.status === 'success') { - // First remove old BM if exists - $('.bookmark_single').filterAttr('data-id', data.item.id).remove(); - - var bookmark = $.extend({}, bookmark, data.item); - updateBookmarksList(bookmark, 'prepend'); - checkEmpty(); - watchUrlField(); - } - }, - error: function () { - OC.Notification.showTemporary(t('bookmarks', 'Could not add bookmark.')); - } - }); -} - -function delBookmark() { - var record = $(this).parent().parent(); - OC.dialogs.confirm(t('bookmarks', 'Are you sure you want to remove this bookmark?'), - t('bookmarks', 'Warning'), function (answer) { - if (answer) { - $.ajax({ - type: 'DELETE', - url: 'bookmark/' + record.data('id'), - success: function (data) { - if (data.status === 'success') { - record.remove(); - checkEmpty(); - } - } - }); - } - }); -} - -function checkEmpty() { - if ($('.bookmarks_list').children().length === 0) { - $("#emptycontent").show(); - $("#bm_export").addClass('disabled'); - $('.bookmarks_list').hide(); - } else { - $("#emptycontent").hide(); - $("#bm_export").removeClass('disabled'); - $('.bookmarks_list').show(); - } -} -function editBookmark() { - if ($('.bookmark_single_form').length) { - $('.bookmark_single_form .reset').click(); - } - var record = $(this).parent().parent(); - var bookmark = record.data('record'); - var html = tmpl("item_form_tmpl", bookmark); - - record.after(html); - record.hide(); - var rec_form = record.next().find('form'); - rec_form.find('.bookmark_form_tags ul').tagit({ - allowSpaces: true, - availableTags: fullTags, - placeholderText: t('bookmarks', 'Tags') - }); - - rec_form.find('.reset').bind('click', cancelBookmark); - rec_form.bind('submit', function (event) { - event.preventDefault(); - var form_values = $(this).serialize(); - if(form_values.indexOf('item%5Btags%5D') === -1) { - // if not tag is selected, the input field does not exist, so - // we need to manually insert an empty array - form_values += "&item%5Btags%5D%5B%5D="; - } - $.ajax({ - type: 'PUT', - url: $(this).attr('action') + "/" + this.elements['record_id'].value, - data: form_values, - success: function (data) { - if (data.status === 'success') { - //@TODO : do better reaction than reloading the page - filterTagsChanged(); - } else { // On failure - //@TODO : show error message? - } - } - }); - }); -} - -function cancelBookmark(event) { - event.preventDefault(); - var rec_form = $(this).closest('form').parent(); - rec_form.prev().show(); - rec_form.remove(); -} - -function updateBookmarksList(bookmark, position) { - position = typeof position !== 'undefined' ? position : 'append'; - bookmark = $.extend({title: '', description: '', added_date: new Date('now'), tags: []}, bookmark); - var tags = bookmark.tags; - var taglist = ''; - for (var i = 0, len = tags.length; i < len; ++i) { - if (tags[i] !== '') - taglist = taglist + '' + escapeHTML(tags[i]) + ' '; - } - if (!hasProtocol(bookmark.url)) { - bookmark.url = 'http://' + bookmark.url; - } - - if (bookmark.added) { - bookmark.added_date.setTime(parseInt(bookmark.added) * 1000); - } - - if (!bookmark.title) - bookmark.title = ''; - - var html = tmpl("item_tmpl", bookmark); - if (position === "prepend") { - $('.bookmarks_list').prepend(html); - } else { - $('.bookmarks_list').append(html); - } - var line = $('.bookmark_single[data-id="' + bookmark.id + '"]'); - line.data('record', bookmark); - if (taglist !== '') { - line.append('

' + taglist + '

'); - } - line.find('a.bookmark_tag').bind('click', addFilterTag); - line.find('.bookmark_link').click(recordClick); - line.find('.bookmark_delete').click(delBookmark); - line.find('.bookmark_edit').click(editBookmark); - -} - -function updateOnBottom() { - //check wether user is on bottom of the page - var top = $('.bookmarks_list>:last-child').position().top; - var height = $('.bookmarks_list').height(); - // use a bit of margin to begin loading before we are really at the - // bottom - if (top < height * 1.2) { - getBookmarks(); - } -} - -function recordClick() { - $.ajax({ - type: 'POST', - url: 'bookmark/click', - data: 'url=' + encodeURIComponent($(this).attr('href')) - }); -} - -function hasProtocol(url) { - var regexp = /(ftp|http|https|sftp)/; - return regexp.test(url); -} - -function renameTag() { - if ($('input[name="tag_new_name"]').length) - return; // Do nothing if a tag is currenlty edited - var tagElement = $(this).closest('li'); - tagElement.append('
'); - var form = tagElement.find('form'); - //tag_el.find('.tags_actions').hide(); - var tagName = tagElement.find('.tag').hide().text(); - tagElement.find('input').val(tagName).focus().bind('blur', function () { - form.trigger('submit'); - }); - form.bind('submit', submitTagName); -} - -function submitTagName(event) { - event.preventDefault(); - var tagElement = $(this).closest('li'); - var newTagName = tagElement.find('input').val(); - var oldTagName = tagElement.find('.tag').show().text(); - //tag_el.find('.tag_edit').show(); - //tag_el.find('.tags_actions').show(); - tagElement.find('input').unbind('blur'); - tagElement.find('form').unbind('submit').remove(); - - if (newTagName !== oldTagName && newTagName !== '') { - //submit - $.ajax({ - type: 'POST', - url: 'tag', - data: {old_name: oldTagName, new_name: newTagName}, - success: function (bookmarks) { - if (bookmarks.status === 'success') { - filterTagsChanged(); - } - } - }); - } -} - -function deleteTag() { - var tag_el = $(this).closest('li'); - var old_tag_name = tag_el.find('.tag').show().text(); - OC.dialogs.confirm(t('bookmarks', 'Are you sure you want to remove this tag from every entry?'), - t('bookmarks', 'Warning'), function (answer) { - if (answer) { - $.ajax({ - type: 'DELETE', - url: 'tag', - data: {old_name: old_tag_name}, - success: function (bookmarks) { - if (bookmarks.status === 'success') { - filterTagsChanged(); - } - } - }); - } - }); -} +var Backbone = OC.Backbone + +var Bookmark = Backbone.Model.extend({ + defaults: { + id: '', + url: '', + title: '', + description: '', + tags: Tags + } +}) + +var Bookmarks = Backbone.Collection.extend({ + model: Bookmark +, url: 'bookmark' +}) + +var Tag = Backbone.Model.extend({ + id: '', + title: '' +}) + +var Tags = Backbone.Collection.extend({ + model: Tag +, url: 'tag' +}) +var BookmarkCardView = Marionette.View.extend({ + template: _.template('

<%- title %>

<%- new URL(url).host %>

'), + tagName: "div", + className: "bookmark-card", + events: { + "click": "open", + }, + initialize: function() { + this.listenTo(this.model, "change", this.render); + }, +}) + +var BookmarksView = Marionette.CollectionView.extend({ + tagName: "div" +, className: 'bookmarks' +, childView: BookmarkCardView +}) + + +var _sync = Backbone.sync +Backbone.sync = function(method, model, options) { + _sync(method, model, _.extend({}, options, { + success: function(json) { + console.log(json) + if (!(model instanceof Tags)) options.success(json.data) + else options.success(json) + } + })) +} + +var bookmarks = new Bookmarks +var view = new BookmarksView({collection: bookmarks}) +$('#app-content').empty().append(view.el) +bookmarks.fetch({success: function() { + view.render() +}}) diff --git a/templates/main.php b/templates/main.php index de7de6b29..d67007152 100644 --- a/templates/main.php +++ b/templates/main.php @@ -3,6 +3,9 @@ script('bookmarks', 'bookmarks'); style('bookmarks', 'bookmarks'); + +script('bookmarks', '3rdparty/backbone.radio'); +script('bookmarks', '3rdparty/backbone.marionette.min'); script('bookmarks', '3rdparty/tag-it'); script('bookmarks', '3rdparty/js_tpl'); style('bookmarks', '3rdparty/jquery.tagit'); From 1b14643c7163b4fed1e37206830573fc4d2e8912 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sat, 9 Sep 2017 00:08:20 +0200 Subject: [PATCH 002/110] Add emptyView --- css/bookmarks.css | 17 +++++++++++------ js/bookmarks.js | 12 +++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/css/bookmarks.css b/css/bookmarks.css index b56f3b721..1fe3f3a52 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -1,14 +1,19 @@ -.bookmarks { - padding: .5cm; +.bookmarks-empty { + width: 4cm; + margin: 30% auto; } + .bookmark-card { width: 5cm; height: 5cm; float: left; - margin: 0 .5cm .5cm 0; + padding: .5cm; + border-right: 1px solid #ebebeb; + border-bottom: 1px solid #ebebeb; } .bookmark-card h1 { - font-weight: bold; - font-family: sans-serif; - font-size: .3cm; + font-size: .5cm; +} +.bookmark-card h2 { + font-size: .4cm; } diff --git a/js/bookmarks.js b/js/bookmarks.js index 219f2fffd..fae1b6563 100644 --- a/js/bookmarks.js +++ b/js/bookmarks.js @@ -40,6 +40,13 @@ var BookmarksView = Marionette.CollectionView.extend({ tagName: "div" , className: 'bookmarks' , childView: BookmarkCardView +, emptyView: EmptyBookmarksView +}) + +var EmptyBookmarksView = Marionette.CollectionView.extend({ + template: _.template('

No bookmarks, here.

There are no bookmarks available for this query. Try adding some using the above form.

') +, tagName: "div" +, className: 'bookmarks-empty' }) @@ -56,7 +63,10 @@ Backbone.sync = function(method, model, options) { var bookmarks = new Bookmarks var view = new BookmarksView({collection: bookmarks}) -$('#app-content').empty().append(view.el) bookmarks.fetch({success: function() { view.render() }}) + +$(function() { + $('#app-content').append(view.el) +}) From 0fb93c34e0604016a2d6d96c64dcb8f7cf7e15c8 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sat, 9 Sep 2017 01:35:47 +0200 Subject: [PATCH 003/110] More structure, replace whole template --- css/bookmarks.css | 19 ++++++- js/bookmarks.js | 129 +++++++++++++++++++++++++++++++++++++-------- templates/main.php | 54 ++----------------- 3 files changed, 128 insertions(+), 74 deletions(-) diff --git a/css/bookmarks.css b/css/bookmarks.css index 1fe3f3a52..a97f962fe 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -1,6 +1,21 @@ +.add-bookmark { + padding: .1cm; + border-bottom: 1px solid #ebebeb; +} +.add-bookmark input { + width: 93%; + font-size: .6cm; + border: none; + width: calc(100% - 1.5cm); +} +.add-bookmark button { + width: 1cm; + height: 1cm; +} + .bookmarks-empty { - width: 4cm; - margin: 30% auto; + width: 5cm; + margin: 20% auto; } .bookmark-card { diff --git a/js/bookmarks.js b/js/bookmarks.js index fae1b6563..a1367d9e7 100644 --- a/js/bookmarks.js +++ b/js/bookmarks.js @@ -16,17 +16,119 @@ var Bookmarks = Backbone.Collection.extend({ }) var Tag = Backbone.Model.extend({ - id: '', - title: '' + defaults: { + id: '' + , name: '' + } }) var Tags = Backbone.Collection.extend({ model: Tag , url: 'tag' }) + + +var AppView = Marionette.View.extend({ + template: _.template('
') +, regions: { + 'navigation': '#app-navigation' + , 'content': '#app-content' + } +, initialize: function() { + this.bookmarks = new Bookmarks + this.bookmarks.fetch() + this.tags = new Tags + this.tags.fetch() + } +, onRender: function() { + this.showChildView('navigation', new NavigationView({tags: this.tags})); + this.showChildView('content', new ContentView({bookmarks: this.bookmarks})); + } +}) + +var NavigationView = Marionette.View.extend({ + className: 'navigation' +, template: _.template('Favorite tags
') +, events: { + 'click .all': 'onClick' + , 'lick .favorites': 'onClick' + , 'click .shared': 'onClick' + } +, regions: { + 'tags': '#favorite-tags-slot' + } +, initialize: function(options) { + this.tags = options.tags + } +, onClick: function(e) { + e.preventDefault() + this.triggerMethod('navigation:open', e.target.parentNode.dataset.id) + } +, onNavigationOpen: function(category) { + $('.active', this.$el).removeClass('active') + $('.'+category, this.$el).addClass('active') + } +, onRender: function() { + this.showChildView('tags', new TagsNavigationView({collection: this.tags})) + } +}) + +var TagsNavigationView = Marionette.CollectionView.extend({ + tagName: 'ul' +, childView: function() {return TagsNavigationTagView} +}) + +var TagsNavigationTagView = Marionette.View.extend({ + className: 'tag-nav-item' +, tagName: 'li' +, template: _.template('
  • <%- name %>
  • ') +}) + +var ContentView = Marionette.View.extend({ + template: _.template('
    ') +, regions: { + 'addBookmarks': { + el: '#add-bookmark-slot' + , replaceElement: true + } + , 'viewBookmarks': { + el: '#view-bookmarks-slot' + , replaceElement: true + } + } +, initialize: function(options) { + this.bookmarks = options.bookmarks + } +, onRender: function() { + this.showChildView('addBookmarks', new AddBookmarkView()); + this.showChildView('viewBookmarks', new BookmarksView({collection: this.bookmarks})); + } +}) + + + + + +var AddBookmarkView = Marionette.View.extend({ + template: _.template('') +, className: 'add-bookmark' +}) + + + +var BookmarksView = Marionette.CollectionView.extend({ + className: 'bookmarks' +, childView: function() {return BookmarkCardView} +, emptyView: function() {return EmptyBookmarksView} +}) + +var EmptyBookmarksView = Marionette.View.extend({ + template: _.template('

    No bookmarks, here.

    There are no bookmarks available for this query. Try adding some using the above form.

    ') +, className: 'bookmarks-empty' +}) + var BookmarkCardView = Marionette.View.extend({ template: _.template('

    <%- title %>

    <%- new URL(url).host %>

    '), - tagName: "div", className: "bookmark-card", events: { "click": "open", @@ -36,18 +138,6 @@ var BookmarkCardView = Marionette.View.extend({ }, }) -var BookmarksView = Marionette.CollectionView.extend({ - tagName: "div" -, className: 'bookmarks' -, childView: BookmarkCardView -, emptyView: EmptyBookmarksView -}) - -var EmptyBookmarksView = Marionette.CollectionView.extend({ - template: _.template('

    No bookmarks, here.

    There are no bookmarks available for this query. Try adding some using the above form.

    ') -, tagName: "div" -, className: 'bookmarks-empty' -}) var _sync = Backbone.sync @@ -61,12 +151,9 @@ Backbone.sync = function(method, model, options) { })) } -var bookmarks = new Bookmarks -var view = new BookmarksView({collection: bookmarks}) -bookmarks.fetch({success: function() { - view.render() -}}) +// init +var view = new AppView() $(function() { - $('#app-content').append(view.el) + $('#content').append(view.render().el) }) diff --git a/templates/main.php b/templates/main.php index d67007152..393d2d67c 100644 --- a/templates/main.php +++ b/templates/main.php @@ -1,11 +1,12 @@ -
    - - -
    -
    - -
    -
    - - - -
    -
    - -
    -
    - -
    -
    - Date: Sat, 9 Sep 2017 14:58:01 +0200 Subject: [PATCH 004/110] Navigation is working --- css/bookmarks.css | 7 + js/3rdparty/backbone.js | 1920 ++++++++++++++++++++ js/3rdparty/backbone.marionette.min.js | 13 + js/3rdparty/backbone.marionette.min.js.map | 1 + js/3rdparty/backbone.radio.js | 350 ++++ js/3rdparty/backbone.radio.js.map | 1 + js/bookmarks.js | 115 +- 7 files changed, 2381 insertions(+), 26 deletions(-) create mode 100644 js/3rdparty/backbone.js create mode 100644 js/3rdparty/backbone.marionette.min.js create mode 100644 js/3rdparty/backbone.marionette.min.js.map create mode 100644 js/3rdparty/backbone.radio.js create mode 100644 js/3rdparty/backbone.radio.js.map diff --git a/css/bookmarks.css b/css/bookmarks.css index a97f962fe..788a5c9f8 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -1,3 +1,10 @@ +#app-navigation h3 { + padding-top: .7cm; + padding-left: 0.1cm; + border-top: 1px solid #ededed; +} + + .add-bookmark { padding: .1cm; border-bottom: 1px solid #ebebeb; diff --git a/js/3rdparty/backbone.js b/js/3rdparty/backbone.js new file mode 100644 index 000000000..55ccb22bd --- /dev/null +++ b/js/3rdparty/backbone.js @@ -0,0 +1,1920 @@ +// Backbone.js 1.3.3 + +// (c) 2010-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(factory) { + + // Establish the root object, `window` (`self`) in the browser, or `global` on the server. + // We use `self` instead of `window` for `WebWorker` support. + var root = (typeof self == 'object' && self.self === self && self) || + (typeof global == 'object' && global.global === global && global); + + // Set up Backbone appropriately for the environment. Start with AMD. + if (typeof define === 'function' && define.amd) { + define(['underscore', 'jquery', 'exports'], function(_, $, exports) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global Backbone. + root.Backbone = factory(root, exports, _, $); + }); + + // Next for Node.js or CommonJS. jQuery may not be needed as a module. + } else if (typeof exports !== 'undefined') { + var _ = require('underscore'), $; + try { $ = require('jquery'); } catch (e) {} + factory(root, exports, _, $); + + // Finally, as a browser global. + } else { + root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); + } + +})(function(root, Backbone, _, $) { + + // Initial Setup + // ------------- + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to a common array method we'll want to use later. + var slice = Array.prototype.slice; + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '1.3.3'; + + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = $; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... this will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Proxy Backbone class methods to Underscore functions, wrapping the model's + // `attributes` object or collection's `models` array behind the scenes. + // + // collection.filter(function(model) { return model.get('age') > 10 }); + // collection.each(this.addView); + // + // `Function#apply` can be slow so we use the method's arg count, if we know it. + var addMethod = function(length, method, attribute) { + switch (length) { + case 1: return function() { + return _[method](this[attribute]); + }; + case 2: return function(value) { + return _[method](this[attribute], value); + }; + case 3: return function(iteratee, context) { + return _[method](this[attribute], cb(iteratee, this), context); + }; + case 4: return function(iteratee, defaultVal, context) { + return _[method](this[attribute], cb(iteratee, this), defaultVal, context); + }; + default: return function() { + var args = slice.call(arguments); + args.unshift(this[attribute]); + return _[method].apply(_, args); + }; + } + }; + var addUnderscoreMethods = function(Class, methods, attribute) { + _.each(methods, function(length, method) { + if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); + }); + }; + + // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. + var cb = function(iteratee, instance) { + if (_.isFunction(iteratee)) return iteratee; + if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); + if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; + return iteratee; + }; + var modelMatcher = function(attrs) { + var matcher = _.matches(attrs); + return function(model) { + return matcher(model.attributes); + }; + }; + + // Backbone.Events + // --------------- + + // A module that can be mixed in to *any object* in order to provide it with + // a custom event channel. You may bind a callback to an event with `on` or + // remove with `off`; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = {}; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Iterates over the standard `event, callback` (as well as the fancy multiple + // space-separated events `"change blur", callback` and jQuery-style event + // maps `{event: callback}`). + var eventsApi = function(iteratee, events, name, callback, opts) { + var i = 0, names; + if (name && typeof name === 'object') { + // Handle event maps. + if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; + for (names = _.keys(name); i < names.length ; i++) { + events = eventsApi(iteratee, events, names[i], name[names[i]], opts); + } + } else if (name && eventSplitter.test(name)) { + // Handle space-separated event names by delegating them individually. + for (names = name.split(eventSplitter); i < names.length; i++) { + events = iteratee(events, names[i], callback, opts); + } + } else { + // Finally, standard events. + events = iteratee(events, name, callback, opts); + } + return events; + }; + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + Events.on = function(name, callback, context) { + return internalOn(this, name, callback, context); + }; + + // Guard the `listening` argument from the public API. + var internalOn = function(obj, name, callback, context, listening) { + obj._events = eventsApi(onApi, obj._events || {}, name, callback, { + context: context, + ctx: obj, + listening: listening + }); + + if (listening) { + var listeners = obj._listeners || (obj._listeners = {}); + listeners[listening.id] = listening; + } + + return obj; + }; + + // Inversion-of-control versions of `on`. Tell *this* object to listen to + // an event in another object... keeping track of what it's listening to + // for easier unbinding later. + Events.listenTo = function(obj, name, callback) { + if (!obj) return this; + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var listening = listeningTo[id]; + + // This object is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listening) { + var thisId = this._listenId || (this._listenId = _.uniqueId('l')); + listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; + } + + // Bind callbacks on obj, and keep track of them on listening. + internalOn(obj, name, callback, this, listening); + return this; + }; + + // The reducing API that adds a callback to the `events` object. + var onApi = function(events, name, callback, options) { + if (callback) { + var handlers = events[name] || (events[name] = []); + var context = options.context, ctx = options.ctx, listening = options.listening; + if (listening) listening.count++; + + handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening}); + } + return events; + }; + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + Events.off = function(name, callback, context) { + if (!this._events) return this; + this._events = eventsApi(offApi, this._events, name, callback, { + context: context, + listeners: this._listeners + }); + return this; + }; + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + Events.stopListening = function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + + var ids = obj ? [obj._listenId] : _.keys(listeningTo); + + for (var i = 0; i < ids.length; i++) { + var listening = listeningTo[ids[i]]; + + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listening) break; + + listening.obj.off(name, callback, this); + } + + return this; + }; + + // The reducing API that removes a callback from the `events` object. + var offApi = function(events, name, callback, options) { + if (!events) return; + + var i = 0, listening; + var context = options.context, listeners = options.listeners; + + // Delete all events listeners and "drop" events. + if (!name && !callback && !context) { + var ids = _.keys(listeners); + for (; i < ids.length; i++) { + listening = listeners[ids[i]]; + delete listeners[listening.id]; + delete listening.listeningTo[listening.objId]; + } + return; + } + + var names = name ? [name] : _.keys(events); + for (; i < names.length; i++) { + name = names[i]; + var handlers = events[name]; + + // Bail out if there are no events stored. + if (!handlers) break; + + // Replace events if there are any remaining. Otherwise, clean up. + var remaining = []; + for (var j = 0; j < handlers.length; j++) { + var handler = handlers[j]; + if ( + callback && callback !== handler.callback && + callback !== handler.callback._callback || + context && context !== handler.context + ) { + remaining.push(handler); + } else { + listening = handler.listening; + if (listening && --listening.count === 0) { + delete listeners[listening.id]; + delete listening.listeningTo[listening.objId]; + } + } + } + + // Update tail event if the list has any events. Otherwise, clean up. + if (remaining.length) { + events[name] = remaining; + } else { + delete events[name]; + } + } + return events; + }; + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, its listener will be removed. If multiple events + // are passed in using the space-separated syntax, the handler will fire + // once for each event, not once for a combination of all events. + Events.once = function(name, callback, context) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); + if (typeof name === 'string' && context == null) callback = void 0; + return this.on(events, callback, context); + }; + + // Inversion-of-control versions of `once`. + Events.listenToOnce = function(obj, name, callback) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); + return this.listenTo(obj, events); + }; + + // Reduces the event callbacks into a map of `{event: onceWrapper}`. + // `offer` unbinds the `onceWrapper` after it has been called. + var onceMap = function(map, name, callback, offer) { + if (callback) { + var once = map[name] = _.once(function() { + offer(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + } + return map; + }; + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + Events.trigger = function(name) { + if (!this._events) return this; + + var length = Math.max(0, arguments.length - 1); + var args = Array(length); + for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; + + eventsApi(triggerApi, this._events, name, void 0, args); + return this; + }; + + // Handles triggering the appropriate event callbacks. + var triggerApi = function(objEvents, name, callback, args) { + if (objEvents) { + var events = objEvents[name]; + var allEvents = objEvents.all; + if (events && allEvents) allEvents = allEvents.slice(); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, [name].concat(args)); + } + return objEvents; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; + } + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId(this.cidPrefix); + this.attributes = {}; + if (options.collection) this.collection = options.collection; + if (options.parse) attrs = this.parse(attrs, options) || {}; + var defaults = _.result(this, 'defaults'); + attrs = _.defaults(_.extend({}, defaults, attrs), defaults); + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // The prefix is used to create the client id which is used to identify models locally. + // You may want to override this if you're experiencing name clashes with model ids. + cidPrefix: 'c', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Special-cased proxy to underscore's `_.matches` method. + matches: function(attrs) { + return !!_.iteratee(attrs, this)(this.attributes); + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + var unset = options.unset; + var silent = options.silent; + var changes = []; + var changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + + var current = this.attributes; + var changed = this.changed; + var prev = this._previousAttributes; + + // For each `set` attribute, update or delete the current value. + for (var attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + changed[attr] = val; + } else { + delete changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Update the `id`. + if (this.idAttribute in attrs) this.id = this.get(this.idAttribute); + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = options; + for (var i = 0; i < changes.length; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + options = this._pending; + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var old = this._changing ? this._previousAttributes : this.attributes; + var changed = {}; + for (var attr in diff) { + var val = diff[attr]; + if (_.isEqual(old[attr], val)) continue; + changed[attr] = val; + } + return _.size(changed) ? changed : false; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server, merging the response with the model's + // local attributes. Any changed attributes will trigger a "change" event. + fetch: function(options) { + options = _.extend({parse: true}, options); + var model = this; + var success = options.success; + options.success = function(resp) { + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (!model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options = _.extend({validate: true, parse: true}, options); + var wait = options.wait; + + // If we're not waiting and attributes exist, save acts as + // `set(attr).save(null, opts)` with validation. Otherwise, check if + // the model will be valid when the attributes, if any, are set. + if (attrs && !wait) { + if (!this.set(attrs, options)) return false; + } else if (!this._validate(attrs, options)) { + return false; + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + var model = this; + var success = options.success; + var attributes = this.attributes; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); + if (serverAttrs && !model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + // Set temporary attributes if `{wait: true}` to properly find new ids. + if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); + + var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch' && !options.attrs) options.attrs = attrs; + var xhr = this.sync(method, this, options); + + // Restore attributes. + this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + var wait = options.wait; + + var destroy = function() { + model.stopListening(); + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (wait) destroy(); + if (success) success.call(options.context, model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + var xhr = false; + if (this.isNew()) { + _.defer(options.success); + } else { + wrapError(this, options); + xhr = this.sync('delete', this, options); + } + if (!wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = + _.result(this, 'urlRoot') || + _.result(this.collection, 'url') || + urlError(); + if (this.isNew()) return base; + var id = this.get(this.idAttribute); + return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return !this.has(this.idAttribute); + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend({}, options, {validate: true})); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options, {validationError: error})); + return false; + } + + }); + + // Underscore methods that we want to implement on the Model, mapped to the + // number of arguments they take. + var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, + omit: 0, chain: 1, isEmpty: 1}; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + addUnderscoreMethods(Model, modelMethods, 'attributes'); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analogous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, remove: false}; + + // Splices `insert` into `array` at index `at`. + var splice = function(array, insert, at) { + at = Math.min(Math.max(at, 0), array.length); + var tail = Array(array.length - at); + var length = insert.length; + var i; + for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; + for (i = 0; i < length; i++) array[i + at] = insert[i]; + for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model) { return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. `models` may be Backbone + // Models or raw JavaScript objects to be converted to Models, or any + // combination of the two. + add: function(models, options) { + return this.set(models, _.extend({merge: false}, options, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + options = _.extend({}, options); + var singular = !_.isArray(models); + models = singular ? [models] : models.slice(); + var removed = this._removeModels(models, options); + if (!options.silent && removed.length) { + options.changes = {added: [], merged: [], removed: removed}; + this.trigger('update', this, options); + } + return singular ? removed[0] : removed; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + if (models == null) return; + + options = _.extend({}, setOptions, options); + if (options.parse && !this._isModel(models)) { + models = this.parse(models, options) || []; + } + + var singular = !_.isArray(models); + models = singular ? [models] : models.slice(); + + var at = options.at; + if (at != null) at = +at; + if (at > this.length) at = this.length; + if (at < 0) at += this.length + 1; + + var set = []; + var toAdd = []; + var toMerge = []; + var toRemove = []; + var modelMap = {}; + + var add = options.add; + var merge = options.merge; + var remove = options.remove; + + var sort = false; + var sortable = this.comparator && at == null && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + var model, i; + for (i = 0; i < models.length; i++) { + model = models[i]; + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + var existing = this.get(model); + if (existing) { + if (merge && model !== existing) { + var attrs = this._isModel(model) ? model.attributes : model; + if (options.parse) attrs = existing.parse(attrs, options); + existing.set(attrs, options); + toMerge.push(existing); + if (sortable && !sort) sort = existing.hasChanged(sortAttr); + } + if (!modelMap[existing.cid]) { + modelMap[existing.cid] = true; + set.push(existing); + } + models[i] = existing; + + // If this is a new, valid model, push it to the `toAdd` list. + } else if (add) { + model = models[i] = this._prepareModel(model, options); + if (model) { + toAdd.push(model); + this._addReference(model, options); + modelMap[model.cid] = true; + set.push(model); + } + } + } + + // Remove stale models. + if (remove) { + for (i = 0; i < this.length; i++) { + model = this.models[i]; + if (!modelMap[model.cid]) toRemove.push(model); + } + if (toRemove.length) this._removeModels(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + var orderChanged = false; + var replace = !sortable && add && remove; + if (set.length && replace) { + orderChanged = this.length !== set.length || _.some(this.models, function(m, index) { + return m !== set[index]; + }); + this.models.length = 0; + splice(this.models, set, 0); + this.length = this.models.length; + } else if (toAdd.length) { + if (sortable) sort = true; + splice(this.models, toAdd, at == null ? this.length : at); + this.length = this.models.length; + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + // Unless silenced, it's time to fire all appropriate add/sort/update events. + if (!options.silent) { + for (i = 0; i < toAdd.length; i++) { + if (at != null) options.index = at + i; + model = toAdd[i]; + model.trigger('add', model, this, options); + } + if (sort || orderChanged) this.trigger('sort', this, options); + if (toAdd.length || toRemove.length || toMerge.length) { + options.changes = { + added: toAdd, + removed: toRemove, + merged: toMerge + }; + this.trigger('update', this, options); + } + } + + // Return the added (or merged) model (or models). + return singular ? models[0] : models; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options = options ? _.clone(options) : {}; + for (var i = 0; i < this.models.length; i++) { + this._removeReference(this.models[i], options); + } + options.previousModels = this.models; + this._reset(); + models = this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return models; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + return this.add(model, _.extend({at: this.length}, options)); + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + return this.remove(model, options); + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + return this.add(model, _.extend({at: 0}, options)); + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + return this.remove(model, options); + }, + + // Slice out a sub-array of models from the collection. + slice: function() { + return slice.apply(this.models, arguments); + }, + + // Get a model from the set by id, cid, model object with id or cid + // properties, or an attributes object that is transformed through modelId. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj] || + this._byId[this.modelId(obj.attributes || obj)] || + obj.cid && this._byId[obj.cid]; + }, + + // Returns `true` if the model is in the collection. + has: function(obj) { + return this.get(obj) != null; + }, + + // Get the model at the given index. + at: function(index) { + if (index < 0) index += this.length; + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + return this[first ? 'find' : 'filter'](attrs); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + var comparator = this.comparator; + if (!comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + var length = comparator.length; + if (_.isFunction(comparator)) comparator = _.bind(comparator, this); + + // Run sort based on type of `comparator`. + if (length === 1 || _.isString(comparator)) { + this.models = this.sortBy(comparator); + } else { + this.models.sort(comparator); + } + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return this.map(attr + ''); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = _.extend({parse: true}, options); + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success.call(options.context, collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + var wait = options.wait; + model = this._prepareModel(model, options); + if (!model) return false; + if (!wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(m, resp, callbackOpts) { + if (wait) collection.add(m, callbackOpts); + if (success) success.call(callbackOpts.context, m, resp, callbackOpts); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models, { + model: this.model, + comparator: this.comparator + }); + }, + + // Define how to uniquely identify models in the collection. + modelId: function(attrs) { + return attrs[this.model.prototype.idAttribute || 'id']; + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (this._isModel(attrs)) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options = options ? _.clone(options) : {}; + options.collection = this; + var model = new this.model(attrs, options); + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; + }, + + // Internal method called by both remove and set. + _removeModels: function(models, options) { + var removed = []; + for (var i = 0; i < models.length; i++) { + var model = this.get(models[i]); + if (!model) continue; + + var index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + + // Remove references before triggering 'remove' event to prevent an + // infinite loop. #3693 + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + + removed.push(model); + this._removeReference(model, options); + } + return removed; + }, + + // Method for checking whether an object should be considered a model for + // the purposes of adding to the collection. + _isModel: function(model) { + return model instanceof Model; + }, + + // Internal method to create a model's ties to a collection. + _addReference: function(model, options) { + this._byId[model.cid] = model; + var id = this.modelId(model.attributes); + if (id != null) this._byId[id] = model; + model.on('all', this._onModelEvent, this); + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model, options) { + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if (model) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (event === 'change') { + var prevId = this.modelId(model.previousAttributes()); + var id = this.modelId(model.attributes); + if (prevId !== id) { + if (prevId != null) delete this._byId[prevId]; + if (id != null) this._byId[id] = model; + } + } + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0, + foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3, + select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, + contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, + head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, + without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, + isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, + sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3}; + + // Mix in each Underscore method as a proxy to `Collection#models`. + addUnderscoreMethods(Collection, collectionMethods, 'models'); + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + _.extend(this, _.pick(options, viewOptions)); + this._ensureElement(); + this.initialize.apply(this, arguments); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be set as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be preferred to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this._removeElement(); + this.stopListening(); + return this; + }, + + // Remove this view's element from the document and all event listeners + // attached to it. Exposed for subclasses using an alternative DOM + // manipulation API. + _removeElement: function() { + this.$el.remove(); + }, + + // Change the view's element (`this.el` property) and re-delegate the + // view's events on the new element. + setElement: function(element) { + this.undelegateEvents(); + this._setElement(element); + this.delegateEvents(); + return this; + }, + + // Creates the `this.el` and `this.$el` references for this view using the + // given `el`. `el` can be a CSS selector or an HTML string, a jQuery + // context or an element. Subclasses can override this to utilize an + // alternative DOM manipulation API and are only required to set the + // `this.el` property. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + delegateEvents: function(events) { + events || (events = _.result(this, 'events')); + if (!events) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[method]; + if (!method) continue; + var match = key.match(delegateEventSplitter); + this.delegate(match[1], match[2], _.bind(method, this)); + } + return this; + }, + + // Add a single event listener to the view's element (or a child element + // using `selector`). This only works for delegate-able events: not `focus`, + // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + delegate: function(eventName, selector, listener) { + this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Clears all callbacks previously bound to the view by `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + if (this.$el) this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // A finer-grained `undelegateEvents` for removing a single delegated event. + // `selector` and `listener` are both optional. + undelegate: function(eventName, selector, listener) { + this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Produces a DOM element to be assigned to your view. Exposed for + // subclasses using an alternative DOM manipulation API. + _createElement: function(tagName) { + return document.createElement(tagName); + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + this.setElement(this._createElement(_.result(this, 'tagName'))); + this._setAttributes(attrs); + } else { + this.setElement(_.result(this, 'el')); + } + }, + + // Set attributes from a hash on this view's element. Exposed for + // subclasses using an alternative DOM manipulation API. + _setAttributes: function(attributes) { + this.$el.attr(attributes); + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // Pass along `textStatus` and `errorThrown` from jQuery. + var error = options.error; + options.error = function(xhr, textStatus, errorThrown) { + options.textStatus = textStatus; + options.errorThrown = errorThrown; + if (error) error.call(options.context, xhr, textStatus, errorThrown); + }; + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + if (router.execute(callback, args, name) !== false) { + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + } + }); + return this; + }, + + // Execute a route handler with the provided parameters. This is an + // excellent place to do pre-route setup or post-route cleanup. + execute: function(callback, args, name) { + if (callback) callback.apply(this, args); + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + return optional ? match : '([^/?]+)'; + }) + .replace(splatParam, '([^?]*?)'); + return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param, i) { + // Don't decode the search params. + if (i === params.length - 1) return param || null; + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + this.checkUrl = _.bind(this.checkUrl, this); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for stripping urls of hash. + var pathStripper = /#.*$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Are we at the app root? + atRoot: function() { + var path = this.location.pathname.replace(/[^\/]$/, '$&/'); + return path === this.root && !this.getSearch(); + }, + + // Does the pathname match the root? + matchRoot: function() { + var path = this.decodeFragment(this.location.pathname); + var rootPath = path.slice(0, this.root.length - 1) + '/'; + return rootPath === this.root; + }, + + // Unicode characters in `location.pathname` are percent encoded so they're + // decoded for comparison. `%25` should not be decoded since it may be part + // of an encoded parameter. + decodeFragment: function(fragment) { + return decodeURI(fragment.replace(/%25/g, '%2525')); + }, + + // In IE6, the hash fragment and search params are incorrect if the + // fragment contains `?`. + getSearch: function() { + var match = this.location.href.replace(/#.*/, '').match(/\?.+/); + return match ? match[0] : ''; + }, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the pathname and search params, without the root. + getPath: function() { + var path = this.decodeFragment( + this.location.pathname + this.getSearch() + ).slice(this.root.length - 1); + return path.charAt(0) === '/' ? path.slice(1) : path; + }, + + // Get the cross-browser normalized URL fragment from the path or hash. + getFragment: function(fragment) { + if (fragment == null) { + if (this._usePushState || !this._wantsHashChange) { + fragment = this.getPath(); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error('Backbone.history has already been started'); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); + this._useHashChange = this._wantsHashChange && this._hasHashChange; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.history && this.history.pushState); + this._usePushState = this._wantsPushState && this._hasPushState; + this.fragment = this.getFragment(); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + // Transition from hashChange to pushState or vice versa if both are + // requested. + if (this._wantsHashChange && this._wantsPushState) { + + // If we've started off with a route from a `pushState`-enabled + // browser, but we're currently in a browser that doesn't support it... + if (!this._hasPushState && !this.atRoot()) { + var rootPath = this.root.slice(0, -1) || '/'; + this.location.replace(rootPath + '#' + this.getPath()); + // Return immediately as browser will do redirect to new url + return true; + + // Or if we've started out with a hash-based route, but we're currently + // in a browser where it could be `pushState`-based instead... + } else if (this._hasPushState && this.atRoot()) { + this.navigate(this.getHash(), {replace: true}); + } + + } + + // Proxy an iframe to handle location events if the browser doesn't + // support the `hashchange` event, HTML5 history, or the user wants + // `hashChange` but not `pushState`. + if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { + this.iframe = document.createElement('iframe'); + this.iframe.src = 'javascript:0'; + this.iframe.style.display = 'none'; + this.iframe.tabIndex = -1; + var body = document.body; + // Using `appendChild` will throw on IE < 9 if the document is not ready. + var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; + iWindow.document.open(); + iWindow.document.close(); + iWindow.location.hash = '#' + this.fragment; + } + + // Add a cross-platform `addEventListener` shim for older browsers. + var addEventListener = window.addEventListener || function(eventName, listener) { + return attachEvent('on' + eventName, listener); + }; + + // Depending on whether we're using pushState or hashes, and whether + // 'onhashchange' is supported, determine how we check the URL state. + if (this._usePushState) { + addEventListener('popstate', this.checkUrl, false); + } else if (this._useHashChange && !this.iframe) { + addEventListener('hashchange', this.checkUrl, false); + } else if (this._wantsHashChange) { + this._checkUrlInterval = setInterval(this.checkUrl, this.interval); + } + + if (!this.options.silent) return this.loadUrl(); + }, + + // Disable Backbone.history, perhaps temporarily. Not useful in a real app, + // but possibly useful for unit testing Routers. + stop: function() { + // Add a cross-platform `removeEventListener` shim for older browsers. + var removeEventListener = window.removeEventListener || function(eventName, listener) { + return detachEvent('on' + eventName, listener); + }; + + // Remove window listeners. + if (this._usePushState) { + removeEventListener('popstate', this.checkUrl, false); + } else if (this._useHashChange && !this.iframe) { + removeEventListener('hashchange', this.checkUrl, false); + } + + // Clean up the iframe if necessary. + if (this.iframe) { + document.body.removeChild(this.iframe); + this.iframe = null; + } + + // Some environments will throw when clearing an undefined interval. + if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); + History.started = false; + }, + + // Add a route to be tested when the fragment changes. Routes added later + // may override previous routes. + route: function(route, callback) { + this.handlers.unshift({route: route, callback: callback}); + }, + + // Checks the current URL to see if it has changed, and if it has, + // calls `loadUrl`, normalizing across the hidden iframe. + checkUrl: function(e) { + var current = this.getFragment(); + + // If the user pressed the back button, the iframe's hash will have + // changed and we should use that for comparison. + if (current === this.fragment && this.iframe) { + current = this.getHash(this.iframe.contentWindow); + } + + if (current === this.fragment) return false; + if (this.iframe) this.navigate(current); + this.loadUrl(); + }, + + // Attempt to load the current URL fragment. If a route succeeds with a + // match, returns `true`. If no defined routes matches the fragment, + // returns `false`. + loadUrl: function(fragment) { + // If the root doesn't match, no routes can match either. + if (!this.matchRoot()) return false; + fragment = this.fragment = this.getFragment(fragment); + return _.some(this.handlers, function(handler) { + if (handler.route.test(fragment)) { + handler.callback(fragment); + return true; + } + }); + }, + + // Save a fragment into the hash history, or replace the URL state if the + // 'replace' option is passed. You are responsible for properly URL-encoding + // the fragment in advance. + // + // The options object can contain `trigger: true` if you wish to have the + // route callback be fired (not usually desirable), or `replace: true`, if + // you wish to modify the current URL without adding an entry to the history. + navigate: function(fragment, options) { + if (!History.started) return false; + if (!options || options === true) options = {trigger: !!options}; + + // Normalize the fragment. + fragment = this.getFragment(fragment || ''); + + // Don't include a trailing slash on the root. + var rootPath = this.root; + if (fragment === '' || fragment.charAt(0) === '?') { + rootPath = rootPath.slice(0, -1) || '/'; + } + var url = rootPath + fragment; + + // Strip the hash and decode for matching. + fragment = this.decodeFragment(fragment.replace(pathStripper, '')); + + if (this.fragment === fragment) return; + this.fragment = fragment; + + // If pushState is available, we use it to set the fragment as a real URL. + if (this._usePushState) { + this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); + + // If hash changes haven't been explicitly disabled, update the hash + // fragment to store history. + } else if (this._wantsHashChange) { + this._updateHash(this.location, fragment, options.replace); + if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) { + var iWindow = this.iframe.contentWindow; + + // Opening and closing the iframe tricks IE7 and earlier to push a + // history entry on hash-tag change. When replace is true, we don't + // want this. + if (!options.replace) { + iWindow.document.open(); + iWindow.document.close(); + } + + this._updateHash(iWindow.location, fragment, options.replace); + } + + // If you've told us that you explicitly don't want fallback hashchange- + // based history, then `navigate` becomes a page refresh. + } else { + return this.location.assign(url); + } + if (options.trigger) return this.loadUrl(fragment); + }, + + // Update the hash location, either replacing the current entry, or adding + // a new one to the browser history. + _updateHash: function(location, fragment, replace) { + if (replace) { + var href = location.href.replace(/(javascript:|#).*$/, ''); + location.replace(href + '#' + fragment); + } else { + // Some browsers require that `hash` contains a leading #. + location.hash = '#' + fragment; + } + } + + }); + + // Create the default Backbone.history. + Backbone.history = new History; + + // Helpers + // ------- + + // Helper function to correctly set up the prototype chain for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // class properties to be extended. + var extend = function(protoProps, staticProps) { + var parent = this; + var child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent constructor. + if (protoProps && _.has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } + + // Add static properties to the constructor function, if supplied. + _.extend(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function and add the prototype properties. + child.prototype = _.create(parent.prototype, protoProps); + child.prototype.constructor = child; + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; + }; + + // Set up inheritance for the model, collection, router, view and history. + Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; + + // Throw an error when a URL is needed, and none is supplied. + var urlError = function() { + throw new Error('A "url" property or function must be specified'); + }; + + // Wrap an optional error callback with a fallback error event. + var wrapError = function(model, options) { + var error = options.error; + options.error = function(resp) { + if (error) error.call(options.context, model, resp, options); + model.trigger('error', model, resp, options); + }; + }; + + return Backbone; +}); diff --git a/js/3rdparty/backbone.marionette.min.js b/js/3rdparty/backbone.marionette.min.js new file mode 100644 index 000000000..7067c3bfe --- /dev/null +++ b/js/3rdparty/backbone.marionette.min.js @@ -0,0 +1,13 @@ +/** +* @license +* MarionetteJS (Backbone.Marionette) +* ---------------------------------- +* v3.4.3 +* +* Copyright (c)2017 Derick Bailey, Muted Solutions, LLC. +* Distributed under MIT license +* +* http://marionettejs.com +*/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("backbone"),require("underscore"),require("backbone.radio")):"function"==typeof define&&define.amd?define(["backbone","underscore","backbone.radio"],t):e.Marionette=t(e.Backbone,e._,e.Backbone.Radio)}(this,function(e,t,i){"use strict";function n(e,t,i){return i.toUpperCase()}function s(e){for(var i=arguments.length,n=Array(i>1?i-1:0),s=1;s1?i-1:0),r=1;r1?i-1:0),s=1;s0)for(e=0;e2&&void 0!==arguments[2]?arguments[2]:I(e)).find(t)},hasEl:function(e,t){return e.contains(t&&t.parentNode)},detachEl:function(e){(arguments.length>1&&void 0!==arguments[1]?arguments[1]:I(e)).detach()},replaceEl:function(e,t){if(e!==t){var i=t.parentNode;i&&i.replaceChild(e,t)}},swapEl:function(e,t){if(e!==t){var i=e.parentNode,n=t.parentNode;if(i&&n){var s=e.nextSibling,r=t.nextSibling;i.insertBefore(t,s),n.insertBefore(e,r)}}},setContents:function(e,t){(arguments.length>2&&void 0!==arguments[2]?arguments[2]:I(e)).html(t)},appendContents:function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=i._$el,s=void 0===n?I(e):n,r=i._$contents,o=void 0===r?I(t):r;s.append(o)},hasContents:function(e){return e.hasChildNodes()},detachContents:function(e){(arguments.length>1&&void 0!==arguments[1]?arguments[1]:I(e)).contents().detach()}},fe={Dom:ue,supportsRenderLifecycle:!0,supportsDestroyLifecycle:!0,_isDestroyed:!1,isDestroyed:function(){return!!this._isDestroyed},_isRendered:!1,isRendered:function(){return!!this._isRendered},_isAttached:!1,isAttached:function(){return!!this._isAttached},delegateEvents:function(i){this._proxyBehaviorViewProperties(),this._buildEventProxies();var n=this._getEvents(i);void 0===i&&(this.events=n);var s=t.extend({},this._getBehaviorEvents(),n,this._getBehaviorTriggers(),this.getTriggers());return e.View.prototype.delegateEvents.call(this,s),this},_getEvents:function(e){var i=e||this.events;return t.isFunction(i)?this.normalizeUIKeys(i.call(this)):this.normalizeUIKeys(i)},getTriggers:function(){if(this.triggers){var e=this.normalizeUIKeys(t.result(this,"triggers"));return this._getViewTriggers(this,e)}},delegateEntityEvents:function(){return this._delegateEntityEvents(this.model,this.collection),this._delegateBehaviorEntityEvents(),this},undelegateEntityEvents:function(){return this._undelegateEntityEvents(this.model,this.collection),this._undelegateBehaviorEntityEvents(),this},destroy:function(){if(this._isDestroyed)return this;for(var e=this._isAttached&&!this._shouldDisableEvents,t=arguments.length,i=Array(t),n=0;n1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{},n=!e._isAttached&&j(this.el)&&!this._shouldDisableMonitoring(),s=void 0===i.replaceElement?!!t.result(this,"replaceElement"):!!i.replaceElement;n&&r(e,"before:attach",e),s?this._replaceEl(e):this.attachHtml(e),n&&(e._isAttached=!0,r(e,"attach",e))},_ensureElement:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(t.isObject(this.el)||(this.$el=this.getEl(this.el),this.el=this.$el[0],this.$el=this.Dom.getEl(this.el)),!this.$el||0===this.$el.length){if(void 0===e.allowMissingEl?!!t.result(this,"allowMissingEl"):!!e.allowMissingEl)return!1;throw new Z('An "el" must exist in DOM for this region '+this.cid)}return!0},_getView:function(t){if(!t)throw new Z({name:"ViewNotValid",message:"The view passed is undefined and therefore invalid. You must pass a view instance to show."});if(t._isDestroyed)throw new Z({name:"ViewDestroyedError",message:'View (cid: "'+t.cid+'") has already been destroyed and cannot be used.'});if(t instanceof e.View)return t;var i=this._getViewOptions(t);return new ye(i)},_getViewOptions:function(e){if(t.isFunction(e))return{template:e};if(t.isObject(e))return e;return{template:function(){return e}}},getEl:function(e){var i=t.result(this,"parentEl");return i&&t.isString(e)?this.Dom.findEl(i,e):this.Dom.getEl(e)},_replaceEl:function(e){this._restoreEl(),e.on("before:destroy",this._restoreEl,this),this.Dom.replaceEl(e.el,this.el),this._isReplaced=!0},_restoreEl:function(){if(this._isReplaced){var e=this.currentView;e&&(this._detachView(e),this._isReplaced=!1)}},isReplaced:function(){return!!this._isReplaced},isSwappingView:function(){return!!this._isSwappingView},attachHtml:function(e){this.Dom.appendContents(this.el,e.el,{_$el:this.$el,_$contents:e.$el})},empty:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{allowMissingEl:!0},t=this.currentView;if(!t)return this._ensureElement(e)&&this.detachHtml(),this;var i=!e.preventDestroy;return i||k("The preventDestroy option is deprecated. Use Region#detachView"),this._empty(t,i),this},_empty:function(e,t){e.off("destroy",this._empty,this),this.triggerMethod("before:empty",this,e),this._restoreEl(),delete this.currentView,e._isDestroyed||(t?this.removeView(e):this._detachView(e),this._stopChildViewEvents(e)),this.triggerMethod("empty",this,e)},_stopChildViewEvents:function(e){this._parentView&&this._parentView.stopListening(e)},destroyView:function(e){return e._isDestroyed?e:(e._shouldDisableEvents=this._shouldDisableMonitoring(),U(e),e)},removeView:function(e){this.destroyView(e)},detachView:function(){var e=this.currentView;if(e)return this._empty(e),e},_detachView:function(e){var t=e._isAttached&&!this._shouldDisableMonitoring(),i=this._isReplaced;t&&r(e,"before:detach",e),i?this.Dom.replaceEl(this.el,e.el):this.detachHtml(),t&&(e._isAttached=!1,r(e,"detach",e))},detachHtml:function(){this.Dom.detachContents(this.el,this.$el)},hasView:function(){return!!this.currentView},reset:function(e){return this.empty(e),this.$el&&(this.el=this._initEl),delete this.$el,this},destroy:function(e){return this._isDestroyed?this:(this.reset(e),this._name&&this._parentView._removeReferences(this._name),delete this._parentView,delete this._name,X.prototype.destroy.apply(this,arguments))}},{setDomApi:$}),ge=function(e,t){return e instanceof pe?e:S(e,t)},ve={regionClass:pe,_initRegions:function(){this.regions=this.regions||{},this._regions={},this.addRegions(t.result(this,"regions"))},_reInitRegions:function(){te(this._regions,"reset")},addRegion:function(e,t){var i={};return i[e]=t,this.addRegions(i)[e]},addRegions:function(e){if(!t.isEmpty(e))return e=this.normalizeUIValues(e,["selector","el"]),this.regions=t.extend({},this.regions,e),this._addRegions(e)},_addRegions:function(e){var i=this,n={regionClass:this.regionClass,parentEl:t.partial(t.result,this,"el")};return t.reduce(e,function(e,t,s){return e[s]=ge(t,n),i._addRegion(e[s],s),e},{})},_addRegion:function(e,t){this.triggerMethod("before:add:region",this,t,e),e._parentView=this,e._name=t,this._regions[t]=e,this.triggerMethod("add:region",this,t,e)},removeRegion:function(e){var t=this._regions[e];return this._removeRegion(t,e),t},removeRegions:function(){var e=this._getRegions();return t.each(this._regions,t.bind(this._removeRegion,this)),e},_removeRegion:function(e,t){this.triggerMethod("before:remove:region",this,t,e),e.destroy(),this.triggerMethod("remove:region",this,t,e)},_removeReferences:function(e){delete this.regions[e],delete this._regions[e]},emptyRegions:function(){var e=this.getRegions();return te(e,"empty"),e},hasRegion:function(e){return!!this.getRegion(e)},getRegion:function(e){return this._isRendered||this.render(),this._regions[e]},_getRegions:function(){return t.clone(this._regions)},getRegions:function(){return this._isRendered||this.render(),this._getRegions()},showChildView:function(e,t){for(var i=this.getRegion(e),n=arguments.length,s=Array(n>2?n-2:0),r=2;r0&&void 0!==arguments[0]?arguments[0]:{},i=t.result(this,"templateContext");return t.extend(e,i)},attachElContent:function(e){return this.Dom.setContents(this.el,e,this.$el),this},_removeChildren:function(){this.removeRegions()},_getImmediateChildren:function(){return t.chain(this._getRegions()).map("currentView").compact().value()}},{setRenderer:function(e){return this.prototype._renderHtml=e,this},setDomApi:$});t.extend(ye.prototype,fe,ve);var Ve=["forEach","each","map","find","detect","filter","select","reject","every","all","some","any","include","contains","invoke","toArray","first","initial","rest","last","without","isEmpty","pluck","reduce","partition"],Ce=function(e,i){t.each(Ve,function(n){e[n]=function(){var e=t.result(this,i),s=Array.prototype.slice.call(arguments);return t[n].apply(t,[e].concat(s))}})},Ee=function(e){this._views={},this._indexByModel={},this._indexByCustom={},this._updateLength(),t.each(e,t.bind(this.add,this))};Ce(Ee.prototype,"_getViews"),t.extend(Ee.prototype,{_getViews:function(){return t.values(this._views)},add:function(e,t){return this._add(e,t)._updateLength()},_add:function(e,t){var i=e.cid;return this._views[i]=e,e.model&&(this._indexByModel[e.model.cid]=i),t&&(this._indexByCustom[t]=i),this},findByModel:function(e){return this.findByModelCid(e.cid)},findByModelCid:function(e){var t=this._indexByModel[e];return this.findByCid(t)},findByCustom:function(e){var t=this._indexByCustom[e];return this.findByCid(t)},findByIndex:function(e){return t.values(this._views)[e]},findByCid:function(e){return this._views[e]},remove:function(e){return this._remove(e)._updateLength()},_remove:function(e){var i=e.cid;return e.model&&delete this._indexByModel[e.model.cid],t.some(this._indexByCustom,t.bind(function(e,t){if(e===i)return delete this._indexByCustom[t],!0},this)),delete this._views[i],this},_updateLength:function(){return this.length=t.size(this._views),this}});var be=["behaviors","childView","childViewEventPrefix","childViewEvents","childViewOptions","childViewTriggers","collectionEvents","events","filter","emptyView","emptyViewOptions","modelEvents","reorderOnSort","sort","triggers","ui","viewComparator"],Me=e.View.extend({sort:!0,constructor:function(i){this.render=t.bind(this.render,this),this._setOptions(i),this.mergeOptions(i,be),w(this),this._initBehaviors(),this.once("render",this._initialEvents),this._initChildViewStorage(),this._bufferedChildren=[];var n=Array.prototype.slice.call(arguments);n[0]=this.options,e.View.prototype.constructor.apply(this,n),this.delegateEntityEvents(),this._triggerEventOnBehaviors("initialize",this)},_startBuffering:function(){this._isBuffering=!0},_endBuffering:function(){var e=this._isAttached&&!1!==this.monitorViewEvents?this._getImmediateChildren():[];this._isBuffering=!1,t.each(e,function(e){r(e,"before:attach",e)}),this.attachBuffer(this,this._createBuffer()),t.each(e,function(e){e._isAttached=!0,r(e,"attach",e)}),this._bufferedChildren=[]},_getImmediateChildren:function(){return t.values(this.children._views)},_initialEvents:function(){this.collection&&(this.listenTo(this.collection,"add",this._onCollectionAdd),this.listenTo(this.collection,"update",this._onCollectionUpdate),this.listenTo(this.collection,"reset",this.render),this.sort&&this.listenTo(this.collection,"sort",this._sortViews))},_onCollectionAdd:function(e,i,n){var s=void 0!==n.at&&(n.index||i.indexOf(e));(this.filter||!1===s)&&(s=t.indexOf(this._filteredSortedModels(s),e)),this._shouldAddChild(e,s)&&(this._destroyEmptyView(),this._addChild(e,s))},_onCollectionUpdate:function(e,t){var i=t.changes;this._removeChildModels(i.removed)},_removeChildModels:function(e){var t=this._getRemovedViews(e);t.length&&(this.children._updateLength(),this._updateIndices(t,!1),this.isEmpty()&&this._showEmptyView())},_getRemovedViews:function(e){var i=this;return t.reduce(e,function(e,t){var n=t&&i.children.findByModel(t);return!n||n._isDestroyed?e:(i._removeChildView(n),e.push(n),e)},[])},_removeChildView:function(e){this.triggerMethod("before:remove:child",this,e),this.children._remove(e),e._shouldDisableEvents=!1===this.monitorViewEvents,U(e),this.stopListening(e),this.triggerMethod("remove:child",this,e)},setElement:function(){var t=!!this.el;return e.View.prototype.setElement.apply(this,arguments),t&&(this._isAttached=j(this.el)),this},render:function(){return this._isDestroyed?this:(this.triggerMethod("before:render",this),this._renderChildren(),this._isRendered=!0,this.triggerMethod("render",this),this)},setFilter:function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).preventRender,i=this._isRendered&&!this._isDestroyed,n=this.filter!==e;if(i&&n&&!t){var s=this._filteredSortedModels();this.filter=e;var r=this._filteredSortedModels();this._applyModelDeltas(r,s)}else this.filter=e;return this},removeFilter:function(e){return this.setFilter(null,e)},_applyModelDeltas:function(e,i){var n=this,s={};t.each(e,function(e,t){!n.children.findByModel(e)&&n._onCollectionAdd(e,n.collection,{at:t}),s[e.cid]=!0});var r=t.filter(i,function(e){return!s[e.cid]&&n.children.findByModel(e)});this._removeChildModels(r)},reorder:function(){var e=this,i=this.children,n=this._filteredSortedModels();if(!n.length&&this._showingEmptyView)return this;if(t.some(n,function(e){return!i.findByModel(e)}))this.render();else{var s=[],r=t.reduce(this.children._views,function(e,i){var r=t.indexOf(n,i.model);return-1===r?(s.push(i.model),e):(i._index=r,e[r]=i.el,e)},new Array(n.length));this.triggerMethod("before:reorder",this);var o=this.Dom.createBuffer();t.each(r,function(t){e.Dom.appendContents(o,t)}),this._appendReorderedChildren(o),this._removeChildModels(s),this.triggerMethod("reorder",this)}return this},resortView:function(){return this.reorderOnSort?this.reorder():this._renderChildren(),this},_sortViews:function(){var e=this,i=this._filteredSortedModels();t.find(i,function(t,i){var n=e.children.findByModel(t);return!n||n._index!==i})&&this.resortView()},_emptyViewIndex:-1,_appendReorderedChildren:function(e){this.Dom.appendContents(this.el,e,{_$el:this.$el})},_renderChildren:function(){this._isRendered&&(this._destroyEmptyView(),this._destroyChildren());var e=this._filteredSortedModels();this.isEmpty({processedModels:e})?this._showEmptyView():(this.triggerMethod("before:render:children",this),this._startBuffering(),this._showCollection(e),this._endBuffering(),this.triggerMethod("render:children",this))},_createView:function(e,t){var i=this._getChildView(e),n=this._getChildViewOptions(e,t);return this.buildChildView(e,i,n)},_setupChildView:function(e,t){w(e),this._proxyChildViewEvents(e),this.sort&&(e._index=t)},_showCollection:function(e){t.each(e,t.bind(this._addChild,this)),this.children._updateLength()},_filteredSortedModels:function(e){if(!this.collection||!this.collection.length)return[];var t=this.getViewComparator(),i=this.collection.models;if(e=Math.min(Math.max(e,0),i.length-1),t){var n=void 0;e&&(n=i[e],i=i.slice(0,e).concat(i.slice(e+1))),i=this._sortModelsBy(i,t),n&&i.splice(e,0,n)}return i=this._filterModels(i)},getViewComparator:function(){return this.viewComparator},_filterModels:function(e){var i=this;return this.filter&&(e=t.filter(e,function(e,t){return i._shouldAddChild(e,t)})),e},_sortModelsBy:function(e,i){return"string"==typeof i?t.sortBy(e,function(e){return e.get(i)}):1===i.length?t.sortBy(e,t.bind(i,this)):t.clone(e).sort(t.bind(i,this))},_showEmptyView:function(){var i=this._getEmptyView();if(i&&!this._showingEmptyView){this._showingEmptyView=!0;var n=new e.Model,s=this.emptyViewOptions||this.childViewOptions;t.isFunction(s)&&(s=s.call(this,n,this._emptyViewIndex));var r=this.buildChildView(n,i,s);this.triggerMethod("before:render:empty",this,r),this.addChildView(r,0),this.triggerMethod("render:empty",this,r)}},_destroyEmptyView:function(){this._showingEmptyView&&(this.triggerMethod("before:remove:empty",this),this._destroyChildren(),delete this._showingEmptyView,this.triggerMethod("remove:empty",this))},_getEmptyView:function(){var e=this.emptyView;if(e)return this._getView(e)},_getChildView:function(e){var t=this.childView;if(!t)throw new Z({name:"NoChildViewError",message:'A "childView" must be specified'});if(!(t=this._getView(t,e)))throw new Z({name:"InvalidChildViewError",message:'"childView" must be a view class or a function that returns a view class'});return t},_getView:function(i,n){return i.prototype instanceof e.View||i===e.View?i:t.isFunction(i)?i.call(this,n):void 0},_addChild:function(e,t){var i=this._createView(e,t);return this.addChildView(i,t),i},_getChildViewOptions:function(e,i){return t.isFunction(this.childViewOptions)?this.childViewOptions(e,i):this.childViewOptions},addChildView:function(e,t){return this.triggerMethod("before:add:child",this,e),this._setupChildView(e,t),this._isBuffering?this.children._add(e):(this._updateIndices(e,!0),this.children.add(e)),T(e),this._attachView(e,t),this.triggerMethod("add:child",this,e),e},_updateIndices:function(e,i){if(this.sort)if(i){var n=t.isArray(e)?t.max(e,"_index"):e;t.isObject(n)&&t.each(this.children._views,function(e){e._index>=n._index&&(e._index+=1)})}else t.each(t.sortBy(this.children._views,"_index"),function(e,t){e._index=t})},_attachView:function(e,t){var i=!e._isAttached&&!this._isBuffering&&this._isAttached&&!1!==this.monitorViewEvents;i&&r(e,"before:attach",e),this.attachHtml(this,e,t),i&&(e._isAttached=!0,r(e,"attach",e))},buildChildView:function(e,i,n){return new i(t.extend({model:e},n))},removeChildView:function(e){return!e||e._isDestroyed?e:(this._removeChildView(e),this.children._updateLength(),this._updateIndices(e,!1),e)},isEmpty:function(e){var i=void 0;return t.result(e,"processedModels")?i=e.processedModels:(i=this.collection?this.collection.models:[],i=this._filterModels(i)),0===i.length},attachBuffer:function(e,t){this.Dom.appendContents(e.el,t,{_$el:e.$el})},_createBuffer:function(){var e=this,i=this.Dom.createBuffer();return t.each(this._bufferedChildren,function(t){e.Dom.appendContents(i,t.el,{_$contents:t.$el})}),i},attachHtml:function(e,t,i){e._isBuffering?e._bufferedChildren.splice(i,0,t):e._insertBefore(t,i)||e._insertAfter(t)},_insertBefore:function(e,i){var n=void 0;return this.sort&&i1&&void 0!==arguments[1]?arguments[1]:this._views.length,i=e.cid;this._viewsByCid[i]=e,e.model&&(this._indexByModel[e.model.cid]=i),this._views.splice(t,0,e),this._updateLength()},_sort:function(e){return"string"==typeof e?(e=t.partial(L,e),this._sortBy(e)):1===e.length?this._sortBy(e):this._views.sort(e)},_sortBy:function(e){var i=t.sortBy(this._views,e);return this._set(i),i},_set:function(e){this._views.length=0,this._views.push.apply(this._views,e.slice(0)),this._updateLength()},_swap:function(e,t){var i=this.findIndexByView(e),n=this.findIndexByView(t);if(-1!==i&&-1!==n){var s=this._views[i];this._views[i]=this._views[n],this._views[n]=s}},findByModel:function(e){return this.findByModelCid(e.cid)},findByModelCid:function(e){var t=this._indexByModel[e];return this.findByCid(t)},findByIndex:function(e){return this._views[e]},findIndexByView:function(e){return this._views.indexOf(e)},findByCid:function(e){return this._viewsByCid[e]},hasView:function(e){return!!this.findByCid(e.cid)},_remove:function(e){if(this._viewsByCid[e.cid]){e.model&&delete this._indexByModel[e.model.cid],delete this._viewsByCid[e.cid];var t=this.findIndexByView(e);this._views.splice(t,1),this._updateLength()}},_updateLength:function(){this.length=this._views.length}});var Be=["behaviors","childView","childViewEventPrefix","childViewEvents","childViewOptions","childViewTriggers","collectionEvents","emptyView","emptyViewOptions","events","modelEvents","sortWithCollection","triggers","ui","viewComparator","viewFilter"],xe=e.View.extend({sortWithCollection:!0,constructor:function(t){this._setOptions(t),this.mergeOptions(t,Be),w(this),this.once("render",this._initialEvents),this._initChildViewStorage(),this._initBehaviors();var i=Array.prototype.slice.call(arguments);i[0]=this.options,e.View.prototype.constructor.apply(this,i),this.getEmptyRegion(),this.delegateEntityEvents(),this._triggerEventOnBehaviors("initialize",this)},_initChildViewStorage:function(){this.children=new Re},getEmptyRegion:function(){return this._emptyRegion&&!this._emptyRegion.isDestroyed()?this._emptyRegion:(this._emptyRegion=new pe({el:this.el,replaceElement:!1}),this._emptyRegion._parentView=this,this._emptyRegion)},_initialEvents:function(){this.listenTo(this.collection,{sort:this._onCollectionSort,reset:this._onCollectionReset,update:this._onCollectionUpdate})},_onCollectionSort:function(){var e=this;this.sortWithCollection&&!1!==this.viewComparator&&this.collection.length===this.children.length&&(this.collection.some(function(t){return!e.children.findByModel(t)})||this.sort())},_onCollectionReset:function(){this.render()},_onCollectionUpdate:function(e,t){var i=t.changes,n=i.removed.length&&this._removeChildModels(i.removed);this._addedViews=i.added.length&&this._addChildModels(i.added),this._detachChildren(n),this._showChildren(),this._removeChildViews(n)},_removeChildModels:function(e){return t.map(e,t.bind(this._removeChildModel,this))},_removeChildModel:function(e){var t=this.children.findByModel(e);return this._removeChild(t),t},_removeChild:function(e){this.triggerMethod("before:remove:child",this,e),this.children._remove(e),this.triggerMethod("remove:child",this,e)},_addChildModels:function(e){return t.map(e,t.bind(this._addChildModel,this))},_addChildModel:function(e){var t=this._createChildView(e);return this._addChild(t),t},_createChildView:function(e){var t=this._getChildView(e),i=this._getChildViewOptions(e);return this.buildChildView(e,t,i)},_addChild:function(e,t){this.triggerMethod("before:add:child",this,e),this._setupChildView(e),this.children._add(e,t),this.triggerMethod("add:child",this,e)},_getChildView:function(e){var t=this.childView;if(!t)throw new Z({name:"NoChildViewError",message:'A "childView" must be specified'});if(!(t=this._getView(t,e)))throw new Z({name:"InvalidChildViewError",message:'"childView" must be a view class or a function that returns a view class'});return t},_getView:function(i,n){return i.prototype instanceof e.View||i===e.View?i:t.isFunction(i)?i.call(this,n):void 0},_getChildViewOptions:function(e){return t.isFunction(this.childViewOptions)?this.childViewOptions(e):this.childViewOptions},buildChildView:function(e,i,n){return new i(t.extend({model:e},n))},_setupChildView:function(e){w(e),e.on("destroy",this.removeChildView,this),this._proxyChildViewEvents(e)},_getImmediateChildren:function(){return this.children._views},setElement:function(){var t=!!this.el;return e.View.prototype.setElement.apply(this,arguments),t&&(this._isAttached=j(this.el)),this},render:function(){return this._isDestroyed?this:(this.triggerMethod("before:render",this),this._destroyChildren(),this.children._init(),this.collection&&this._addChildModels(this.collection.models),this._showChildren(),this._isRendered=!0,this.triggerMethod("render",this),this)},sort:function(){return this._isDestroyed?this:this.children.length?(this._showChildren(),this):this},_showChildren:function(){this.isEmpty()?this._showEmptyView():(this._sortChildren(),this.filter())},isEmpty:function(e){return e||!this.children.length},_showEmptyView:function(){var e=this._getEmptyView();if(e){var t=this._getEmptyViewOptions();this.getEmptyRegion().show(new e(t))}},_getEmptyView:function(){var e=this.emptyView;if(e)return this._getView(e)},_destroyEmptyView:function(){var e=this.getEmptyRegion();e.hasView()&&e.empty()},_getEmptyViewOptions:function(){var e=this.emptyViewOptions||this.childViewOptions;return t.isFunction(e)?e.call(this):e},_sortChildren:function(){if(!1!==this.viewComparator){this.triggerMethod("before:sort",this);var e=this.getComparator();t.isFunction(e)&&(e=e.bind(this)),this.children._sort(e),this.triggerMethod("sort",this)}},setComparator:function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).preventRender,i=this.viewComparator!==e&&!t;return this.viewComparator=e,i&&this.sort(),this},removeComparator:function(e){return this.setComparator(null,e)},getComparator:function(){return this.viewComparator||this._viewComparator},_viewComparator:function(e){if(this.collection)return this.collection.indexOf(e.model)},filter:function(){if(this._isDestroyed)return this;if(!this.children.length)return this;var e=this._filterChildren();return this._renderChildren(e),this},_isAddedAtEnd:function(e,t,i){var n=this.children._views.length-i.length+t;return e===this.children._views[n]},_filterChildren:function(){var e=this._getFilter(),i=this._addedViews;if(delete this._addedViews,!e)return i&&t.every(i,t.bind(this._isAddedAtEnd,this))?i:this.children._views;this.triggerMethod("before:filter",this);var n=t.partition(this.children._views,t.bind(e,this));return this._detachChildren(n[1]),this.triggerMethod("filter",this),n[0]},_getFilter:function(){var e=this.getFilter();if(!e)return!1;if(t.isFunction(e))return e;if(t.isObject(e)){var i=t.matches(e);return function(e){return i(e.model&&e.model.attributes)}}if(t.isString(e))return function(t){return t.model&&t.model.get(e)};throw new Z({name:"InvalidViewFilterError",message:'"viewFilter" must be a function, predicate object literal, a string indicating a model attribute, or falsy'})},getFilter:function(){return this.viewFilter},setFilter:function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).preventRender,i=this.viewFilter!==e&&!t;return this.viewFilter=e,i&&this.filter(),this},removeFilter:function(e){return this.setFilter(null,e)},_detachChildren:function(e){t.each(e,t.bind(this._detachChildView,this))},_detachChildView:function(e){var t=e._isAttached&&!1!==this.monitorViewEvents;t&&r(e,"before:detach",e),this.detachHtml(e),t&&(e._isAttached=!1,r(e,"detach",e))},detachHtml:function(e){this.Dom.detachEl(e.el,e.$el)},_renderChildren:function(e){if(this.isEmpty(!e.length))this._showEmptyView();else{this._destroyEmptyView(),this.triggerMethod("before:render:children",this,e);var t=this._getBuffer(e);this._attachChildren(t,e),this.triggerMethod("render:children",this,e)}},_attachChildren:function(e,i){i=this._isAttached&&!1!==this.monitorViewEvents?i:[],t.each(i,function(e){e._isAttached||r(e,"before:attach",e)}),this.attachHtml(e),t.each(i,function(e){e._isAttached||(e._isAttached=!0,r(e,"attach",e))})},_getBuffer:function(e){var i=this,n=this.Dom.createBuffer();return t.each(e,function(e){T(e),i.Dom.appendContents(n,e.el,{_$contents:e.$el})}),n},attachHtml:function(e){this.Dom.appendContents(this.el,e,{_$el:this.$el})},swapChildViews:function(e,t){if(!this.children.hasView(e)||!this.children.hasView(t))throw new Z({name:"ChildSwapError",message:"Both views must be children of the collection view"});return this.children._swap(e,t),this.Dom.swapEl(e.el,t.el),this.Dom.hasEl(this.el,e.el)!==this.Dom.hasEl(this.el,t.el)&&this.filter(),this},addChildView:function(e,t){return!e||e._isDestroyed?e:(this._addChild(e,t),this._addedViews=[e],this._showChildren(),e)},detachChildView:function(e){return this.removeChildView(e,{shouldDetach:!0}),e},removeChildView:function(e,t){return e?(this._removeChildView(e,t),this._removeChild(e),this.isEmpty()&&this._showEmptyView(),e):e},_removeChildViews:function(e){t.each(e,t.bind(this._removeChildView,this))},_removeChildView:function(e){var t=(arguments.length>1&&void 0!==arguments[1]?arguments[1]:{}).shouldDetach;e.off("destroy",this.removeChildView,this),t?this._detachChildView(e):this._destroyChildView(e),this.stopListening(e)},_destroyChildView:function(e){e._isDestroyed||(e._shouldDisableEvents=!1===this.monitorViewEvents,U(e))},_removeChildren:function(){this._destroyChildren(),this.getEmptyRegion().destroy(),delete this._addedViews},_destroyChildren:function(){this.children&&this.children.length&&(this.triggerMethod("before:destroy:children",this),!1===this.monitorViewEvents&&this.Dom.detachContents(this.el,this.$el),t.each(this.children._views,t.bind(this._removeChildView,this)),this.triggerMethod("destroy:children",this))}},{setDomApi:$});t.extend(xe.prototype,fe);var De=["childViewContainer","template","templateContext"],Ae=Me.extend({constructor:function(e){k("CompositeView is deprecated. Convert to View at your earliest convenience"),this.mergeOptions(e,De),Me.prototype.constructor.apply(this,arguments)},_initialEvents:function(){this.collection&&(this.listenTo(this.collection,"add",this._onCollectionAdd),this.listenTo(this.collection,"update",this._onCollectionUpdate),this.listenTo(this.collection,"reset",this.renderChildren),this.sort&&this.listenTo(this.collection,"sort",this._sortViews))},_getChildView:function(e){var t=this.childView;if(!t)return this.constructor;if(!(t=this._getView(t,e)))throw new Z({name:"InvalidChildViewError",message:'"childView" must be a view class or a function that returns a view class'});return t},serializeData:function(){return this.serializeModel()},render:function(){return this._isDestroyed?this:(this._isRendering=!0,this.resetChildViewContainer(),this.triggerMethod("before:render",this),this._renderTemplate(),this.bindUIElements(),this.renderChildren(),this._isRendering=!1,this._isRendered=!0,this.triggerMethod("render",this),this)},renderChildren:function(){(this._isRendered||this._isRendering)&&Me.prototype._renderChildren.call(this)},attachBuffer:function(e,t){var i=this.getChildViewContainer(e);this.Dom.appendContents(i[0],t,{_$el:i})},_insertAfter:function(e){var t=this.getChildViewContainer(this,e);this.Dom.appendContents(t[0],e.el,{_$el:t,_$contents:e.$el})},_appendReorderedChildren:function(e){var t=this.getChildViewContainer(this);this.Dom.appendContents(t[0],e,{_$el:t})},getChildViewContainer:function(e,i){if(e.$childViewContainer)return e.$childViewContainer;var n=void 0;if(e.childViewContainer){var s=t.result(e,"childViewContainer");if((n="@"===s.charAt(0)&&e.ui?e.ui[s.substr(4)]:this.$(s)).length<=0)throw new Z({name:"ChildViewContainerMissingError",message:'The specified "childViewContainer" was not found: '+e.childViewContainer})}else n=e.$el;return e.$childViewContainer=n,n},resetChildViewContainer:function(){this.$childViewContainer&&(this.$childViewContainer=void 0)}}),Oe=t.pick(ye.prototype,"serializeModel","getTemplate","_renderTemplate","_renderHtml","mixinTemplateContext","attachElContent");t.extend(Ae.prototype,Oe);var Ie=["collectionEvents","events","modelEvents","triggers","ui"],$e=X.extend({cidPrefix:"mnb",constructor:function(e,i){this.view=i,this.defaults&&k("Behavior defaults are deprecated. For similar functionality set options on the Behavior class."),this.defaults=t.clone(t.result(this,"defaults",{})),this._setOptions(t.extend({},this.defaults,e)),this.mergeOptions(this.options,Ie),this.ui=t.extend({},t.result(this,"ui"),t.result(i,"ui")),X.apply(this,arguments)},$:function(){return this.view.$.apply(this.view,arguments)},destroy:function(){return this.stopListening(),this.view._removeBehavior(this),this},proxyViewProperties:function(){return this.$el=this.view.$el,this.el=this.view.el,this},bindUIElements:function(){return this._bindUIElements(),this},unbindUIElements:function(){return this._unbindUIElements(),this},getUI:function(e){return this._getUI(e)},delegateEntityEvents:function(){return this._delegateEntityEvents(this.view.model,this.view.collection),this},undelegateEntityEvents:function(){return this._undelegateEntityEvents(this.view.model,this.view.collection),this},getEvents:function(){var e=this,i=this.normalizeUIKeys(t.result(this,"events"));return t.reduce(i,function(i,n,s){if(t.isFunction(n)||(n=e[n]),n)return s=re(s),i[s]=t.bind(n,e),i},{})},getTriggers:function(){if(this.triggers){var e=this.normalizeUIKeys(t.result(this,"triggers"));return this._getViewTriggers(this.view,e)}}});t.extend($e.prototype,ne,he,ce);var Te=["region","regionClass"],Ue=X.extend({cidPrefix:"mna",constructor:function(e){this._setOptions(e),this.mergeOptions(e,Te),this._initRegion(),X.prototype.constructor.apply(this,arguments)},regionClass:pe,_initRegion:function(){var e=this.region;if(e){var t={regionClass:this.regionClass};this._region=ge(e,t)}},getRegion:function(){return this._region},showView:function(e){for(var t=this.getRegion(),i=arguments.length,n=Array(i>1?i-1:0),s=1;s {\n if (!shouldTrigger(child)) { return; }\n triggerMethodOn(child, event, child);\n });\n}\n\nfunction shouldTriggerAttach(view) {\n return !view._isAttached;\n}\n\nfunction shouldAttach(view) {\n if (!shouldTriggerAttach(view)) { return false; }\n view._isAttached = true;\n return true;\n}\n\nfunction shouldTriggerDetach(view) {\n return view._isAttached;\n}\n\nfunction shouldDetach(view) {\n if (!shouldTriggerDetach(view)) { return false; }\n view._isAttached = false;\n return true;\n}\n\nfunction triggerDOMRefresh(view) {\n if (view._isAttached && view._isRendered) {\n triggerMethodOn(view, 'dom:refresh', view);\n }\n}\n\nfunction triggerDOMRemove(view) {\n if (view._isAttached && view._isRendered) {\n triggerMethodOn(view, 'dom:remove', view);\n }\n}\n\nfunction handleBeforeAttach() {\n triggerMethodChildren(this, 'before:attach', shouldTriggerAttach);\n}\n\nfunction handleAttach() {\n triggerMethodChildren(this, 'attach', shouldAttach);\n triggerDOMRefresh(this);\n}\n\nfunction handleBeforeDetach() {\n triggerMethodChildren(this, 'before:detach', shouldTriggerDetach);\n triggerDOMRemove(this);\n}\n\nfunction handleDetach() {\n triggerMethodChildren(this, 'detach', shouldDetach);\n}\n\nfunction handleBeforeRender() {\n triggerDOMRemove(this);\n}\n\nfunction handleRender() {\n triggerDOMRefresh(this);\n}\n\n// Monitor a view's state, propagating attach/detach events to children and firing dom:refresh\n// whenever a rendered view is attached or an attached view is rendered.\nfunction monitorViewEvents(view) {\n if (view._areViewEventsMonitored || view.monitorViewEvents === false) { return; }\n\n view._areViewEventsMonitored = true;\n\n view.on({\n 'before:attach': handleBeforeAttach,\n 'attach': handleAttach,\n 'before:detach': handleBeforeDetach,\n 'detach': handleDetach,\n 'before:render': handleBeforeRender,\n 'render': handleRender\n });\n}\n\nexport default monitorViewEvents;\n","// Bind Entity Events & Unbind Entity Events\n// -----------------------------------------\n//\n// These methods are used to bind/unbind a backbone \"entity\" (e.g. collection/model)\n// to methods on a target object.\n//\n// The first parameter, `target`, must have the Backbone.Events module mixed in.\n//\n// The second parameter is the `entity` (Backbone.Model, Backbone.Collection or\n// any object that has Backbone.Events mixed in) to bind the events from.\n//\n// The third parameter is a hash of { \"event:name\": \"eventHandler\" }\n// configuration. Multiple handlers can be separated by a space. A\n// function can be supplied instead of a string handler name.\n\nimport _ from 'underscore';\nimport MarionetteError from '../error';\n\n// Bind/unbind the event to handlers specified as a string of\n// handler names on the target object\nfunction bindFromStrings(target, entity, evt, methods, actionName) {\n const methodNames = methods.split(/\\s+/);\n\n _.each(methodNames, function(methodName) {\n const method = target[methodName];\n if (!method) {\n throw new MarionetteError(`Method \"${methodName}\" was configured as an event handler, but does not exist.`);\n }\n\n target[actionName](entity, evt, method);\n });\n}\n\n// generic looping function\nfunction iterateEvents(target, entity, bindings, actionName) {\n if (!entity || !bindings) { return; }\n\n // type-check bindings\n if (!_.isObject(bindings)) {\n throw new MarionetteError({\n message: 'Bindings must be an object.',\n url: 'marionette.functions.html#marionettebindevents'\n });\n }\n\n // iterate the bindings and bind/unbind them\n _.each(bindings, function(method, evt) {\n\n // allow for a list of method names as a string\n if (_.isString(method)) {\n bindFromStrings(target, entity, evt, method, actionName);\n return;\n }\n\n target[actionName](entity, evt, method);\n });\n}\n\nfunction bindEvents(entity, bindings) {\n iterateEvents(this, entity, bindings, 'listenTo');\n return this;\n}\n\nfunction unbindEvents(entity, bindings) {\n iterateEvents(this, entity, bindings, 'stopListening');\n return this;\n}\n\n// Export Public API\nexport {\n bindEvents,\n unbindEvents\n};\n","// Bind/Unbind Radio Requests\n// -----------------------------------------\n//\n// These methods are used to bind/unbind a backbone.radio request\n// to methods on a target object.\n//\n// The first parameter, `target`, will set the context of the reply method\n//\n// The second parameter is the `Radio.channel` to bind the reply to.\n//\n// The third parameter is a hash of { \"request:name\": \"replyHandler\" }\n// configuration. A function can be supplied instead of a string handler name.\n\nimport _ from 'underscore';\nimport normalizeMethods from './normalize-methods';\nimport MarionetteError from '../error';\n\nfunction iterateReplies(target, channel, bindings, actionName) {\n if (!channel || !bindings) { return; }\n\n // type-check bindings\n if (!_.isObject(bindings)) {\n throw new MarionetteError({\n message: 'Bindings must be an object.',\n url: 'marionette.functions.html#marionettebindrequests'\n });\n }\n\n const normalizedRadioRequests = normalizeMethods.call(target, bindings);\n\n channel[actionName](normalizedRadioRequests, target);\n}\n\nfunction bindRequests(channel, bindings) {\n iterateReplies(this, channel, bindings, 'reply');\n return this;\n}\n\nfunction unbindRequests(channel, bindings) {\n iterateReplies(this, channel, bindings, 'stopReplying');\n return this;\n}\n\nexport {\n bindRequests,\n unbindRequests\n};\n","import _ from 'underscore';\nimport _invoke from '../utils/invoke';\nimport { triggerMethod } from '../common/trigger-method';\nimport Marionette from '../backbone.marionette';\n\n// MixinOptions\n// - behaviors\n\n// Takes care of getting the behavior class\n// given options and a key.\n// If a user passes in options.behaviorClass\n// default to using that.\n// If a user passes in a Behavior Class directly, use that\n// Otherwise delegate the lookup to the users `behaviorsLookup` implementation.\nfunction getBehaviorClass(options, key) {\n if (options.behaviorClass) {\n return options.behaviorClass;\n //treat functions as a Behavior constructor\n } else if (_.isFunction(options)) {\n return options;\n }\n\n // behaviorsLookup can be either a flat object or a method\n if (_.isFunction(Marionette.Behaviors.behaviorsLookup)) {\n return Marionette.Behaviors.behaviorsLookup(options, key)[key];\n }\n\n return Marionette.Behaviors.behaviorsLookup[key];\n}\n\n// Iterate over the behaviors object, for each behavior\n// instantiate it and get its grouped behaviors.\n// This accepts a list of behaviors in either an object or array form\nfunction parseBehaviors(view, behaviors) {\n return _.chain(behaviors).map(function(options, key) {\n const BehaviorClass = getBehaviorClass(options, key);\n //if we're passed a class directly instead of an object\n const _options = options === BehaviorClass ? {} : options;\n const behavior = new BehaviorClass(_options, view);\n const nestedBehaviors = parseBehaviors(view, _.result(behavior, 'behaviors'));\n\n return [behavior].concat(nestedBehaviors);\n }).flatten().value();\n}\n\nexport default {\n _initBehaviors() {\n this._behaviors = this._getBehaviors();\n },\n\n _getBehaviors() {\n const behaviors = _.result(this, 'behaviors');\n\n // Behaviors defined on a view can be a flat object literal\n // or it can be a function that returns an object.\n return _.isObject(behaviors) ? parseBehaviors(this, behaviors) : {};\n },\n\n _getBehaviorTriggers() {\n const triggers = _invoke(this._behaviors, 'getTriggers');\n return _.reduce(triggers, function(memo, _triggers) {\n return _.extend(memo, _triggers);\n }, {});\n },\n\n _getBehaviorEvents() {\n const events = _invoke(this._behaviors, 'getEvents');\n return _.reduce(events, function(memo, _events) {\n return _.extend(memo, _events);\n }, {});\n },\n\n // proxy behavior $el to the view's $el.\n _proxyBehaviorViewProperties() {\n _invoke(this._behaviors, 'proxyViewProperties');\n },\n\n // delegate modelEvents and collectionEvents\n _delegateBehaviorEntityEvents() {\n _invoke(this._behaviors, 'delegateEntityEvents');\n },\n\n // undelegate modelEvents and collectionEvents\n _undelegateBehaviorEntityEvents() {\n _invoke(this._behaviors, 'undelegateEntityEvents');\n },\n\n _destroyBehaviors(...args) {\n // Call destroy on each behavior after\n // destroying the view.\n // This unbinds event listeners\n // that behaviors have registered for.\n _invoke(this._behaviors, 'destroy', ...args);\n },\n\n // Remove a behavior\n _removeBehavior(behavior) {\n // Don't worry about the clean up if the view is destroyed\n if (this._isDestroyed) { return; }\n this._behaviors = _.without(this._behaviors, behavior);\n },\n\n _bindBehaviorUIElements() {\n _invoke(this._behaviors, 'bindUIElements');\n },\n\n _unbindBehaviorUIElements() {\n _invoke(this._behaviors, 'unbindUIElements');\n },\n\n _triggerEventOnBehaviors() {\n const behaviors = this._behaviors;\n // Use good ol' for as this is a very hot function\n for (let i = 0, length = behaviors && behaviors.length; i < length; i++) {\n triggerMethod.apply(behaviors[i], arguments);\n }\n }\n};\n","import _ from 'underscore';\n\n// Borrow event splitter from Backbone\nconst delegateEventSplitter = /^(\\S+)\\s*(.*)$/;\n\nfunction uniqueName(eventName, selector) {\n return `${ eventName }${ _.uniqueId('.evt') } ${ selector }`;\n}\n\n// Set event name to be namespaced using a unique index\n// to generate a non colliding event namespace\n// http://api.jquery.com/event.namespace/\nconst getUniqueEventName = function(eventName) {\n const match = eventName.match(delegateEventSplitter);\n return uniqueName(match[1], match[2]);\n};\n\nexport default getUniqueEventName;\n","// Add Feature flags here\n// e.g. 'class' => false\nconst FEATURES = {\n childViewEventPrefix: true,\n triggersStopPropagation: true,\n triggersPreventDefault: true\n};\n\nfunction isEnabled(name) {\n return !!FEATURES[name];\n}\n\nfunction setEnabled(name, state) {\n return FEATURES[name] = state;\n}\n\nexport {\n FEATURES,\n setEnabled,\n isEnabled\n};\n","import _ from 'underscore';\nimport getUniqueEventName from '../utils/get-unique-event-name';\nimport { isEnabled } from '../config/features';\n\n// Internal method to create an event handler for a given `triggerDef` like\n// 'click:foo'\nfunction buildViewTrigger(view, triggerDef) {\n if (_.isString(triggerDef)) {\n triggerDef = {event: triggerDef};\n }\n\n const eventName = triggerDef.event;\n\n let shouldPreventDefault = !!triggerDef.preventDefault;\n\n if (isEnabled('triggersPreventDefault')) {\n shouldPreventDefault = triggerDef.preventDefault !== false;\n }\n\n let shouldStopPropagation = !!triggerDef.stopPropagation;\n\n if (isEnabled('triggersStopPropagation')) {\n shouldStopPropagation = triggerDef.stopPropagation !== false;\n }\n\n return function(event) {\n if (shouldPreventDefault) {\n event.preventDefault();\n }\n\n if (shouldStopPropagation) {\n event.stopPropagation();\n }\n\n view.triggerMethod(eventName, view, event);\n };\n}\n\nexport default {\n\n // Configure `triggers` to forward DOM events to view\n // events. `triggers: {\"click .foo\": \"do:foo\"}`\n _getViewTriggers(view, triggers) {\n // Configure the triggers, prevent default\n // action and stop propagation of DOM events\n return _.reduce(triggers, (events, value, key) => {\n key = getUniqueEventName(key);\n events[key] = buildViewTrigger(view, value);\n return events;\n }, {});\n }\n\n};\n","// DomApi\n// ---------\nimport _ from 'underscore';\nimport Backbone from 'backbone';\n\n// Performant method for returning the jQuery instance\nfunction getEl(el) {\n return el instanceof Backbone.$ ? el : Backbone.$(el);\n}\n\n// Static setter\nexport function setDomApi(mixin) {\n this.prototype.Dom = _.extend({}, this.prototype.Dom, mixin);\n return this;\n}\n\nexport default {\n\n // Returns a new HTML DOM node instance\n createBuffer() {\n return document.createDocumentFragment();\n },\n\n // Lookup the `selector` string\n // Selector may also be a DOM element\n // Returns an array-like object of nodes\n getEl(selector) {\n return getEl(selector);\n },\n\n // Finds the `selector` string with the el\n // Returns an array-like object of nodes\n findEl(el, selector, _$el = getEl(el)) {\n return _$el.find(selector);\n },\n\n // Returns true if the el contains the node childEl\n hasEl(el, childEl) {\n return el.contains(childEl && childEl.parentNode);\n },\n\n // Detach `el` from the DOM without removing listeners\n detachEl(el, _$el = getEl(el)) {\n _$el.detach();\n },\n\n // Remove `oldEl` from the DOM and put `newEl` in its place\n replaceEl(newEl, oldEl) {\n if (newEl === oldEl) {\n return;\n }\n\n const parent = oldEl.parentNode;\n\n if (!parent) {\n return;\n }\n\n parent.replaceChild(newEl, oldEl);\n },\n\n // Swaps the location of `el1` and `el2` in the DOM\n swapEl(el1, el2) {\n if (el1 === el2) {\n return;\n }\n\n const parent1 = el1.parentNode;\n const parent2 = el2.parentNode;\n\n if (!parent1 || !parent2) {\n return;\n }\n\n const next1 = el1.nextSibling;\n const next2 = el2.nextSibling;\n\n parent1.insertBefore(el2, next1);\n parent2.insertBefore(el1, next2);\n },\n\n // Replace the contents of `el` with the HTML string of `html`\n setContents(el, html, _$el = getEl(el)) {\n _$el.html(html);\n },\n\n // Takes the DOM node `el` and appends the DOM node `contents`\n // to the end of the element's contents.\n appendContents(el, contents, {_$el = getEl(el), _$contents = getEl(contents)} = {}) {\n _$el.append(_$contents);\n },\n\n // Does the el have child nodes\n hasContents(el) {\n return el.hasChildNodes();\n },\n\n // Remove the inner contents of `el` from the DOM while leaving\n // `el` itself in the DOM.\n detachContents(el, _$el = getEl(el)) {\n _$el.contents().detach();\n }\n};\n","import { triggerMethodOn } from '../common/trigger-method';\n\nexport function renderView(view) {\n if (view._isRendered) {\n return;\n }\n\n if (!view.supportsRenderLifecycle) {\n triggerMethodOn(view, 'before:render', view);\n }\n\n view.render();\n\n if (!view.supportsRenderLifecycle) {\n view._isRendered = true;\n triggerMethodOn(view, 'render', view);\n }\n}\n\nexport function destroyView(view) {\n if (view.destroy) {\n view.destroy();\n return;\n }\n\n if (!view.supportsDestroyLifecycle) {\n triggerMethodOn(view, 'before:destroy', view);\n }\n\n const shouldTriggerDetach = view._isAttached && !view._shouldDisableEvents;\n\n if (shouldTriggerDetach) {\n triggerMethodOn(view, 'before:detach', view);\n }\n\n view.remove();\n\n if (shouldTriggerDetach) {\n view._isAttached = false;\n triggerMethodOn(view, 'detach', view);\n }\n\n view._isDestroyed = true;\n\n if (!view.supportsDestroyLifecycle) {\n triggerMethodOn(view, 'destroy', view);\n }\n}\n","import _ from 'underscore';\nimport deprecate from '../utils/deprecate';\nimport MarionetteError from '../error';\nimport Region from '../region';\n\n// return the region instance from the definition\nexport default function(definition, defaults) {\n if (definition instanceof Region) {\n return definition;\n }\n\n return buildRegionFromDefinition(definition, defaults);\n}\n\nfunction buildRegionFromDefinition(definition, defaults) {\n const opts = _.extend({}, defaults);\n\n if (_.isString(definition)) {\n _.extend(opts, { el: definition });\n\n return buildRegionFromObject(opts);\n }\n\n if (_.isFunction(definition)) {\n _.extend(opts, { regionClass: definition });\n\n return buildRegionFromObject(opts);\n }\n\n if (_.isObject(definition)) {\n if (definition.selector) {\n deprecate('The selector option on a Region definition object is deprecated. Use el to pass a selector string');\n }\n\n _.extend(opts, { el: definition.selector }, definition);\n\n return buildRegionFromObject(opts);\n }\n\n throw new MarionetteError({\n message: 'Improper region configuration type.',\n url: 'marionette.region.html#region-configuration-types'\n });\n}\n\nfunction buildRegionFromObject(definition) {\n const RegionClass = definition.regionClass\n\n const options = _.omit(definition, 'regionClass');\n\n return new RegionClass(options);\n}\n","import _ from 'underscore';\nimport emulateCollection from './utils/emulate-collection';\n\n// Provide a container to store, retrieve and\n// shut down child views.\nconst Container = function() {\n this._init();\n};\n\nemulateCollection(Container.prototype, '_views');\n\nfunction stringComparator(comparator, view) {\n return view.model && view.model.get(comparator);\n}\n\n// Container Methods\n// -----------------\n\n_.extend(Container.prototype, {\n\n // Initializes an empty container\n _init() {\n this._views = [];\n this._viewsByCid = {};\n this._indexByModel = {};\n this._updateLength();\n },\n\n // Add a view to this container. Stores the view\n // by `cid` and makes it searchable by the model\n // cid (and model itself). Additionally it stores\n // the view by index in the _views array\n _add(view, index = this._views.length) {\n const viewCid = view.cid;\n\n // store the view\n this._viewsByCid[viewCid] = view;\n\n // index it by model\n if (view.model) {\n this._indexByModel[view.model.cid] = viewCid;\n }\n\n // add to end by default\n this._views.splice(index, 0, view);\n\n this._updateLength();\n },\n\n // Sort (mutate) and return the array of the child views.\n _sort(comparator) {\n if (typeof comparator === 'string') {\n comparator = _.partial(stringComparator, comparator);\n return this._sortBy(comparator);\n }\n\n if (comparator.length === 1) {\n return this._sortBy(comparator);\n }\n\n return this._views.sort(comparator);\n },\n\n // Makes `_.sortBy` mutate the array to match `this._views.sort`\n _sortBy(comparator) {\n const sortedViews = _.sortBy(this._views, comparator);\n\n this._set(sortedViews);\n\n return sortedViews;\n },\n\n // Replace array contents without overwriting the reference.\n _set(views) {\n this._views.length = 0;\n\n this._views.push.apply(this._views, views.slice(0));\n\n this._updateLength();\n },\n\n // Swap views by index\n _swap(view1, view2) {\n const view1Index = this.findIndexByView(view1);\n const view2Index = this.findIndexByView(view2);\n\n if (view1Index === -1 || view2Index === -1) {\n return;\n }\n\n const swapView = this._views[view1Index];\n this._views[view1Index] = this._views[view2Index];\n this._views[view2Index] = swapView;\n },\n\n // Find a view by the model that was attached to it.\n // Uses the model's `cid` to find it.\n findByModel(model) {\n return this.findByModelCid(model.cid);\n },\n\n // Find a view by the `cid` of the model that was attached to it.\n // Uses the model's `cid` to find the view `cid` and\n // retrieve the view using it.\n findByModelCid(modelCid) {\n const viewCid = this._indexByModel[modelCid];\n return this.findByCid(viewCid);\n },\n\n // Find a view by index.\n findByIndex(index) {\n return this._views[index];\n },\n\n // Find the index of a view instance\n findIndexByView(view) {\n return this._views.indexOf(view);\n },\n\n // Retrieve a view by its `cid` directly\n findByCid(cid) {\n return this._viewsByCid[cid];\n },\n\n hasView(view) {\n return !!this.findByCid(view.cid);\n },\n\n // Remove a view and clean up index references.\n _remove(view) {\n if (!this._viewsByCid[view.cid]) {\n return;\n }\n\n // delete model index\n if (view.model) {\n delete this._indexByModel[view.model.cid];\n }\n\n // remove the view from the container\n delete this._viewsByCid[view.cid];\n\n const index = this.findIndexByView(view);\n this._views.splice(index, 1);\n\n this._updateLength();\n },\n\n // Update the `.length` attribute on this container\n _updateLength() {\n this.length = this._views.length;\n }\n});\n\nexport default Container;\n","//Internal utility for creating context style global utils\nconst proxy = function(method) {\n return function(context, ...args) {\n return method.apply(context, args);\n };\n};\n\nexport default proxy;\n","// Marionette.extend\n// -----------------\n\nimport Backbone from 'backbone';\n\n// Borrow the Backbone `extend` method so we can use it as needed\nconst extend = Backbone.Model.extend;\n\nexport default extend;\n","/* global console */\n\nimport _ from 'underscore';\n\nimport Marionette from '../backbone.marionette';\n\nconst deprecate = function(message, test) {\n if (_.isObject(message)) {\n message = (\n message.prev + ' is going to be removed in the future. ' +\n 'Please use ' + message.next + ' instead.' +\n (message.url ? ' See: ' + message.url : '')\n );\n }\n\n if (!Marionette.DEV_MODE) {\n return;\n }\n\n if ((test === undefined || !test) && !deprecate._cache[message]) {\n deprecate._warn('Deprecation warning: ' + message);\n deprecate._cache[message] = true;\n }\n};\n\n/* istanbul ignore next: can't clear console */\ndeprecate._console = typeof console !== 'undefined' ? console : {};\ndeprecate._warn = function() {\n const warn = deprecate._console.warn || deprecate._console.log || _.noop;\n return warn.apply(deprecate._console, arguments);\n};\ndeprecate._cache = {};\n\nexport default deprecate;\n","// Marionette.isNodeAttached\n// -------------------------\n\n// Determine if `el` is a child of the document\nconst isNodeAttached = function(el) {\n return document.documentElement.contains(el && el.parentNode);\n};\n\nexport default isNodeAttached;\n","import _ from 'underscore';\n\n// Merge `keys` from `options` onto `this`\nconst mergeOptions = function(options, keys) {\n if (!options) { return; }\n\n _.each(keys, (key) => {\n const option = options[key];\n if (option !== undefined) {\n this[key] = option;\n }\n });\n};\n\nexport default mergeOptions;\n","// Marionette.getOption\n// --------------------\n\n// Retrieve an object, function or other value from the\n// object or its `options`, with `options` taking precedence.\nconst getOption = function(optionName) {\n if (!optionName) { return; }\n if (this.options && (this.options[optionName] !== undefined)) {\n return this.options[optionName];\n } else {\n return this[optionName];\n }\n};\n\nexport default getOption;\n","import _ from 'underscore';\n\n// Marionette.normalizeMethods\n// ----------------------\n\n// Pass in a mapping of events => functions or function names\n// and return a mapping of events => functions\nconst normalizeMethods = function(hash) {\n return _.reduce(hash, (normalizedHash, method, name) => {\n if (!_.isFunction(method)) {\n method = this[method];\n }\n if (method) {\n normalizedHash[name] = method;\n }\n return normalizedHash;\n }, {});\n};\n\nexport default normalizeMethods;\n","// Error\n// -----\n\nimport _ from 'underscore';\nimport extend from './utils/extend';\nimport {version} from '../package.json';\n\nconst errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number'];\n\nconst MarionetteError = extend.call(Error, {\n urlRoot: `http://marionettejs.com/docs/v${version}/`,\n\n constructor(message, options) {\n if (_.isObject(message)) {\n options = message;\n message = options.message;\n } else if (!options) {\n options = {};\n }\n\n const error = Error.call(this, message);\n _.extend(this, _.pick(error, errorProps), _.pick(options, errorProps));\n\n this.captureStackTrace();\n\n if (options.url) {\n this.url = this.urlRoot + options.url;\n }\n },\n\n captureStackTrace() {\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, MarionetteError);\n }\n },\n\n toString() {\n return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : '');\n }\n});\n\nMarionetteError.extend = extend;\n\nexport default MarionetteError;\n","import _ from 'underscore';\n\n// Internal utility for setting options consistently across Mn\nconst setOptions = function(options) {\n this.options = _.extend({}, _.result(this, 'options'), options);\n};\n\nexport default setOptions;\n","import _setOptions from '../utils/set-options';\nimport getOption from '../common/get-option';\nimport mergeOptions from '../common/merge-options';\nimport normalizeMethods from '../common/normalize-methods';\nimport {\n bindEvents,\n unbindEvents\n} from '../common/bind-events';\n\nexport default {\n\n // Imports the \"normalizeMethods\" to transform hashes of\n // events=>function references/names to a hash of events=>function references\n normalizeMethods,\n\n _setOptions,\n\n // A handy way to merge passed-in options onto the instance\n mergeOptions,\n\n // Enable getting options from this or this.options by name.\n getOption,\n\n // Enable binding view's events from another entity.\n bindEvents,\n\n // Enable unbinding view's events from another entity.\n unbindEvents\n};\n","import _ from 'underscore';\nimport Radio from 'backbone.radio';\n\nimport {\n bindRequests,\n unbindRequests\n} from '../common/bind-requests';\n\nimport {\n bindEvents,\n unbindEvents\n} from '../common/bind-events';\n\nimport MarionetteError from '../error';\n\n// MixinOptions\n// - channelName\n// - radioEvents\n// - radioRequests\n\nexport default {\n\n _initRadio() {\n const channelName = _.result(this, 'channelName');\n\n if (!channelName) {\n return;\n }\n\n /* istanbul ignore next */\n if (!Radio) {\n throw new MarionetteError({\n name: 'BackboneRadioMissing',\n message: 'The dependency \"backbone.radio\" is missing.'\n });\n }\n\n const channel = this._channel = Radio.channel(channelName);\n\n const radioEvents = _.result(this, 'radioEvents');\n this.bindEvents(channel, radioEvents);\n\n const radioRequests = _.result(this, 'radioRequests');\n this.bindRequests(channel, radioRequests);\n\n this.on('destroy', this._destroyRadio);\n },\n\n _destroyRadio() {\n this._channel.stopReplying(null, null, this);\n },\n\n getChannel() {\n return this._channel;\n },\n\n // Proxy `bindEvents`\n bindEvents: bindEvents,\n\n // Proxy `unbindEvents`\n unbindEvents: unbindEvents,\n\n // Proxy `bindRequests`\n bindRequests: bindRequests,\n\n // Proxy `unbindRequests`\n unbindRequests: unbindRequests\n\n};\n","// Object\n// ------\n\nimport _ from 'underscore';\nimport Backbone from 'backbone';\nimport extend from './utils/extend';\nimport { triggerMethod } from './common/trigger-method';\nimport CommonMixin from './mixins/common';\nimport RadioMixin from './mixins/radio';\n\nconst ClassOptions = [\n 'channelName',\n 'radioEvents',\n 'radioRequests'\n];\n\n// A Base Class that other Classes should descend from.\n// Object borrows many conventions and utilities from Backbone.\nconst MarionetteObject = function(options) {\n if (!this.hasOwnProperty('options')) {\n this._setOptions(options);\n }\n this.mergeOptions(options, ClassOptions);\n this._setCid();\n this._initRadio();\n this.initialize.apply(this, arguments);\n};\n\nMarionetteObject.extend = extend;\n\n// Object Methods\n// --------------\n\n// Ensure it can trigger events with Backbone.Events\n_.extend(MarionetteObject.prototype, Backbone.Events, CommonMixin, RadioMixin, {\n cidPrefix: 'mno',\n\n // for parity with Marionette.AbstractView lifecyle\n _isDestroyed: false,\n\n isDestroyed() {\n return this._isDestroyed;\n },\n\n //this is a noop method intended to be overridden by classes that extend from this base\n initialize() {},\n\n _setCid() {\n if (this.cid) { return; }\n this.cid = _.uniqueId(this.cidPrefix);\n },\n\n destroy(...args) {\n if (this._isDestroyed) { return this; }\n\n this.triggerMethod('before:destroy', this, ...args);\n\n this._isDestroyed = true;\n this.triggerMethod('destroy', this, ...args);\n this.stopListening();\n\n return this;\n },\n\n triggerMethod\n});\n\nexport default MarionetteObject;\n","// Template Cache\n// --------------\n\nimport _ from 'underscore';\nimport Backbone from 'backbone';\nimport MarionetteError from './error';\n\n// Manage templates stored in ` - - - diff --git a/templates/main.php b/templates/main.php index 393d2d67c..9049c06f2 100644 --- a/templates/main.php +++ b/templates/main.php @@ -1,14 +1,9 @@ - - Date: Fri, 2 Feb 2018 19:58:10 +0100 Subject: [PATCH 058/110] Add l10n to templates and format them to improve legibility --- js/templates/AddBookmark.html | 10 +++++- js/templates/App.html | 11 ++++++- js/templates/BookmarkCard.html | 39 +++++++++++++++++++++++- js/templates/BookmarkDetail_default.html | 10 +++++- js/templates/BookmarkDetail_editing.html | 16 +++++++++- js/templates/BulkActions.html | 12 +++++++- js/templates/Content.html | 5 ++- js/templates/EmptyBookmarks.html | 3 +- js/templates/MobileNav.html | 2 +- js/templates/Navigation.html | 10 +++++- 10 files changed, 108 insertions(+), 10 deletions(-) diff --git a/js/templates/AddBookmark.html b/js/templates/AddBookmark.html index 33f49ad47..f9e3434d7 100644 --- a/js/templates/AddBookmark.html +++ b/js/templates/AddBookmark.html @@ -1 +1,9 @@ -
  • + +
  • + + +
  • diff --git a/js/templates/App.html b/js/templates/App.html index d876f48e8..26d0afcc1 100644 --- a/js/templates/App.html +++ b/js/templates/App.html @@ -1 +1,10 @@ -

    Tags

    +
    +
    + +

    <%- t('bookmarks', 'Tags') %>

    +
    +
    +
    +
    +
    +
    diff --git a/js/templates/BookmarkCard.html b/js/templates/BookmarkCard.html index 91070654b..85e5bff27 100644 --- a/js/templates/BookmarkCard.html +++ b/js/templates/BookmarkCard.html @@ -1 +1,38 @@ -

    <%- title %>

    <%- new URL(url).host %>

    +
    + + +

    <%- title %>

    +

    <%- new URL(url).host %>

    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +
    diff --git a/js/templates/BookmarkDetail_default.html b/js/templates/BookmarkDetail_default.html index 90f12a90c..7443c9ce1 100644 --- a/js/templates/BookmarkDetail_default.html +++ b/js/templates/BookmarkDetail_default.html @@ -1 +1,9 @@ -

    <%- title %>

    <%- new URL(url).host %>

    <%- description %>
    +
    +
    + + +
    +

    <%- title %>

    +

    <%- new URL(url).host %>

    +
    +
    <%- description %>
    diff --git a/js/templates/BookmarkDetail_editing.html b/js/templates/BookmarkDetail_editing.html index 18aeef5a6..760fcb60d 100644 --- a/js/templates/BookmarkDetail_editing.html +++ b/js/templates/BookmarkDetail_editing.html @@ -1 +1,15 @@ -

    +
    +

    +

    +
    + +
    +
    + +
    +
    + + +
    diff --git a/js/templates/BulkActions.html b/js/templates/BulkActions.html index 890d81182..f1864ff92 100644 --- a/js/templates/BulkActions.html +++ b/js/templates/BulkActions.html @@ -1 +1,11 @@ -
    +
    + +
    +
    + +
    + +
    diff --git a/js/templates/Content.html b/js/templates/Content.html index 306caeb8c..33eef09c6 100644 --- a/js/templates/Content.html +++ b/js/templates/Content.html @@ -1 +1,4 @@ -
    +
    +
    +
    +
    diff --git a/js/templates/EmptyBookmarks.html b/js/templates/EmptyBookmarks.html index 1eac414da..fe91e76c5 100644 --- a/js/templates/EmptyBookmarks.html +++ b/js/templates/EmptyBookmarks.html @@ -1 +1,2 @@ -

    No bookmarks, here.

    There are no bookmarks available for this query. Try adding some using the above form.

    +

    <%- t('bookmarks', 'No bookmarks here.') %>

    +

    <%- t('bookmarks', 'There are no bookmarks available for this query. Try changing your filter or add some using the menu entry on the left.') %>

    diff --git a/js/templates/MobileNav.html b/js/templates/MobileNav.html index f29a89153..9822086f4 100644 --- a/js/templates/MobileNav.html +++ b/js/templates/MobileNav.html @@ -1 +1 @@ - + diff --git a/js/templates/Navigation.html b/js/templates/Navigation.html index ee460a2c0..425264cde 100644 --- a/js/templates/Navigation.html +++ b/js/templates/Navigation.html @@ -1 +1,9 @@ -
  • All bookmarks
  • Favorites
  • Shared
  • +
  • + <%- t('bookmarks', '>All bookmarks') %> +
  • +
  • + <%- t('bookmarks', 'Favorites') %> +
  • +
  • + <%- t('bookmarks', 'Shared') %> +
  • From e51afba4e9d813c551fa933cb745eb0de2aeb75f Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 2 Feb 2018 20:00:52 +0100 Subject: [PATCH 059/110] Fix webpack build --- js/Application.js | 4 ++-- js/Router.js | 2 +- js/models/Bookmarks.js | 1 + js/models/Tags.js | 1 + js/views/AddBookmark.js | 2 +- js/views/App.js | 9 ++++----- js/views/BookmarkCard.js | 10 ++++++---- js/views/BookmarkDetail.js | 8 +++++--- js/views/Bookmarks.js | 9 ++++++--- js/views/BulkActions.js | 9 +++++---- js/views/Content.js | 17 +++++++++-------- js/views/EmptyBookmarks.js | 2 +- js/views/MobileNav.js | 2 +- js/views/Navigation.js | 2 +- js/views/SearchController.js | 2 +- js/views/Settings.js | 2 +- js/views/TagsManagement.js | 2 +- js/views/TagsManagementTag.js | 6 +++--- js/views/TagsNavigation.js | 2 +- js/views/TagsNavigationTag.js | 2 +- templates/main.php | 2 +- webpack.config.js | 2 +- 22 files changed, 54 insertions(+), 44 deletions(-) diff --git a/js/Application.js b/js/Application.js index e1e3b5420..db0f8b526 100644 --- a/js/Application.js +++ b/js/Application.js @@ -4,7 +4,7 @@ import Tags from './models/Tags' import Router from './Router' import AppView from './views/App' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette export default Marionette.Application.extend({ region: '#content' @@ -26,7 +26,7 @@ export default Marionette.Application.extend({ this.router = new Router({app: this}) } , onStart: function() { - this.showView(new AppView({bookmarks: this.bookmarks, tags: this.tags})); + this.showView(new AppView({app: this})); Backbone.history.start(); } , onTagChanged: function(tag) { diff --git a/js/Router.js b/js/Router.js index 295c1bce0..0bfc370ba 100644 --- a/js/Router.js +++ b/js/Router.js @@ -4,7 +4,7 @@ import Tags from './models/Tags' import Router from './Router' import AppView from './views/App' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.AppRouter.extend({ diff --git a/js/models/Bookmarks.js b/js/models/Bookmarks.js index 5751de44c..8bf674483 100644 --- a/js/models/Bookmarks.js +++ b/js/models/Bookmarks.js @@ -1,4 +1,5 @@ import Backbone from 'backbone' +import Bookmark from './Bookmark' export default Backbone.Collection.extend({ model: Bookmark diff --git a/js/models/Tags.js b/js/models/Tags.js index 3d88edc0b..6b4df5af6 100644 --- a/js/models/Tags.js +++ b/js/models/Tags.js @@ -1,4 +1,5 @@ import Backbone from 'backbone' +import Tag from './Tag' var Tags = Backbone.Collection.extend({ model: Tag diff --git a/js/views/AddBookmark.js b/js/views/AddBookmark.js index a373b2b70..af02aa813 100644 --- a/js/views/AddBookmark.js +++ b/js/views/AddBookmark.js @@ -3,7 +3,7 @@ import Backbone from 'backbone' import Bookmark from '../models/Bookmark' import templateString from '../templates/AddBookmark.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ diff --git a/js/views/App.js b/js/views/App.js index c8f65abd8..358dcfa06 100644 --- a/js/views/App.js +++ b/js/views/App.js @@ -8,7 +8,7 @@ import ContentView from './Content' import SettingsView from './Settings' import templateString from '../templates/App.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ @@ -36,8 +36,7 @@ export default Marionette.View.extend({ } } , initialize: function(options) { - this.bookmarks = options.bookmarks - this.tags = options.tags + this.app = options.app this.searchController = new SearchController $(window.document).click(function(e) { @@ -47,8 +46,8 @@ export default Marionette.View.extend({ , onRender: function() { this.showChildView('addBookmarks', new AddBookmarkView()); this.showChildView('navigation', new NavigationView); - this.showChildView('content', new ContentView({bookmarks: this.bookmarks})); - this.showChildView('tags', new TagsManagementView({collection: this.tags})) + this.showChildView('content', new ContentView({app: this.app})); + this.showChildView('tags', new TagsManagementView({collection: this.app.tags})) this.showChildView('settings', new SettingsView()) } }) diff --git a/js/views/BookmarkCard.js b/js/views/BookmarkCard.js index 84b654f85..f86fc2755 100644 --- a/js/views/BookmarkCard.js +++ b/js/views/BookmarkCard.js @@ -3,7 +3,7 @@ import Backbone from 'backbone' import TagsNavigationView from './TagsNavigation' import templateString from '../templates/BookmarkCard.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ @@ -27,7 +27,8 @@ export default Marionette.View.extend({ , 'click .action-select': 'select' , 'click .action-unselect': 'select' }, - initialize: function() { + 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); @@ -35,9 +36,10 @@ export default Marionette.View.extend({ this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions) } , onRender: function() { - this.$el.css('background-image', 'url(bookmark/'+this.model.get('id')+'/image)') + var that = this + this.$el.css('background-image', 'url(bookmark/'+this.model.get('id')+'/image)') var tags = new Tags(this.model.get('tags').map(function(id) { - return app.tags.findWhere({name: id}) + return that.app.tags.findWhere({name: id}) })) this.showChildView('tags', new TagsNavigationView({collection: tags})) this.$('.checkbox').prop('checked', this.$el.hasClass('active')) diff --git a/js/views/BookmarkDetail.js b/js/views/BookmarkDetail.js index 57da0345c..b7a44437c 100644 --- a/js/views/BookmarkDetail.js +++ b/js/views/BookmarkDetail.js @@ -4,7 +4,7 @@ import TagsNavigationView from './TagsNavigation' import templateStringDefault from '../templates/BookmarkDetail_default.html' import templateStringEditing from '../templates/BookmarkDetail_default.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ @@ -36,16 +36,18 @@ export default Marionette.View.extend({ , 'click .cancel': 'cancel' }, initialize: function(opts) { + this.app = opts.app this.listenTo(this.model, "change", this.render); this.listenTo(app.tags, 'sync', this.render) }, onRender: function() { + var that = this if (this.editing) { this.$('.tags input') .val(this.model.get('tags').join(',')) .tagit({ allowSpaces: true, - availableTags: app.tags.pluck('name'), + availableTags: this.app.tags.pluck('name'), placeholderText: t('bookmarks', 'Enter tags'), onTagRemoved: function() {}, onTagFinishRemoved: function() {}, @@ -53,7 +55,7 @@ export default Marionette.View.extend({ }) }else{ var tags = new Tags(this.model.get('tags').map(function(id) { - return app.tags.findWhere({name: id}) + return that.app.tags.findWhere({name: id}) })) this.showChildView('tags', new TagsNavigationView({collection: tags})) } diff --git a/js/views/Bookmarks.js b/js/views/Bookmarks.js index 6d1833429..986456c1f 100644 --- a/js/views/Bookmarks.js +++ b/js/views/Bookmarks.js @@ -2,13 +2,16 @@ import Backbone from 'backbone' import BookmarkCardView from './BookmarkCard' import EmptyBookmarksView from './EmptyBookmarks' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.CollectionView.extend({ className: 'bookmarks' -, regions: { - +, initialize: function(opts) { + this.app = opts.app + } +, childViewOptions: function() { + return {app: this.app} } , childView: function() {return BookmarkCardView} , emptyView: function() {return EmptyBookmarksView} diff --git a/js/views/BulkActions.js b/js/views/BulkActions.js index e3cf20358..9ae904de1 100644 --- a/js/views/BulkActions.js +++ b/js/views/BulkActions.js @@ -2,7 +2,7 @@ import _ from 'underscore' import Backbone from 'backbone' import templateString from '../templates/BulkActions.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ @@ -14,19 +14,20 @@ export default Marionette.View.extend({ , 'click .selection-tools .close': 'abort' } , initialize: function(opts) { - this.all = opts.all + this.app = opts.app + this.all = this.app.bookmarks this.selected = opts.selected this.listenTo(this.selected, 'remove', this.onReduceSelection) this.listenTo(this.selected, 'add', this.onExtendSelection) } , onRender: function() { // hack to ignore events caused by tagit setup -- we should really get something else... - this.rendering = true + this.rendering = true this.$('.tags input') .val(_.intersection.apply(_, this.selected.pluck('tags')).join(',')) .tagit({ allowSpaces: true, - availableTags: app.tags.pluck('name'), + availableTags: this.app.tags.pluck('name'), placeholderText: t('bookmarks', 'Enter tags'), onTagRemoved: this.onTagRemoved.bind(this), onTagAdded: this.onTagAdded.bind(this), diff --git a/js/views/Content.js b/js/views/Content.js index d4b07240f..09fcffc3b 100644 --- a/js/views/Content.js +++ b/js/views/Content.js @@ -7,7 +7,7 @@ import BookmarksView from './Bookmarks' import BookmarkDetailView from './BookmarkDetail' import templateString from '../templates/Content.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ @@ -31,7 +31,8 @@ export default Marionette.View.extend({ } } , initialize: function(options) { - this.bookmarks = options.bookmarks + this.app = options.app + this.bookmarks = this.app.bookmarks this.selected = new Bookmarks this.listenTo(this.bookmarks, 'select', this.onSelect) this.listenTo(this.bookmarks, 'unselect', this.onUnselect) @@ -42,14 +43,14 @@ export default Marionette.View.extend({ } , onRender: function() { this.showChildView('mobileNav', new MobileNavView()) - this.showChildView('bulkActions', new BulkActionsView({all: this.bookmarks, selected: this.selected})) - this.showChildView('viewBookmarks', new BookmarksView({collection: this.bookmarks})); + this.showChildView('bulkActions', new BulkActionsView({selected: this.selected, app: this.app})) + this.showChildView('viewBookmarks', new BookmarksView({collection: this.bookmarks, app: this.app})); } , onSelect: function(model) { if (this.selected.length == 0) { - this.$el.addClass('selection-active') - } - this.selected.add(model) + this.$el.addClass('selection-active') + } + this.selected.add(model) } , onUnselect: function(model) { if (this.selected.length == 1) { @@ -59,7 +60,7 @@ export default Marionette.View.extend({ } , onShowDetails: function(model) { var that = this - var view = new BookmarkDetailView({model: model}) + var view = new BookmarkDetailView({model: model, app: this.app}) this.showChildView('bookmarkDetail', view) } , onEditDetails: function(model) { diff --git a/js/views/EmptyBookmarks.js b/js/views/EmptyBookmarks.js index 8cd4c4e59..73f3c756d 100644 --- a/js/views/EmptyBookmarks.js +++ b/js/views/EmptyBookmarks.js @@ -2,7 +2,7 @@ import _ from 'underscore' import Backbone from 'backbone' import templateString from '../templates/EmptyBookmarks.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ diff --git a/js/views/MobileNav.js b/js/views/MobileNav.js index 5c38bd929..cf0b37273 100644 --- a/js/views/MobileNav.js +++ b/js/views/MobileNav.js @@ -2,7 +2,7 @@ import _ from 'underscore' import Backbone from 'backbone' import templateString from '../templates/MobileNav.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ diff --git a/js/views/Navigation.js b/js/views/Navigation.js index d070d1143..2852f9b82 100644 --- a/js/views/Navigation.js +++ b/js/views/Navigation.js @@ -2,7 +2,7 @@ import _ from 'underscore' import Backbone from 'backbone' import templateString from '../templates/Navigation.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ diff --git a/js/views/SearchController.js b/js/views/SearchController.js index 934054d56..b8d1b8dfa 100644 --- a/js/views/SearchController.js +++ b/js/views/SearchController.js @@ -1,6 +1,6 @@ import Backbone from 'backbone' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ diff --git a/js/views/Settings.js b/js/views/Settings.js index 76e28471d..12aad83de 100644 --- a/js/views/Settings.js +++ b/js/views/Settings.js @@ -2,7 +2,7 @@ import _ from 'underscore' import Backbone from 'backbone' import templateString from '../templates/Settings.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ diff --git a/js/views/TagsManagement.js b/js/views/TagsManagement.js index 0b38f8eef..5aed7cb24 100644 --- a/js/views/TagsManagement.js +++ b/js/views/TagsManagement.js @@ -1,7 +1,7 @@ import Backbone from 'backbone' import TagView from './TagsManagementTag' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.CollectionView.extend({ diff --git a/js/views/TagsManagementTag.js b/js/views/TagsManagementTag.js index 464f7e979..5f03cf310 100644 --- a/js/views/TagsManagementTag.js +++ b/js/views/TagsManagementTag.js @@ -3,7 +3,7 @@ import Backbone from 'backbone' import templateStringDefault from '../templates/TagsManagementTag_default.html' import templateStringEditing from '../templates/TagsManagementTag_editing.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ @@ -15,8 +15,8 @@ export default Marionette.View.extend({ } return this.templateDefault } -, templateDefault: _.template(templateString_default) -, templateEditing: _.template(templateString_editing) +, templateDefault: _.template(templateStringDefault) +, templateEditing: _.template(templateStringEditing) , ui: { 'actionsMenu': '.popovermenu' , 'actionsToggle': '.app-navigation-entry-utils-menu-button' diff --git a/js/views/TagsNavigation.js b/js/views/TagsNavigation.js index 3a39d1c90..d3bfcaf13 100644 --- a/js/views/TagsNavigation.js +++ b/js/views/TagsNavigation.js @@ -1,7 +1,7 @@ import Backbone from 'backbone' import TagView from '../views/TagsNavigationTag' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.CollectionView.extend({ diff --git a/js/views/TagsNavigationTag.js b/js/views/TagsNavigationTag.js index 227c59067..fba7b7893 100644 --- a/js/views/TagsNavigationTag.js +++ b/js/views/TagsNavigationTag.js @@ -2,7 +2,7 @@ import _ from 'underscore' import Backbone from 'backbone' import templateString from '../templates/TagsNavigationTag.html' -const Marionette = Backone.Marionette +const Marionette = Backbone.Marionette const Radio = Backbone.Radio export default Marionette.View.extend({ diff --git a/templates/main.php b/templates/main.php index 9049c06f2..e5785d419 100644 --- a/templates/main.php +++ b/templates/main.php @@ -1,5 +1,5 @@ Date: Fri, 2 Feb 2018 21:30:48 +0100 Subject: [PATCH 060/110] Replace tagit with select2 + Fix use of global app var --- css/3rdparty/jquery.tagit.css | 111 ------ css/bookmarks.css | 37 +- css/select2.css | 484 +++++++++++++++++++++++ js/3rdparty/tag-it.js | 365 ----------------- js/Application.js | 1 + js/index.js | 4 + js/templates/BookmarkDetail_editing.html | 1 - js/templates/BulkActions.html | 1 - js/templates/Navigation.html | 2 +- js/views/BookmarkCard.js | 3 +- js/views/BookmarkDetail.js | 28 +- js/views/BulkActions.js | 56 +-- js/views/TagsManagement.js | 1 + js/views/TagsSelection.js | 64 +++ package.json | 1 + templates/main.php | 5 +- 16 files changed, 613 insertions(+), 551 deletions(-) delete mode 100644 css/3rdparty/jquery.tagit.css create mode 100644 css/select2.css delete mode 100644 js/3rdparty/tag-it.js create mode 100644 js/views/TagsSelection.js diff --git a/css/3rdparty/jquery.tagit.css b/css/3rdparty/jquery.tagit.css deleted file mode 100644 index ae814986c..000000000 --- a/css/3rdparty/jquery.tagit.css +++ /dev/null @@ -1,111 +0,0 @@ -ul.tagit { - padding: 1px 5px; - overflow: auto; - margin-left: inherit; /* usually we don't want the regular ul margins. */ - margin-right: inherit; - width: 91.5%; -} -ul.tagit li { - display: block; - float: left; - margin: 2px 5px 2px 0; -} -ul.tagit li.tagit-choice { - padding: .2em 18px .2em .5em; - position: relative; - line-height: inherit; -} -ul.tagit li.tagit-new { - padding: .25em 4px .25em 0; -} - -ul.tagit li.tagit-choice a.tagit-label { - cursor: pointer; - text-decoration: none; -} -ul.tagit li.tagit-choice .close { - cursor: pointer; - position: absolute; - right: .1em; - top: 50%; - margin-top: -8px; -} - -/* used for some custom themes that don't need image icons */ -ul.tagit li.tagit-choice .close .text-icon { - display: none; -} - -ul.tagit li.tagit-choice input { - display: block; - float: left; - margin: 2px 5px 2px 0; -} -ul.tagit input[type="text"] { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - border: none; - margin: 0; - padding: 0; - width: inherit; - background-color: inherit; - outline: none; -} - - -/***** ZENDESK ***/ - -/* Optional scoped theme for tag-it which mimics the zendesk widget. */ - - -ul.tagit { - border-style: solid; - border-width: 1px; - border-color: #C6C6C6; - background: inherit; -} -ul.tagit li.tagit-choice { - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-border-radius: 6px; - border: 1px solid #CAD8F3; - - background: none; - background-color: #DEE7F8; - - color: #555; - font-weight: normal; -} -ul.tagit li.tagit-choice a.close { - text-decoration: none; -} -ul.tagit li.tagit-choice .close { - right: .4em; -} -ul.tagit li.tagit-choice .ui-icon { - display: none; -} -ul.tagit li.tagit-choice .close .text-icon { - display: inline; - font-family: arial, sans-serif; - font-size: 16px; - line-height: 16px; - color: #777; -} -ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove { - background-color: #bbcef1; - border-color: #6d95e0; -} -ul.tagit li.tagit-choice a.tagLabel:hover, -ul.tagit li.tagit-choice a.close .text-icon:hover { - color: #222; -} -ul.tagit input[type="text"] { - color: #333333; - background: none; -} - - - diff --git a/css/bookmarks.css b/css/bookmarks.css index 08fbf4b4b..ed820b913 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -95,7 +95,7 @@ display: block; } .bulk-actions.active + * { - margin-top: 1.5cm; + margin-top: 2.5cm; } @media only screen and (max-width: 768px) { .bulk-actions { @@ -107,15 +107,17 @@ } } .bulk-actions > button { - vertical-align: text-bottom; + vertical-align: top; } .bulk-actions .tags { display: inline-block; width: 60%; margin: 3px 3px 3px 0; + position: relative; } -.bulk-actions .tagit { - background: white; +.bulk-actions .tags-selection { + display: block; + width: 100%; } .bulk-actions .selection-tools { float: right; @@ -307,18 +309,18 @@ button span, height: 1cm; width: 1cm; } -.bulk-actions ul.tagit li.tagit-choice, -.bookmark-detail ul.tagit li.tagit-choice, +.bookmark-detail .select2-container--default .select2-selection--multiple .select2-selection__choice, +.bulk-actions .select2-container--default .select2-selection--multiple .select2-selection__choice, .bookmark-detail .tag-nav-item { border-radius: .2cm; border: none; background: #ebebeb; display: inline-block; - padding: .1cm .4cm; + padding: .1cm .2cm; margin: .1cm; } -.bulk-actions ul.tagit li.tagit-choice:hover, -.bookmark-detail ul.tagit li.tagit-choice:hover, +.bookmark-detail .select2-container--default .select2-selection--multiple .select2-selection__choice:hover, +.bulk-actions .select2-container--default .select2-selection--multiple .select2-selection__choice:hover, .bookmark-detail .tag-nav-item:hover { background: #e0e0e0; } @@ -336,29 +338,16 @@ button span, .bookmark-detail .input-url, .bookmark-detail .input-title, .bookmark-detail .input-desc, -.bookmarks-detail ul.tagit, -.bulk-actions ul.tagit { +.bookmark-detail .tags-selection { width: 100%; font-size: inherit; box-sizing: border-box; padding: 0 !important; } .bookmark-detail .input-url { - padding-left: 25px; + padding-left: 25px !important; background-position: 5px; } -.bookmatks-detail ul.tagit li.tagit-choice, -.bulk-actions ul.tagit li.tagit-choice { - padding: .1cm .5cm .1cm .2cm; -} -.bulk-actions ul.tagit li.tagit-new, -.bookmarks-detail ul.tagit li.tagit-new { - float: none; - display: inline-block; - padding: 0 !important; - cursor: auto; - margin: 0; -} .bookmark-detail .input-desc { height: 300px; } diff --git a/css/select2.css b/css/select2.css new file mode 100644 index 000000000..ce3afd1d5 --- /dev/null +++ b/css/select2.css @@ -0,0 +1,484 @@ +.select2-container { + box-sizing: border-box; + display: inline-block; + margin: 0; + position: relative; + vertical-align: middle; } + .select2-container .select2-selection--single { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 28px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--single .select2-selection__rendered { + display: block; + padding-left: 8px; + padding-right: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-selection--single .select2-selection__clear { + position: relative; } + .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 8px; + padding-left: 20px; } + .select2-container .select2-selection--multiple { + box-sizing: border-box; + cursor: pointer; + display: block; + min-height: 32px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--multiple .select2-selection__rendered { + display: inline-block; + overflow: hidden; + padding-left: 8px; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-search--inline { + float: left; } + .select2-container .select2-search--inline .select2-search__field { + box-sizing: border-box; + border: none; + font-size: 100%; + margin-top: 5px; + padding: 0; } + .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + +.select2-dropdown { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + box-sizing: border-box; + display: block; + position: absolute; + left: -100000px; + width: 100%; + z-index: 1051; } + +.select2-results { + display: block; } + +.select2-results__options { + list-style: none; + margin: 0; + padding: 0; } + +.select2-results__option { + padding: 6px; + user-select: none; + -webkit-user-select: none; } + .select2-results__option[aria-selected] { + cursor: pointer; } + +.select2-container--open .select2-dropdown { + left: 0; } + +.select2-container--open .select2-dropdown--above { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--open .select2-dropdown--below { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-search--dropdown { + display: block; + padding: 4px; } + .select2-search--dropdown .select2-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; } + .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + .select2-search--dropdown.select2-search--hide { + display: none; } + +.select2-close-mask { + border: 0; + margin: 0; + padding: 0; + display: block; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 99; + background-color: #fff; + filter: alpha(opacity=0); } + +.select2-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; } + +.select2-container--default .select2-selection--single { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; } + .select2-container--default .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--default .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; } + .select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; } + .select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; } + +.select2-container--default.select2-container--disabled .select2-selection--single { + background-color: #eee; + cursor: default; } + .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; } + +.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--default .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 5px; + width: 100%; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered li { + list-style: none; } + .select2-container--default .select2-selection--multiple .select2-selection__placeholder { + color: #999; + margin-top: 5px; + float: left; } + .select2-container--default .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-top: 5px; + margin-right: 10px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + color: #999; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #333; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border: solid black 1px; + outline: 0; } + +.select2-container--default.select2-container--disabled .select2-selection--multiple { + background-color: #eee; + cursor: default; } + +.select2-container--default.select2-container--disabled .select2-selection__choice__remove { + display: none; } + +.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; } + +.select2-container--default .select2-search--inline .select2-search__field { + background: transparent; + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; } + +.select2-container--default .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--default .select2-results__option[role=group] { + padding: 0; } + +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; } + +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; } + +.select2-container--default .select2-results__option .select2-results__option { + padding-left: 1em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; } + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5897fb; + color: white; } + +.select2-container--default .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic .select2-selection--single { + background-color: #f7f7f7; + border: 1px solid #aaa; + border-radius: 4px; + outline: 0; + background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + .select2-container--classic .select2-selection--single:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--classic .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-right: 10px; } + .select2-container--classic .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--classic .select2-selection--single .select2-selection__arrow { + background-color: #ddd; + border: none; + border-left: 1px solid #aaa; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); } + .select2-container--classic .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { + border: none; + border-right: 1px solid #aaa; + border-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + left: 1px; + right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--single { + border: 1px solid #5897fb; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { + background: transparent; + border: none; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); } + +.select2-container--classic .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; + outline: 0; } + .select2-container--classic .select2-selection--multiple:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--multiple .select2-selection__rendered { + list-style: none; + margin: 0; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__clear { + display: none; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { + color: #888; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #555; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + float: right; + margin-left: 5px; + margin-right: auto; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--multiple { + border: 1px solid #5897fb; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--classic .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; + outline: 0; } + +.select2-container--classic .select2-search--inline .select2-search__field { + outline: 0; + box-shadow: none; } + +.select2-container--classic .select2-dropdown { + background-color: white; + border: 1px solid transparent; } + +.select2-container--classic .select2-dropdown--above { + border-bottom: none; } + +.select2-container--classic .select2-dropdown--below { + border-top: none; } + +.select2-container--classic .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--classic .select2-results__option[role=group] { + padding: 0; } + +.select2-container--classic .select2-results__option[aria-disabled=true] { + color: grey; } + +.select2-container--classic .select2-results__option--highlighted[aria-selected] { + background-color: #3875d7; + color: white; } + +.select2-container--classic .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic.select2-container--open .select2-dropdown { + border-color: #5897fb; } diff --git a/js/3rdparty/tag-it.js b/js/3rdparty/tag-it.js deleted file mode 100644 index f16d021f6..000000000 --- a/js/3rdparty/tag-it.js +++ /dev/null @@ -1,365 +0,0 @@ -/* - * jQuery UI Tag-it! - * - * @version v2.0 (06/2011) - * - * Copyright 2011, Levy Carneiro Jr. - * Released under the MIT license. - * http://aehlke.github.com/tag-it/LICENSE - * - * Homepage: - * http://aehlke.github.com/tag-it/ - * - * Authors: - * Levy Carneiro Jr. - * Martin Rehfeld - * Tobias Schmidt - * Skylar Challand - * Alex Ehlke - * - * Maintainer: - * Alex Ehlke - Twitter: @aehlke - * - * Dependencies: - * jQuery v1.4+ - * jQuery UI v1.8+ - */ -(function ($) { - - $.widget('ui.tagit', { - options: { - itemName: 'item', - fieldName: 'tags', - availableTags: [], - tagSource: null, - removeConfirmation: false, - caseSensitive: true, - placeholderText: null, - // When enabled, quotes are not neccesary - // for inputting multi-word tags. - allowSpaces: false, - // The below options are for using a single field instead of several - // for our form values. - // - // When enabled, will use a single hidden field for the form, - // rather than one per tag. It will delimit tags in the field - // with singleFieldDelimiter. - // - // The easiest way to use singleField is to just instantiate tag-it - // on an INPUT element, in which case singleField is automatically - // set to true, and singleFieldNode is set to that element. This - // way, you don't need to fiddle with these options. - singleField: false, - singleFieldDelimiter: ',', - // Set this to an input DOM node to use an existing form field. - // Any text in it will be erased on init. But it will be - // populated with the text of tags as they are created, - // delimited by singleFieldDelimiter. - // - // If this is not set, we create an input node for it, - // with the name given in settings.fieldName, - // ignoring settings.itemName. - singleFieldNode: null, - // Optionally set a tabindex attribute on the input that gets - // created for tag-it. - tabIndex: null, - // Event callbacks. - onTagAdded: null, - onTagRemoved: null, - onTagClicked: null - }, - _create: function () { - // for handling static scoping inside callbacks - var that = this; - - // There are 2 kinds of DOM nodes this widget can be instantiated on: - // 1. UL, OL, or some element containing either of these. - // 2. INPUT, in which case 'singleField' is overridden to true, - // a UL is created and the INPUT is hidden. - if (this.element.is('input')) { - this.tagList = $('
      ').insertAfter(this.element); - this.options.singleField = true; - this.options.singleFieldNode = this.element; - this.element.css('display', 'none'); - } else { - this.tagList = this.element.find('ul, ol').andSelf().last(); - } - - this._tagInput = $('').addClass('ui-widget-content'); - if (this.options.tabIndex) { - this._tagInput.attr('tabindex', this.options.tabIndex); - } - if (this.options.placeholderText) { - this._tagInput.attr('placeholder', this.options.placeholderText); - } - this.options.tagSource = this.options.tagSource || function (search, showChoices) { - var filter = search.term.toLowerCase(); - var choices = $.grep(that.options.availableTags, function (element) { - // Only match autocomplete options that begin with the search term. - // (Case insensitive.) - return (element.toLowerCase().indexOf(filter) === 0); - }); - showChoices(that._subtractArray(choices, that.assignedTags())); - }; - - this.tagList - .addClass('tagit') - .addClass('ui-widget ui-widget-content ui-corner-all') - // Create the input field. - .append($('
    • ').append(this._tagInput)) - .click(function (e) { - var target = $(e.target); - if (target.hasClass('tagit-label')) { - that._trigger('onTagClicked', e, target.closest('.tagit-choice')); - } else { - // Sets the focus() to the input field, if the user - // clicks anywhere inside the UL. This is needed - // because the input field needs to be of a small size. - that._tagInput.focus(); - } - }); - - // Add existing tags from the list, if any. - this.tagList.children('li').each(function () { - if (!$(this).hasClass('tagit-new')) { - that.createTag($(this).html(), $(this).attr('class')); - $(this).remove(); - } - }); - - // Single field support. - if (this.options.singleField) { - if (this.options.singleFieldNode) { - // Add existing tags from the input field. - var node = $(this.options.singleFieldNode); - var tags = node.val().split(this.options.singleFieldDelimiter); - node.val(''); - $.each(tags, function (index, tag) { - that.createTag(tag); - }); - } else { - // Create our single field input after our list. - this.options.singleFieldNode = this.tagList.after(''); - } - } - - // Events. - this._tagInput - .keydown(function (event) { - // Backspace is not detected within a keypress, so it must use keydown. - if (event.which == $.ui.keyCode.BACKSPACE && that._tagInput.val() === '') { - var tag = that._lastTag(); - if (!that.options.removeConfirmation || tag.hasClass('remove')) { - // When backspace is pressed, the last tag is deleted. - that.removeTag(tag); - } else if (that.options.removeConfirmation) { - tag.addClass('remove ui-state-highlight'); - } - } else if (that.options.removeConfirmation) { - that._lastTag().removeClass('remove ui-state-highlight'); - } - - // Comma/Space/Enter are all valid delimiters for new tags, - // except when there is an open quote or if setting allowSpaces = true. - // Tab will also create a tag, unless the tag input is empty, in which case it isn't caught. - if ( - event.which == $.ui.keyCode.COMMA || - event.which == $.ui.keyCode.ENTER || - ( - event.which == $.ui.keyCode.TAB && - that._tagInput.val() !== '' - ) || - ( - event.which == $.ui.keyCode.SPACE && - that.options.allowSpaces !== true && - ( - $.trim(that._tagInput.val()).replace(/^s*/, '').charAt(0) != '"' || - ( - $.trim(that._tagInput.val()).charAt(0) == '"' && - $.trim(that._tagInput.val()).charAt($.trim(that._tagInput.val()).length - 1) == '"' && - $.trim(that._tagInput.val()).length - 1 !== 0 - ) - ) - ) - ) { - event.preventDefault(); - that.createTag(that._cleanedInput()); - - // The autocomplete doesn't close automatically when TAB is pressed. - // So let's ensure that it closes. - that._tagInput.autocomplete('close'); - } - }).blur(function (e) { - //If autocomplete is enabled and suggestion was clicked, don't add it - if (that.options.tagSource && that._tagInput.data('autocomplete-open')) { - that._cleanedInput(); - } else { - that.createTag(that._cleanedInput()); - } - }); - - - // Autocomplete. - if (this.options.availableTags || this.options.tagSource) { - this._tagInput.autocomplete({ - source: this.options.tagSource, - open: function () { - that._tagInput.data('autocomplete-open', true) - }, - close: function () { - that._tagInput.data('autocomplete-open', false) - }, - select: function (event, ui) { - that.createTag(ui.item.value); - // Preventing the tag input to be updated with the chosen value. - return false; - } - }); - } - }, - _cleanedInput: function () { - // Returns the contents of the tag input, cleaned and ready to be passed to createTag - return $.trim(this._tagInput.val().replace(/^"(.*)"$/, '$1')); - }, - _lastTag: function () { - return this.tagList.children('.tagit-choice:last'); - }, - assignedTags: function () { - // Returns an array of tag string values - var that = this; - var tags = []; - if (this.options.singleField) { - tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); - if (tags[0] === '') { - tags = []; - } - } else { - this.tagList.children('.tagit-choice').each(function () { - tags.push(that.tagLabel(this)); - }); - } - return tags; - }, - _updateSingleTagsField: function (tags) { - // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter - $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)); - }, - _subtractArray: function (a1, a2) { - var result = []; - for (var i = 0; i < a1.length; i++) { - if ($.inArray(a1[i], a2) == -1) { - result.push(a1[i]); - } - } - return result; - }, - tagLabel: function (tag) { - // Returns the tag's string label. - if (this.options.singleField) { - return $(tag).children('.tagit-label').text(); - } else { - return $(tag).children('input').val(); - } - }, - _isNew: function (value) { - var that = this; - var isNew = true; - this.tagList.children('.tagit-choice').each(function (i) { - if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) { - isNew = false; - return; - } - }); - return isNew; - }, - _formatStr: function (str) { - if (this.options.caseSensitive) { - return str; - } - return $.trim(str.toLowerCase()); - }, - createTag: function (value, additionalClass) { - that = this; - // Automatically trims the value of leading and trailing whitespace. - value = $.trim(value); - - if (!this._isNew(value) || value === '') { - return false; - } - - var label = $(this.options.onTagClicked ? '' : '').text(value); - - // Create tag. - var tag = $('
    • ') - .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') - .addClass(additionalClass) - .append(label); - - // Button for removing the tag. - var removeTagIcon = $('') - .addClass('ui-icon ui-icon-close'); - var removeTag = $('\xd7') // \xd7 is an X - .addClass('close') - .append(removeTagIcon) - .click(function (e) { - // Removes a tag when the little 'x' is clicked. - that.removeTag(tag); - }); - tag.append(removeTag); - - // Unless options.singleField is set, each tag has a hidden input field inline. - if (this.options.singleField) { - var tags = this.assignedTags(); - tags.push(value); - this._updateSingleTagsField(tags); - } else { - var escapedValue = label.html(); - tag.append(''); - } - - this._trigger('onTagAdded', null, tag); - - // Cleaning the input. - this._tagInput.val(''); - - // insert tag - this._tagInput.parent().after(tag); - }, - removeTag: function (tag, animate) { - if (typeof animate === 'undefined') { - animate = true; - } - - tag = $(tag); - - this._trigger('onTagRemoved', null, tag); - - if (this.options.singleField) { - var tags = this.assignedTags(); - var removedTagLabel = this.tagLabel(tag); - tags = $.grep(tags, function (el) { - return el != removedTagLabel; - }); - this._updateSingleTagsField(tags); - } - // Animate the removal. - if (animate) { - tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function () { - tag.remove(); - }).dequeue(); - } else { - tag.remove(); - } - this._trigger('onTagFinishRemoved', null, tag); - }, - removeAll: function () { - // Removes all tags. Takes an optional `animate` argument. - var that = this; - this.tagList.children('.tagit-choice').each(function (index, tag) { - that.removeTag(tag, false); - }); - } - - }); - -})(jQuery); diff --git a/js/Application.js b/js/Application.js index db0f8b526..07375bb99 100644 --- a/js/Application.js +++ b/js/Application.js @@ -1,5 +1,6 @@ import Backbone from 'backbone' import Bookmarks from './models/Bookmarks' +import Tag from './models/Tag' import Tags from './models/Tags' import Router from './Router' import AppView from './views/App' diff --git a/js/index.js b/js/index.js index bfbf494e6..8e3e5d907 100644 --- a/js/index.js +++ b/js/index.js @@ -1,6 +1,7 @@ import _ from 'underscore' import Backbone from 'backbone' import Marionette from 'backbone.marionette' +import select2 from 'select2' import Tag from './models/Tag' import Tags from './models/Tags' import App from './Application' @@ -19,6 +20,9 @@ Backbone.sync = function(method, model, options) { , error: function() { console.log(arguments) } + , headers: { + 'requesttoken': oc_requesttoken + } } if (method === 'update' && model instanceof Tag) { overrideOptions.url = model.urlRoot+'/'+model.previous('name') diff --git a/js/templates/BookmarkDetail_editing.html b/js/templates/BookmarkDetail_editing.html index 760fcb60d..db15e17f8 100644 --- a/js/templates/BookmarkDetail_editing.html +++ b/js/templates/BookmarkDetail_editing.html @@ -2,7 +2,6 @@

      -
      diff --git a/js/templates/BulkActions.html b/js/templates/BulkActions.html index f1864ff92..f3b8ba40b 100644 --- a/js/templates/BulkActions.html +++ b/js/templates/BulkActions.html @@ -7,5 +7,4 @@ <%- t('bookmarks', 'Delete') %>
      -
      diff --git a/js/templates/Navigation.html b/js/templates/Navigation.html index 425264cde..f4cae13b7 100644 --- a/js/templates/Navigation.html +++ b/js/templates/Navigation.html @@ -1,5 +1,5 @@
    • - <%- t('bookmarks', '>All bookmarks') %> + <%- t('bookmarks', 'All bookmarks') %>
    • <%- t('bookmarks', 'Favorites') %> diff --git a/js/views/BookmarkCard.js b/js/views/BookmarkCard.js index f86fc2755..893f3887c 100644 --- a/js/views/BookmarkCard.js +++ b/js/views/BookmarkCard.js @@ -1,5 +1,6 @@ import _ from 'underscore' import Backbone from 'backbone' +import Tags from '../models/Tags' import TagsNavigationView from './TagsNavigation' import templateString from '../templates/BookmarkCard.html' @@ -32,7 +33,7 @@ export default Marionette.View.extend({ this.listenTo(this.model, "change", this.render); this.listenTo(this.model, "select", this.onSelect); this.listenTo(this.model, "unselect", this.onUnselect); - this.listenTo(app.tags, 'sync', this.render) + this.listenTo(this.app.tags, 'sync', this.render) this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions) } , onRender: function() { diff --git a/js/views/BookmarkDetail.js b/js/views/BookmarkDetail.js index b7a44437c..32897fb22 100644 --- a/js/views/BookmarkDetail.js +++ b/js/views/BookmarkDetail.js @@ -1,8 +1,10 @@ import _ from 'underscore' import Backbone from 'backbone' +import Tags from '../models/Tags' import TagsNavigationView from './TagsNavigation' +import TagsSelectionView from './TagsSelection' import templateStringDefault from '../templates/BookmarkDetail_default.html' -import templateStringEditing from '../templates/BookmarkDetail_default.html' +import templateStringEditing from '../templates/BookmarkDetail_editing.html' const Marionette = Backbone.Marionette const Radio = Backbone.Radio @@ -20,7 +22,6 @@ export default Marionette.View.extend({ regions: { 'tags': { el: '.tags' - , replaceElement: true } }, ui: { @@ -38,26 +39,17 @@ export default Marionette.View.extend({ initialize: function(opts) { this.app = opts.app this.listenTo(this.model, "change", this.render); - this.listenTo(app.tags, 'sync', this.render) + this.listenTo(this.app.tags, 'sync', this.render) }, onRender: function() { var that = this + this.tags = new Tags(this.model.get('tags').map(function(id) { + return that.app.tags.findWhere({name: id}) + })) if (this.editing) { - this.$('.tags input') - .val(this.model.get('tags').join(',')) - .tagit({ - allowSpaces: true, - availableTags: this.app.tags.pluck('name'), - placeholderText: t('bookmarks', 'Enter tags'), - onTagRemoved: function() {}, - onTagFinishRemoved: function() {}, - onTagClicked: function(){} - }) + this.showChildView('tags', new TagsSelectionView({collection: this.app.tags, selected: this.tags, app: this.app })) }else{ - 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.showChildView('tags', new TagsNavigationView({collection: this.tags})) } }, close: function() { @@ -81,7 +73,7 @@ export default Marionette.View.extend({ this.model.set({ 'title': this.$('.input-title').val() , 'url': this.$('.input-url').val() - , 'tags': this.$('.tags input').tagit("assignedTags") + , 'tags': this.tags.pluck('name') , 'description': this.$('.input-desc').val() }) this.model.save({wait: true}) diff --git a/js/views/BulkActions.js b/js/views/BulkActions.js index 9ae904de1..d9f380b54 100644 --- a/js/views/BulkActions.js +++ b/js/views/BulkActions.js @@ -1,5 +1,7 @@ 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 @@ -8,7 +10,12 @@ const Radio = Backbone.Radio export default Marionette.View.extend({ className: 'bulk-actions' , template: _.template(templateString) -, events: { +, regions: { + 'tags': { + el: '.tags' + } + }, + events: { 'click .delete': 'delete' , 'click .select-all': 'selectAll' , 'click .selection-tools .close': 'abort' @@ -17,36 +24,37 @@ export default Marionette.View.extend({ 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() { - // hack to ignore events caused by tagit setup -- we should really get something else... - this.rendering = true - this.$('.tags input') - .val(_.intersection.apply(_, this.selected.pluck('tags')).join(',')) - .tagit({ - allowSpaces: true, - availableTags: this.app.tags.pluck('name'), - placeholderText: t('bookmarks', 'Enter tags'), - onTagRemoved: this.onTagRemoved.bind(this), - onTagAdded: this.onTagAdded.bind(this), - onTagFinishRemoved: function() {}, - onTagClicked: function(){} - }) - this.rendering = false + 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.render() + this.updateTags() } , onExtendSelection: function() { if (this.selected.length == 1) { this.$el.addClass('active') } - this.render() + this.updateTags() } , delete: function() { var that = this @@ -59,21 +67,19 @@ export default Marionette.View.extend({ }) }) } -, onTagAdded: function(e, el) { - if (this.rendering) return - var tagName = $('.tagit-label', el).text() +, onTagAdded: function(tag) { + if (this.triggeredByAlgo) return this.selected.forEach(function(model) { var tags = model.get('tags') - model.set('tags', _.union(tags, [tagName])) + model.set('tags', _.union(tags, [tag.get('name')])) model.save() }) } -, onTagRemoved: function(e, el) { - if (this.rendering) return - var tagName = $('.tagit-label', el).text() +, onTagRemoved: function(tag) { + if (this.triggeredByAlgo) return this.selected.forEach(function(model) { var tags = model.get('tags') - model.set('tags', _.without(tags, tagName)) + model.set('tags', _.without(tags, tag.get('name'))) model.save() }) } diff --git a/js/views/TagsManagement.js b/js/views/TagsManagement.js index 5aed7cb24..8a2ec216c 100644 --- a/js/views/TagsManagement.js +++ b/js/views/TagsManagement.js @@ -1,4 +1,5 @@ import Backbone from 'backbone' +import Tags from '../models/Tags' import TagView from './TagsManagementTag' const Marionette = Backbone.Marionette diff --git a/js/views/TagsSelection.js b/js/views/TagsSelection.js new file mode 100644 index 000000000..f6fa0dbc6 --- /dev/null +++ b/js/views/TagsSelection.js @@ -0,0 +1,64 @@ +import _ from 'underscore' +import Backbone from 'backbone' + +const Marionette = Backbone.Marionette +const Radio = Backbone.Radio + +export default Marionette.View.extend({ + tagName: 'select' +, template: _.template('') +, className: 'tags-selection' +, events: { + 'select2:select': 'onAdd' + , 'select2:unselect': 'onRemove' + } +, 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) + this.listenTo(this.collection, 'add', this.onAttach) + } +, onAttach: function() { + if (this.$el.hasClass("select2-hidden-accessible")) { + this.$el.select2('destroy') + } + this.$el.select2({ + placeholder: t('bookmarks', 'Set tags') + , allowClear: true + , 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) { + if (this.triggeredByAlgo) { + return this.triggeredByAlgo = false + } + this.selected.add(this.app.tags.get(e.params.data.id)) + } +, onRemoveByUser: function(e) { + if (this.triggeredByAlgo) { + return this.triggeredByAlgo = false + } + this.selected.remove(this.app.tags.get(e.params.data.id)) + } +, onChangeByAlgo: function(e) { + this.$el + .val(this.selected.pluck('name')) + .trigger('change') + this.triggeredByAlgo = true + } +}) diff --git a/package.json b/package.json index 0a61b44c4..273fa061d 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "backbone": "^1.3.3", "backbone.marionette": "^3.5.1", "jquery": "^3.3.1", + "select2": "^4.0.6-rc.1", "underscore": "^1.8.3" } } diff --git a/templates/main.php b/templates/main.php index e5785d419..e717f3f60 100644 --- a/templates/main.php +++ b/templates/main.php @@ -2,10 +2,7 @@ script('bookmarks', 'dist/bundle'); style('bookmarks', 'bookmarks'); - -script('bookmarks', '3rdparty/tag-it'); -style('bookmarks', '3rdparty/jquery.tagit'); - +style('bookmarks', 'select2'); /** * Copyright (c) 2011 Marvin Thomas Rabe * Copyright (c) 2011 Arthur Schiwon From 0bdab7bb93f85f88c3c1274994d5c3dd0f51912d Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 2 Feb 2018 23:07:32 +0100 Subject: [PATCH 061/110] Disable 'Shared' & 'Favorites' views for now --- css/bookmarks.css | 3 --- js/templates/Navigation.html | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/css/bookmarks.css b/css/bookmarks.css index ed820b913..142d5a090 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -39,9 +39,6 @@ } } -.add-bookmark { - border-bottom: 1px solid #ebebeb; -} .add-bookmark input { margin: 0; width: 100%; diff --git a/js/templates/Navigation.html b/js/templates/Navigation.html index f4cae13b7..fee537b79 100644 --- a/js/templates/Navigation.html +++ b/js/templates/Navigation.html @@ -1,9 +1,11 @@
    • <%- t('bookmarks', 'All bookmarks') %>
    • + From e88ca12ab5203df26c13142dcae3f9fdf322cb35 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 2 Feb 2018 23:39:52 +0100 Subject: [PATCH 062/110] Fix tags-selection --- js/views/TagsSelection.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/js/views/TagsSelection.js b/js/views/TagsSelection.js index f6fa0dbc6..19213a9d1 100644 --- a/js/views/TagsSelection.js +++ b/js/views/TagsSelection.js @@ -1,5 +1,7 @@ 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 @@ -9,8 +11,8 @@ export default Marionette.View.extend({ , template: _.template('') , className: 'tags-selection' , events: { - 'select2:select': 'onAdd' - , 'select2:unselect': 'onRemove' + 'select2:select': 'onAddByUser' + , 'select2:unselect': 'onRemoveByUser' } , initialize: function(options) { this.app = options.app @@ -44,21 +46,16 @@ export default Marionette.View.extend({ this.$el.select2('destroy') } , onAddByUser: function(e) { - if (this.triggeredByAlgo) { - return this.triggeredByAlgo = false - } - this.selected.add(this.app.tags.get(e.params.data.id)) + var tag = this.app.tags.get(e.params.data.text) || new Tag({name: e.params.data.text}) + this.selected.add(tag) } , onRemoveByUser: function(e) { - if (this.triggeredByAlgo) { - return this.triggeredByAlgo = false - } - this.selected.remove(this.app.tags.get(e.params.data.id)) + var tag = this.app.tags.get(e.params.data.text) || new Tag({name: e.params.data.text}) + this.selected.remove(tag) } , onChangeByAlgo: function(e) { this.$el .val(this.selected.pluck('name')) .trigger('change') - this.triggeredByAlgo = true } }) From d42c1810819da83f6d905623613dbe5dce334f7f Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 2 Feb 2018 23:42:08 +0100 Subject: [PATCH 063/110] Allow scrolling through sidebar tags --- css/bookmarks.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/css/bookmarks.css b/css/bookmarks.css index 142d5a090..87f6c879f 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -7,6 +7,7 @@ } #app-navigation > ul { height: auto; + overflow: hidden; } .navigation a > span { display: inline-block; @@ -352,7 +353,7 @@ button span, .tags-management { float: left; width: 250px; - overflow: visible !important; + overflow: auto !important; } @media only screen and (max-width: 768px) { .tags-management { From e95a12039f80e5d5d612767106ac46f260bb3600 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 8 Feb 2018 19:50:53 +0100 Subject: [PATCH 064/110] Pretty-print templates/Settings.html --- js/templates/Settings.html | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/js/templates/Settings.html b/js/templates/Settings.html index 0a45a70c9..6efd35500 100644 --- a/js/templates/Settings.html +++ b/js/templates/Settings.html @@ -1 +1,13 @@ -
      +
      + +
      +
      +
      + + + +
      + + +
      +
      From de38d43f6345a63c682dabb78cbed4fce975ee13 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 8 Feb 2018 20:16:41 +0100 Subject: [PATCH 065/110] Prettify css --- css/bookmarks.css | 428 ---------------------------------------- css/select2.css | 483 ---------------------------------------------- 2 files changed, 911 deletions(-) diff --git a/css/bookmarks.css b/css/bookmarks.css index 87f6c879f..8b1378917 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -1,429 +1 @@ -#content > div { - height: 100%; -} -#app-navigation { - height: 100%; - position: relative; -} -#app-navigation > ul { - height: auto; - overflow: hidden; -} -.navigation a > span { - display: inline-block; - vertical-align: text-bottom; - margin-right: 12px; -} -#app-navigation h3 { - padding-top: .7cm; - padding-left: 12px; - border-top: 1px solid #ededed; -} -#app-content { - transition: transform .5s ease; - transform: none; -} -.mobile-nav { - padding: .3cm; - display: none; -} -.mobile-nav .toggle-menu { - display: inline-block; -} -@media only screen and (max-width: 768px) { - .mobile-nav-open #app-content { - transform: translate(250px, 0); - } - .mobile-nav { - display: block; - } -} - -.add-bookmark input { - margin: 0; - width: 100%; - padding: 12px 40px 12px 12px; - box-sizing: border-box; - background-color: transparent; - border: none; - border-bottom: 1px solid #eee; - border-radius: 0; -} -.add-bookmark button { - position: absolute; - right: 0; - top: 0; - min-height: 34px; - padding: 10px 20px; - background-color: transparent; - border: none; - opacity: .5; - font-weight: bold; -} -.add-bookmark li.link a span { - margin-left: 28px; -} -.add-bookmark li.link a span::before { - position: absolute; - left: 12px; - content: '+'; - font-weight: bold; - font-size: 28px; -} -.add-bookmark .form { - display: none; -} - -.bulk-actions { - display: none; - position: fixed; - top: 45px; - left: 250px; - right: 0; - border-bottom: 1px solid #ebebeb; - padding: .3cm .5cm; - background: #e9f7ff; - z-index: 3; -} -.bulk-actions + * { - transition: margin-top .5s ease; -} -.bulk-actions.active { - display: block; -} -.bulk-actions.active + * { - margin-top: 2.5cm; -} -@media only screen and (max-width: 768px) { - .bulk-actions { - position: relative; - top: 0; right: 0; bottom: 0; left: 0; - } - .bulk-actions.active + * { - margin-top: 0; - } -} -.bulk-actions > button { - vertical-align: top; -} -.bulk-actions .tags { - display: inline-block; - width: 60%; - margin: 3px 3px 3px 0; - position: relative; -} -.bulk-actions .tags-selection { - display: block; - width: 100%; -} -.bulk-actions .selection-tools { - float: right; -} -.bulk-actions .selection-tools .close { - display: inline-block; - cursor: pointer; - margin: 3px 3px 3px 0; - padding: 6px 12px; - width: auto; - min-height: 34px; - cursor: pointer; - box-sizing: border-box; -} -.bulk-actions .selection-tools .close span { - display: inline-block; - vertical-align: text-top; -} -button span, -.bulk-actions .selection-tools .close span { - cursor: pointer; -} - -.bookmarks-empty { - width: 5cm; - margin: 20% auto; -} - -.bookmarks { - display: flex; - flex-wrap: wrap; - overflow: hidden; - padding-right: .6cm; -} -.bookmark-card { - position: relative; - box-sizing: border-box; /*necessary for hover effect goodness to work*/ - flex-grow: 1; - width: 6cm; - height: 6cm; - margin: .6cm 0 0 .6cm; - padding: .5cm; - border: 1px solid #ddd; - overflow: hidden; - background-color: rgb(210, 210, 210); - background-size: cover; - background-position: center; - transition: background-color .3s ease; - background-blend-mode: overlay; -} -.bookmark-card, -.bookmark-card * { - cursor: pointer; -} -.bookmark-card:hover { - background-color: rgb(180, 180, 180); -} -.bookmark-card.active, -.bookmark-card.active:hover { - transform: none; - border: 1px solid #0082c9; - box-shadow: inset 0 0 10px #0082c9; -} -.bookmark-card .panel { - position: absolute; - top: 0; left: 0; right: 0; - padding: .5cm .5cm .1cm .6cm; - background: rgba(255, 255, 255, .8); - box-shadow: #aaa 0 0 5px; -} -.bookmark-card:hover .panel { - background: rgba(255, 255, 255, .90); -} -.bookmark-card .selectbox { - display: none; - position: absolute; - left: .1cm; - top: .5cm; -} -.selection-active .bookmark-card .selectbox, -.bookmark-card:hover .selectbox, -.bookmark-card.active .selectbox { - display: inline-block; -} -.bookmark-card h1 { - font-size: .5cm; - padding-right: .5cm; -} -.bookmark-card h2 { - margin-top: .1cm; - font-size: .4cm; - opacity: .4; -} -.bookmark-card h2 :link span { - display: inline-block; - vertical-align: text-bottom; - margin-right: 3px; -} -.bookmark-card h2 :link:hover { - text-decoration: underline; -} -.bookmark-card .actions { - display: none; - position: absolute; - top: .5cm; - right: .5cm; -} -.bookmark-card:hover .actions, -.bookmark-card.actions-open .actions { - display: block; -} -.bookmark-card .actions .popovermenu { - top: 24px; - right: -14px; -} -.bookmark-card .actions .action-select, -.bookmark-card.active .actions .action-unselect { - display: block; -} -.bookmark-card .actions .action-unselect, -.bookmark-card.active .actions .action-select { - display: none; -} -.bookmark-card .tags { - max-height: 2.5cm; - overflow: hidden; - position: absolute; - bottom: .5cm; -} -.bookmark-card .tag-nav-item { - border-radius: .2cm; - background: #ebebeb; - display: inline-block; - padding: 0 .2cm; - margin: .1cm; -} -.bookmark-card .tag-nav-item:hover { - background: #e0e0e0; -} - -.bookmark-detail { - position: fixed; - top: 45px; - right: 0; - z-index: 2; - width: 500px; - height: 100%; - box-sizing: border-box; - padding: 1cm; - background: white; - border-left: 1px solid #ebebeb; -} -@media only screen and (max-width: 768px) { - .bookmark-detail { - width: auto; - left: 0; - } -} -.bookmark-detail h1 { - font-size: .8cm; - line-height: 1; - padding-right: 2.3cm; -} -.bookmark-detail h2 { - margin-top: .3cm; - opacity: .4; -} -.bookmark-detail h2 :link:hover { - text-decoration: underline; -} -.bookmark-detail h2 :link span { - display: inline-block; - vertical-align: middle; - margin-right: 3px; -} -.bookmark-detail > .close { - position: absolute; - top: .1cm; - right: .1cm; - width: 1cm; - height: 1cm; - cursor: pointer; -} -.bookmark-detail .actions { - position: absolute; - right: 1cm; -} -.bookmark-detail .actions button { - height: 1cm; - width: 1cm; -} -.bookmark-detail .select2-container--default .select2-selection--multiple .select2-selection__choice, -.bulk-actions .select2-container--default .select2-selection--multiple .select2-selection__choice, -.bookmark-detail .tag-nav-item { - border-radius: .2cm; - border: none; - background: #ebebeb; - display: inline-block; - padding: .1cm .2cm; - margin: .1cm; -} -.bookmark-detail .select2-container--default .select2-selection--multiple .select2-selection__choice:hover, -.bulk-actions .select2-container--default .select2-selection--multiple .select2-selection__choice:hover, -.bookmark-detail .tag-nav-item:hover { - background: #e0e0e0; -} -.bookmark-detail .description { - margin-top: .5cm; -} - -.bookmark-detail.editing h1 { - padding-right: 0; -} -.bookmark-detail.editing .actions button { - height: auto; - width: auto; -} -.bookmark-detail .input-url, -.bookmark-detail .input-title, -.bookmark-detail .input-desc, -.bookmark-detail .tags-selection { - width: 100%; - font-size: inherit; - box-sizing: border-box; - padding: 0 !important; -} -.bookmark-detail .input-url { - padding-left: 25px !important; - background-position: 5px; -} -.bookmark-detail .input-desc { - height: 300px; -} - -.tags-management { - float: left; - width: 250px; - overflow: auto !important; -} -@media only screen and (max-width: 768px) { - .tags-management { - width: auto; - float: none; - border-bottom: 2px solid #bbb; - } -} -.tags-management > ul:first-child > li:last-child { - border-bottom: 2px solid #ebebeb; -} -.tags-management .tag-man-item { - /*padding: .3cm;*/ - cursor: pointer; -} -.tags-management .tag-man-item.active { - background: #f3f3f3; -} -.tags-management .tag-man-item > a { - padding-left: .2cm; -} -.tags-management .tag-man-item .actions { - float: right; -} - -.tags-management .tag-man-item .actions ul { - display: block !important; -} -.tags-management .tag-man-item .actions .action { - display: inline-block; - width: auto !important; -} -.tags-management .tag-man-item .action button { - border: none; - background-color: transparent; - opacity: .45; - padding: 3px 12px; -} -.tags-management .tag-man-item .action button:hover { - opacity: 1; -} -.tags-management .tag-man-item input[type="text"] { - min-height: .7cm; - padding: 0 .1cm; - margin: 0; - width: 70%; -} - -/* make checkbox look like an icon */ -.menu-item-checkbox span:first-child { - padding: 0 10px; -} - -/* display 'remove from filter' when selected, and 'add to ...' when unselected*/ -.tags-management .tag-man-item.active .menu-filter-add { - display: none; -} -.tags-management .tag-man-item .menu-filter-remove { - display: none; -} -.tags-management .tag-man-item.active .menu-filter-remove { - display: block; -} - -.settings .import-form { - display: inline-block; -} -.settings .import { - display: none; -} -.settings .upload { - display: none; -} diff --git a/css/select2.css b/css/select2.css index ce3afd1d5..8b1378917 100644 --- a/css/select2.css +++ b/css/select2.css @@ -1,484 +1 @@ -.select2-container { - box-sizing: border-box; - display: inline-block; - margin: 0; - position: relative; - vertical-align: middle; } - .select2-container .select2-selection--single { - box-sizing: border-box; - cursor: pointer; - display: block; - height: 28px; - user-select: none; - -webkit-user-select: none; } - .select2-container .select2-selection--single .select2-selection__rendered { - display: block; - padding-left: 8px; - padding-right: 20px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } - .select2-container .select2-selection--single .select2-selection__clear { - position: relative; } - .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { - padding-right: 8px; - padding-left: 20px; } - .select2-container .select2-selection--multiple { - box-sizing: border-box; - cursor: pointer; - display: block; - min-height: 32px; - user-select: none; - -webkit-user-select: none; } - .select2-container .select2-selection--multiple .select2-selection__rendered { - display: inline-block; - overflow: hidden; - padding-left: 8px; - text-overflow: ellipsis; - white-space: nowrap; } - .select2-container .select2-search--inline { - float: left; } - .select2-container .select2-search--inline .select2-search__field { - box-sizing: border-box; - border: none; - font-size: 100%; - margin-top: 5px; - padding: 0; } - .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { - -webkit-appearance: none; } -.select2-dropdown { - background-color: white; - border: 1px solid #aaa; - border-radius: 4px; - box-sizing: border-box; - display: block; - position: absolute; - left: -100000px; - width: 100%; - z-index: 1051; } - -.select2-results { - display: block; } - -.select2-results__options { - list-style: none; - margin: 0; - padding: 0; } - -.select2-results__option { - padding: 6px; - user-select: none; - -webkit-user-select: none; } - .select2-results__option[aria-selected] { - cursor: pointer; } - -.select2-container--open .select2-dropdown { - left: 0; } - -.select2-container--open .select2-dropdown--above { - border-bottom: none; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } - -.select2-container--open .select2-dropdown--below { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; } - -.select2-search--dropdown { - display: block; - padding: 4px; } - .select2-search--dropdown .select2-search__field { - padding: 4px; - width: 100%; - box-sizing: border-box; } - .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { - -webkit-appearance: none; } - .select2-search--dropdown.select2-search--hide { - display: none; } - -.select2-close-mask { - border: 0; - margin: 0; - padding: 0; - display: block; - position: fixed; - left: 0; - top: 0; - min-height: 100%; - min-width: 100%; - height: auto; - width: auto; - opacity: 0; - z-index: 99; - background-color: #fff; - filter: alpha(opacity=0); } - -.select2-hidden-accessible { - border: 0 !important; - clip: rect(0 0 0 0) !important; - -webkit-clip-path: inset(50%) !important; - clip-path: inset(50%) !important; - height: 1px !important; - overflow: hidden !important; - padding: 0 !important; - position: absolute !important; - width: 1px !important; - white-space: nowrap !important; } - -.select2-container--default .select2-selection--single { - background-color: #fff; - border: 1px solid #aaa; - border-radius: 4px; } - .select2-container--default .select2-selection--single .select2-selection__rendered { - color: #444; - line-height: 28px; } - .select2-container--default .select2-selection--single .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; } - .select2-container--default .select2-selection--single .select2-selection__placeholder { - color: #999; } - .select2-container--default .select2-selection--single .select2-selection__arrow { - height: 26px; - position: absolute; - top: 1px; - right: 1px; - width: 20px; } - .select2-container--default .select2-selection--single .select2-selection__arrow b { - border-color: #888 transparent transparent transparent; - border-style: solid; - border-width: 5px 4px 0 4px; - height: 0; - left: 50%; - margin-left: -4px; - margin-top: -2px; - position: absolute; - top: 50%; - width: 0; } - -.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { - float: left; } - -.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { - left: 1px; - right: auto; } - -.select2-container--default.select2-container--disabled .select2-selection--single { - background-color: #eee; - cursor: default; } - .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { - display: none; } - -.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { - border-color: transparent transparent #888 transparent; - border-width: 0 4px 5px 4px; } - -.select2-container--default .select2-selection--multiple { - background-color: white; - border: 1px solid #aaa; - border-radius: 4px; - cursor: text; } - .select2-container--default .select2-selection--multiple .select2-selection__rendered { - box-sizing: border-box; - list-style: none; - margin: 0; - padding: 0 5px; - width: 100%; } - .select2-container--default .select2-selection--multiple .select2-selection__rendered li { - list-style: none; } - .select2-container--default .select2-selection--multiple .select2-selection__placeholder { - color: #999; - margin-top: 5px; - float: left; } - .select2-container--default .select2-selection--multiple .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; - margin-top: 5px; - margin-right: 10px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice { - background-color: #e4e4e4; - border: 1px solid #aaa; - border-radius: 4px; - cursor: default; - float: left; - margin-right: 5px; - margin-top: 5px; - padding: 0 5px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { - color: #999; - cursor: pointer; - display: inline-block; - font-weight: bold; - margin-right: 2px; } - .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { - color: #333; } - -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { - float: right; } - -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { - margin-left: 5px; - margin-right: auto; } - -.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { - margin-left: 2px; - margin-right: auto; } - -.select2-container--default.select2-container--focus .select2-selection--multiple { - border: solid black 1px; - outline: 0; } - -.select2-container--default.select2-container--disabled .select2-selection--multiple { - background-color: #eee; - cursor: default; } - -.select2-container--default.select2-container--disabled .select2-selection__choice__remove { - display: none; } - -.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { - border-top-left-radius: 0; - border-top-right-radius: 0; } - -.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } - -.select2-container--default .select2-search--dropdown .select2-search__field { - border: 1px solid #aaa; } - -.select2-container--default .select2-search--inline .select2-search__field { - background: transparent; - border: none; - outline: 0; - box-shadow: none; - -webkit-appearance: textfield; } - -.select2-container--default .select2-results > .select2-results__options { - max-height: 200px; - overflow-y: auto; } - -.select2-container--default .select2-results__option[role=group] { - padding: 0; } - -.select2-container--default .select2-results__option[aria-disabled=true] { - color: #999; } - -.select2-container--default .select2-results__option[aria-selected=true] { - background-color: #ddd; } - -.select2-container--default .select2-results__option .select2-results__option { - padding-left: 1em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__group { - padding-left: 0; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option { - margin-left: -1em; - padding-left: 2em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -2em; - padding-left: 3em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -3em; - padding-left: 4em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -4em; - padding-left: 5em; } - .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { - margin-left: -5em; - padding-left: 6em; } - -.select2-container--default .select2-results__option--highlighted[aria-selected] { - background-color: #5897fb; - color: white; } - -.select2-container--default .select2-results__group { - cursor: default; - display: block; - padding: 6px; } - -.select2-container--classic .select2-selection--single { - background-color: #f7f7f7; - border: 1px solid #aaa; - border-radius: 4px; - outline: 0; - background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); - background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); - background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } - .select2-container--classic .select2-selection--single:focus { - border: 1px solid #5897fb; } - .select2-container--classic .select2-selection--single .select2-selection__rendered { - color: #444; - line-height: 28px; } - .select2-container--classic .select2-selection--single .select2-selection__clear { - cursor: pointer; - float: right; - font-weight: bold; - margin-right: 10px; } - .select2-container--classic .select2-selection--single .select2-selection__placeholder { - color: #999; } - .select2-container--classic .select2-selection--single .select2-selection__arrow { - background-color: #ddd; - border: none; - border-left: 1px solid #aaa; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - height: 26px; - position: absolute; - top: 1px; - right: 1px; - width: 20px; - background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); - background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); - background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); } - .select2-container--classic .select2-selection--single .select2-selection__arrow b { - border-color: #888 transparent transparent transparent; - border-style: solid; - border-width: 5px 4px 0 4px; - height: 0; - left: 50%; - margin-left: -4px; - margin-top: -2px; - position: absolute; - top: 50%; - width: 0; } - -.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { - float: left; } - -.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { - border: none; - border-right: 1px solid #aaa; - border-radius: 0; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - left: 1px; - right: auto; } - -.select2-container--classic.select2-container--open .select2-selection--single { - border: 1px solid #5897fb; } - .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { - background: transparent; - border: none; } - .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { - border-color: transparent transparent #888 transparent; - border-width: 0 4px 5px 4px; } - -.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; - background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); - background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); - background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } - -.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { - border-bottom: none; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); - background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); - background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); } - -.select2-container--classic .select2-selection--multiple { - background-color: white; - border: 1px solid #aaa; - border-radius: 4px; - cursor: text; - outline: 0; } - .select2-container--classic .select2-selection--multiple:focus { - border: 1px solid #5897fb; } - .select2-container--classic .select2-selection--multiple .select2-selection__rendered { - list-style: none; - margin: 0; - padding: 0 5px; } - .select2-container--classic .select2-selection--multiple .select2-selection__clear { - display: none; } - .select2-container--classic .select2-selection--multiple .select2-selection__choice { - background-color: #e4e4e4; - border: 1px solid #aaa; - border-radius: 4px; - cursor: default; - float: left; - margin-right: 5px; - margin-top: 5px; - padding: 0 5px; } - .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { - color: #888; - cursor: pointer; - display: inline-block; - font-weight: bold; - margin-right: 2px; } - .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { - color: #555; } - -.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { - float: right; - margin-left: 5px; - margin-right: auto; } - -.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { - margin-left: 2px; - margin-right: auto; } - -.select2-container--classic.select2-container--open .select2-selection--multiple { - border: 1px solid #5897fb; } - -.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; } - -.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { - border-bottom: none; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; } - -.select2-container--classic .select2-search--dropdown .select2-search__field { - border: 1px solid #aaa; - outline: 0; } - -.select2-container--classic .select2-search--inline .select2-search__field { - outline: 0; - box-shadow: none; } - -.select2-container--classic .select2-dropdown { - background-color: white; - border: 1px solid transparent; } - -.select2-container--classic .select2-dropdown--above { - border-bottom: none; } - -.select2-container--classic .select2-dropdown--below { - border-top: none; } - -.select2-container--classic .select2-results > .select2-results__options { - max-height: 200px; - overflow-y: auto; } - -.select2-container--classic .select2-results__option[role=group] { - padding: 0; } - -.select2-container--classic .select2-results__option[aria-disabled=true] { - color: grey; } - -.select2-container--classic .select2-results__option--highlighted[aria-selected] { - background-color: #3875d7; - color: white; } - -.select2-container--classic .select2-results__group { - cursor: default; - display: block; - padding: 6px; } - -.select2-container--classic.select2-container--open .select2-dropdown { - border-color: #5897fb; } From 447a35d16cd404736a834db12d2c9e3b9c1d2f43 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 8 Feb 2018 21:12:23 +0100 Subject: [PATCH 066/110] Format js correctly --- js/Application.js | 104 +++++++++---------- js/Router.js | 104 +++++++++---------- js/bookmarklet.js | 16 +-- js/index.js | 64 ++++++------ js/models/Bookmark.js | 6 +- js/models/Bookmarks.js | 10 +- js/models/Tag.js | 8 +- js/models/Tags.js | 14 +-- js/views/AddBookmark.js | 148 +++++++++++++-------------- js/views/App.js | 98 +++++++++--------- js/views/BookmarkCard.js | 160 ++++++++++++++--------------- js/views/BookmarkDetail.js | 174 +++++++++++++++---------------- js/views/Bookmarks.js | 30 +++--- js/views/BulkActions.js | 185 +++++++++++++++++---------------- js/views/Content.js | 137 +++++++++++++------------ js/views/EmptyBookmarks.js | 16 +-- js/views/MobileNav.js | 30 +++--- js/views/Navigation.js | 50 ++++----- js/views/SearchController.js | 66 ++++++------ js/views/Settings.js | 187 +++++++++++++++++----------------- js/views/TagsManagement.js | 116 +++++++++++---------- js/views/TagsManagementTag.js | 182 ++++++++++++++++----------------- js/views/TagsNavigation.js | 14 +-- js/views/TagsNavigationTag.js | 50 ++++----- js/views/TagsSelection.js | 114 ++++++++++----------- 25 files changed, 1037 insertions(+), 1046 deletions(-) diff --git a/js/Application.js b/js/Application.js index 07375bb99..e3cabe21f 100644 --- a/js/Application.js +++ b/js/Application.js @@ -1,58 +1,58 @@ -import Backbone from 'backbone' -import Bookmarks from './models/Bookmarks' -import Tag from './models/Tag' -import Tags from './models/Tags' -import Router from './Router' -import AppView from './views/App' +import Backbone from 'backbone'; +import Bookmarks from './models/Bookmarks'; +import Tag from './models/Tag'; +import Tags from './models/Tags'; +import Router from './Router'; +import AppView from './views/App'; -const Marionette = Backbone.Marionette +const Marionette = Backbone.Marionette; export default Marionette.Application.extend({ - region: '#content' -, onBeforeStart: function() { - var that = this - this.bookmarks = new Bookmarks - this.tags = new Tags - this.tags.fetch({ - 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', that.onTagChanged) - that.listenTo(that.tags, 'add', that.onTagChanged) - that.listenTo(that.tags, 'remove', that.onTagChanged) - } - }) - this.listenTo(this.bookmarks, 'sync', this.onBookmarkTagsChanged) + region: '#content', + onBeforeStart: function() { + var that = this; + this.bookmarks = new Bookmarks; + this.tags = new Tags; + this.tags.fetch({ + 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', that.onTagChanged); + that.listenTo(that.tags, 'add', that.onTagChanged); + that.listenTo(that.tags, 'remove', that.onTagChanged); + } + }); + this.listenTo(this.bookmarks, 'sync', this.onBookmarkTagsChanged); - this.router = new Router({app: this}) - } -, onStart: function() { - this.showView(new AppView({app: this})); - Backbone.history.start(); - } -, 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 + this.router = new Router({app: this}); + }, + onStart: function() { + this.showView(new AppView({app: this})); + Backbone.history.start(); + }, + 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 - } + // 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 + } }); diff --git a/js/Router.js b/js/Router.js index 0bfc370ba..57409559b 100644 --- a/js/Router.js +++ b/js/Router.js @@ -1,57 +1,53 @@ -import Backbone from 'backbone' -import Bookmarks from './models/Bookmarks' -import Tags from './models/Tags' -import Router from './Router' -import AppView from './views/App' +import Backbone from 'backbone'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +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.fetch({ - data: {page: -1} - }) - Radio.channel('nav').trigger('navigate', 'all') - } - , favorites: function() { - Radio.channel('nav').trigger('navigate', 'favorites') - } - , shared: function() { - this.app.bookmarks.fetch({ - data: {page: -1} - }) - Radio.channel('nav').trigger('navigate', 'shared') - } - , tags: function(tagString) { - var tags = tagString? tagString.split(',').map(decodeURIComponent) : [] - this.app.bookmarks.fetch({ - data: {tags: tags, page: -1} - }) - Radio.channel('nav').trigger('navigate', 'tags', tags) - } - , search: function(query) { - this.app.bookmarks.fetch({ - data: {search: decodeURIComponent(query).split(' '), page: -1} - }) - Radio.channel('nav').trigger('navigate', 'search', query) - } - } -, appRoutes: { - '': 'index' - , 'all': 'all' - , 'favorites': 'favorites' - , 'shared': 'shared' - , 'tags(/*tags)': 'tags' - , 'search/:query': 'search' - } -, initialize: function(options) { - this.controller.app = options.app - } -}) + controller: { + index: function() { + setTimeout(function(){ + Backbone.history.navigate('all', {trigger: true}); + }, 1); + }, + all: function() { + this.app.bookmarks.fetch({ + data: {page: -1} + }); + Radio.channel('nav').trigger('navigate', 'all'); + }, + favorites: function() { + Radio.channel('nav').trigger('navigate', 'favorites'); + }, + shared: function() { + this.app.bookmarks.fetch({ + data: {page: -1} + }); + Radio.channel('nav').trigger('navigate', 'shared'); + }, + tags: function(tagString) { + var tags = tagString? tagString.split(',').map(decodeURIComponent) : []; + this.app.bookmarks.fetch({ + data: {tags: tags, page: -1} + }); + Radio.channel('nav').trigger('navigate', 'tags', tags); + }, + search: function(query) { + this.app.bookmarks.fetch({ + data: {search: decodeURIComponent(query).split(' '), page: -1} + }); + Radio.channel('nav').trigger('navigate', 'search', query); + } + }, + appRoutes: { + '': 'index', + 'all': 'all', + 'favorites': 'favorites', + 'shared': 'shared', + 'tags(/*tags)': 'tags', + 'search/:query': 'search' + }, + initialize: function(options) { + this.controller.app = options.app; + } +}); diff --git a/js/bookmarklet.js b/js/bookmarklet.js index bd5895748..c3b54e154 100644 --- a/js/bookmarklet.js +++ b/js/bookmarklet.js @@ -16,14 +16,14 @@ function decreaseAjaxCallCount() { function updateLoadingAnimation() { if (ajaxCallCount === 0) { - $("#add_form_loading").css("visibility", "hidden"); + $('#add_form_loading').css('visibility', 'hidden'); } else { - $("#add_form_loading").css("visibility", "visible"); + $('#add_form_loading').css('visibility', 'visible'); } } $(document).ready(function () { - $(".submit").click(function () { + $('.submit').click(function () { increaseAjaxCallCount(); var endpoint = 'bookmark'; @@ -39,8 +39,8 @@ $(document).ready(function () { $('.tagit-choice .tagit-label').each(function() { tags += '&item[tags][]='+$(this).text(); }); - var dataString = 'url=' + $("input#url").val() + '&description=' + - $("textarea#description").val() + '&title=' + $("input#title").val() + tags + id; + var dataString = 'url=' + $('input#url').val() + '&description=' + + $('textarea#description').val() + '&title=' + $('input#title').val() + tags + id; $.ajax({ type: method, url: endpoint, @@ -50,13 +50,13 @@ $(document).ready(function () { }, success: function (data) { if (data.status === 'success') { - OC.dialogs.message("Bookmark added.", "Success", undefined, [], undefined, true) + OC.dialogs.message('Bookmark added.', 'Success', undefined, [], undefined, true); _.delay(function() { window.close(); }, 1e3); } else { - OC.dialogs.alert(t("bookmarks", "Some Error happened."), - t("bookmarks", "Error"), null, true); + OC.dialogs.alert(t('bookmarks', 'Some Error happened.'), + t('bookmarks', 'Error'), null, true); } } }); diff --git a/js/index.js b/js/index.js index 8e3e5d907..b3f77b68e 100644 --- a/js/index.js +++ b/js/index.js @@ -1,38 +1,38 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import Marionette from 'backbone.marionette' -import select2 from 'select2' -import Tag from './models/Tag' -import Tags from './models/Tags' -import App from './Application' +import _ from 'underscore'; +import Backbone from 'backbone'; +import Marionette from 'backbone.marionette'; +import select2 from 'select2'; +import Tag from './models/Tag'; +import Tags from './models/Tags'; +import App from './Application'; -var _sync = Backbone.sync +var _sync = Backbone.sync; Backbone.sync = function(method, model, options) { - var overrideOptions = { - success: function(json) { - console.log(json) - if (!(model instanceof Tags)) { - options.success(json.item || json.data) - } else { - options.success(json) - } - } - , error: function() { - console.log(arguments) - } - , headers: { - 'requesttoken': oc_requesttoken - } - } - if (method === 'update' && model instanceof Tag) { - overrideOptions.url = model.urlRoot+'/'+model.previous('name') - } - _sync(method, model, _.extend({}, options, overrideOptions)) -} + var overrideOptions = { + success: function(json) { + console.log(json); + if (!(model instanceof Tags)) { + options.success(json.item || json.data); + } else { + options.success(json); + } + }, + error: function() { + console.log(arguments); + }, + headers: { + 'requesttoken': oc_requesttoken + } + }; + if (method === 'update' && model instanceof Tag) { + overrideOptions.url = model.urlRoot+'/'+model.previous('name'); + } + _sync(method, model, _.extend({}, options, overrideOptions)); +}; // init -var app = new App() +var app = new App(); $(function() { - app.start() -}) + app.start(); +}); diff --git a/js/models/Bookmark.js b/js/models/Bookmark.js index 05568aa46..3663b9f90 100644 --- a/js/models/Bookmark.js +++ b/js/models/Bookmark.js @@ -1,5 +1,5 @@ -import Backbone from 'backbone' +import Backbone from 'backbone'; export default Backbone.Model.extend({ - urlRoot: 'bookmark' -}) + urlRoot: 'bookmark' +}); diff --git a/js/models/Bookmarks.js b/js/models/Bookmarks.js index 8bf674483..c36734761 100644 --- a/js/models/Bookmarks.js +++ b/js/models/Bookmarks.js @@ -1,7 +1,7 @@ -import Backbone from 'backbone' -import Bookmark from './Bookmark' +import Backbone from 'backbone'; +import Bookmark from './Bookmark'; export default Backbone.Collection.extend({ - model: Bookmark -, url: 'bookmark' -}) + model: Bookmark, + url: 'bookmark' +}); diff --git a/js/models/Tag.js b/js/models/Tag.js index 844af4826..78999f55e 100644 --- a/js/models/Tag.js +++ b/js/models/Tag.js @@ -1,6 +1,6 @@ -import Backbone from 'backbone' +import Backbone from 'backbone'; export default Backbone.Model.extend({ - idAttribute: 'name' -, urlRoot: 'tag' -}) + idAttribute: 'name', + urlRoot: 'tag' +}); diff --git a/js/models/Tags.js b/js/models/Tags.js index 6b4df5af6..bcdb4f026 100644 --- a/js/models/Tags.js +++ b/js/models/Tags.js @@ -1,10 +1,10 @@ -import Backbone from 'backbone' -import Tag from './Tag' +import Backbone from 'backbone'; +import Tag from './Tag'; var Tags = Backbone.Collection.extend({ - model: Tag -, comparator: function(t) {return -t.get('count')} -, url: 'tag' -}) + model: Tag, + comparator: function(t) {return -t.get('count');}, + url: 'tag' +}); -export default Tags +export default Tags; diff --git a/js/views/AddBookmark.js b/js/views/AddBookmark.js index af02aa813..30c4e662a 100644 --- a/js/views/AddBookmark.js +++ b/js/views/AddBookmark.js @@ -1,77 +1,77 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import Bookmark from '../models/Bookmark' -import templateString from '../templates/AddBookmark.html' +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 +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() { - this.getUI('linkEntry').show() - this.getUI('formEntry').hide() - this.getUI('input').val('') - } -, 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}) - that.setPending(false) - that.deactivate() - } - , 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 - } -}) + 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() { + this.getUI('linkEntry').show(); + this.getUI('formEntry').hide(); + this.getUI('input').val(''); + }, + 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}); + that.setPending(false); + that.deactivate(); + }, + 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; + } +}); diff --git a/js/views/App.js b/js/views/App.js index 358dcfa06..491217362 100644 --- a/js/views/App.js +++ b/js/views/App.js @@ -1,53 +1,53 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import SearchController from './SearchController' -import AddBookmarkView from './AddBookmark' -import NavigationView from './Navigation' -import TagsManagementView from './TagsManagement' -import ContentView from './Content' -import SettingsView from './Settings' -import templateString from '../templates/App.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import SearchController from './SearchController'; +import AddBookmarkView from './AddBookmark'; +import NavigationView from './Navigation'; +import TagsManagementView from './TagsManagement'; +import ContentView from './Content'; +import SettingsView from './Settings'; +import templateString from '../templates/App.html'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.View.extend({ - template: _.template(templateString) -, regions: { - 'addBookmarks': { - el: '#add-bookmark-slot' - , replaceElement: true - } - , 'navigation': { - el: '#navigation-slot' - , replaceElement: true - } - , 'content': { - el: '#content-slot' - , replaceElement: true - } - , 'tags': { - el: '#favorite-tags-slot' - , replaceElement: true - } - , 'settings': { - el: '#settings-slot' - , replaceElement: true - } - } -, initialize: function(options) { - this.app = options.app - this.searchController = new SearchController + template: _.template(templateString), + regions: { + 'addBookmarks': { + el: '#add-bookmark-slot', + replaceElement: true + }, + 'navigation': { + el: '#navigation-slot', + replaceElement: true + }, + 'content': { + el: '#content-slot', + replaceElement: true + }, + 'tags': { + el: '#favorite-tags-slot', + replaceElement: true + }, + 'settings': { + el: '#settings-slot', + 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('addBookmarks', new AddBookmarkView()); - this.showChildView('navigation', new NavigationView); - this.showChildView('content', new ContentView({app: this.app})); - this.showChildView('tags', new TagsManagementView({collection: this.app.tags})) - this.showChildView('settings', new SettingsView()) - } -}) + $(window.document).click(function(e) { + Radio.channel('documentClicked').trigger('click', e); + }); + }, + onRender: function() { + this.showChildView('addBookmarks', new AddBookmarkView()); + this.showChildView('navigation', new NavigationView); + this.showChildView('content', new ContentView({app: this.app})); + this.showChildView('tags', new TagsManagementView({collection: this.app.tags})); + this.showChildView('settings', new SettingsView()); + } +}); diff --git a/js/views/BookmarkCard.js b/js/views/BookmarkCard.js index 893f3887c..9669a1e11 100644 --- a/js/views/BookmarkCard.js +++ b/js/views/BookmarkCard.js @@ -1,83 +1,83 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import Tags from '../models/Tags' -import TagsNavigationView from './TagsNavigation' -import templateString from '../templates/BookmarkCard.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import Tags from '../models/Tags'; +import TagsNavigationView from './TagsNavigation'; +import templateString from '../templates/BookmarkCard.html'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.View.extend({ - template: _.template(templateString), - className: "bookmark-card", - ui: { - 'checkbox': '.selectbox' - , 'actionsToggle': '.actions .toggle' - , 'actionsMenu': '.actions .popovermenu' - }, - regions: { - 'tags': '.tags' - }, - events: { - "click": "open" - , "click @ui.checkbox": "select" - , 'click @ui.actionsToggle': 'toggleActions' - , 'blur @ui.actionsToggle': 'closeActions' - , 'click .action-edit': 'actionEdit' - , 'click .action-delete': 'actionDelete' - , 'click .action-select': 'select' - , 'click .action-unselect': 'select' - }, - 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); - this.listenTo(this.app.tags, 'sync', this.render) - this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions) - } -, onRender: function() { - var that = this - this.$el.css('background-image', 'url(bookmark/'+this.model.get('id')+'/image)') - 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')) - } -, open: function(e) { - if (e && e.target !== this.el && e.target !== this.$('h1')[0]) return - Radio.channel('details').trigger('show', this.model) - } -, 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() - } -, toggleActions: function(e) { - this.getUI('actionsMenu').toggleClass('open') - this.$el.toggleClass('actions-open') - } -, closeActions: function(e) { - if (e.target === this.getUI('actionsToggle')[0]) return - this.getUI('actionsMenu').removeClass('open') - this.$el.removeClass('actions-open') - } -, actionDelete: function() { - this.model.destroy() - } -, actionEdit: function() { - Radio.channel('details').trigger('edit', this.model) - } -}) + template: _.template(templateString), + className: 'bookmark-card', + ui: { + 'checkbox': '.selectbox', + 'actionsToggle': '.actions .toggle', + 'actionsMenu': '.actions .popovermenu' + }, + regions: { + 'tags': '.tags' + }, + events: { + 'click': 'open', + 'click @ui.checkbox': 'select', + 'click @ui.actionsToggle': 'toggleActions', + 'blur @ui.actionsToggle': 'closeActions', + 'click .action-edit': 'actionEdit', + 'click .action-delete': 'actionDelete', + 'click .action-select': 'select', + 'click .action-unselect': 'select' + }, + 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); + this.listenTo(this.app.tags, 'sync', this.render); + this.listenTo(Radio.channel('documentClicked'), 'click', this.closeActions); + }, + onRender: function() { + var that = this; + this.$el.css('background-image', 'url(bookmark/'+this.model.get('id')+'/image)'); + 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')); + }, + open: function(e) { + if (e && e.target !== this.el && e.target !== this.$('h1')[0]) return; + Radio.channel('details').trigger('show', this.model); + }, + 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(); + }, + toggleActions: function(e) { + this.getUI('actionsMenu').toggleClass('open'); + this.$el.toggleClass('actions-open'); + }, + closeActions: function(e) { + if (e.target === this.getUI('actionsToggle')[0]) return; + this.getUI('actionsMenu').removeClass('open'); + this.$el.removeClass('actions-open'); + }, + actionDelete: function() { + this.model.destroy(); + }, + actionEdit: function() { + Radio.channel('details').trigger('edit', this.model); + } +}); diff --git a/js/views/BookmarkDetail.js b/js/views/BookmarkDetail.js index 32897fb22..2fda1dac8 100644 --- a/js/views/BookmarkDetail.js +++ b/js/views/BookmarkDetail.js @@ -1,90 +1,90 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import Tags from '../models/Tags' -import TagsNavigationView from './TagsNavigation' -import TagsSelectionView from './TagsSelection' -import templateStringDefault from '../templates/BookmarkDetail_default.html' -import templateStringEditing from '../templates/BookmarkDetail_editing.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import Tags from '../models/Tags'; +import TagsNavigationView from './TagsNavigation'; +import TagsSelectionView from './TagsSelection'; +import templateStringDefault from '../templates/BookmarkDetail_default.html'; +import templateStringEditing from '../templates/BookmarkDetail_editing.html'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.View.extend({ - getTemplate: function() { - if (this.editing) { - return this.templateEditing - } - return this.templateDefault - }, - templateDefault: _.template(templateStringDefault), - templateEditing: _.template(templateStringEditing), - className: "bookmark-detail", - regions: { - 'tags': { - el: '.tags' - } - }, - ui: { - 'close': '> .close' - , 'edit': '.edit' - , 'delete': '.delete' - }, - events: { - 'click @ui.close': 'close' - , 'click @ui.edit': 'edit' - , 'click @ui.delete': 'delete' - , 'click .submit': 'submit' - , 'click .cancel': 'cancel' - }, - initialize: function(opts) { - this.app = opts.app - this.listenTo(this.model, "change", this.render); - this.listenTo(this.app.tags, 'sync', this.render) - }, - onRender: function() { - var that = this - this.tags = new Tags(this.model.get('tags').map(function(id) { - return that.app.tags.findWhere({name: id}) - })) - if (this.editing) { - this.showChildView('tags', new TagsSelectionView({collection: this.app.tags, selected: this.tags, app: this.app })) - }else{ - this.showChildView('tags', new TagsNavigationView({collection: this.tags})) - } - }, - close: function() { - Radio.channel('details').trigger('close') - }, - setEditing: function(isEditing) { - if (isEditing) { - this.editing = true - this.$el.addClass('editing') - }else{ - this.editing = false - this.$el.removeClass('editing') - } - this.render() - }, - edit: function() { - this.setEditing(true) - } -, submit: function() { - var that = this - this.model.set({ - 'title': this.$('.input-title').val() - , 'url': this.$('.input-url').val() - , 'tags': this.tags.pluck('name') - , 'description': this.$('.input-desc').val() - }) - this.model.save({wait: true}) - this.model.once('sync', function() { - that.cancel() - }) - } -, cancel: function() { - this.setEditing(false) - } -, onDestroy: function() { - this.close() - } -}) + getTemplate: function() { + if (this.editing) { + return this.templateEditing; + } + return this.templateDefault; + }, + templateDefault: _.template(templateStringDefault), + templateEditing: _.template(templateStringEditing), + className: 'bookmark-detail', + regions: { + 'tags': { + el: '.tags' + } + }, + ui: { + 'close': '> .close', + 'edit': '.edit', + 'delete': '.delete' + }, + events: { + 'click @ui.close': 'close', + 'click @ui.edit': 'edit', + 'click @ui.delete': 'delete', + 'click .submit': 'submit', + 'click .cancel': 'cancel' + }, + initialize: function(opts) { + this.app = opts.app; + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.app.tags, 'sync', this.render); + }, + onRender: function() { + var that = this; + this.tags = new Tags(this.model.get('tags').map(function(id) { + return that.app.tags.findWhere({name: id}); + })); + if (this.editing) { + this.showChildView('tags', new TagsSelectionView({collection: this.app.tags, selected: this.tags, app: this.app })); + }else{ + this.showChildView('tags', new TagsNavigationView({collection: this.tags})); + } + }, + close: function() { + Radio.channel('details').trigger('close'); + }, + setEditing: function(isEditing) { + if (isEditing) { + this.editing = true; + this.$el.addClass('editing'); + }else{ + this.editing = false; + this.$el.removeClass('editing'); + } + this.render(); + }, + edit: function() { + this.setEditing(true); + }, + submit: function() { + var that = this; + this.model.set({ + 'title': this.$('.input-title').val(), + 'url': this.$('.input-url').val(), + 'tags': this.tags.pluck('name'), + 'description': this.$('.input-desc').val() + }); + this.model.save({wait: true}); + this.model.once('sync', function() { + that.cancel(); + }); + }, + cancel: function() { + this.setEditing(false); + }, + onDestroy: function() { + this.close(); + } +}); diff --git a/js/views/Bookmarks.js b/js/views/Bookmarks.js index 986456c1f..8bd7e7a7b 100644 --- a/js/views/Bookmarks.js +++ b/js/views/Bookmarks.js @@ -1,18 +1,18 @@ -import Backbone from 'backbone' -import BookmarkCardView from './BookmarkCard' -import EmptyBookmarksView from './EmptyBookmarks' +import Backbone from 'backbone'; +import BookmarkCardView from './BookmarkCard'; +import EmptyBookmarksView from './EmptyBookmarks'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.CollectionView.extend({ - className: 'bookmarks' -, initialize: function(opts) { - this.app = opts.app - } -, childViewOptions: function() { - return {app: this.app} - } -, childView: function() {return BookmarkCardView} -, emptyView: function() {return EmptyBookmarksView} -}) + className: 'bookmarks', + initialize: function(opts) { + this.app = opts.app; + }, + childViewOptions: function() { + return {app: this.app}; + }, + childView: function() {return BookmarkCardView;}, + emptyView: function() {return EmptyBookmarksView;} +}); diff --git a/js/views/BulkActions.js b/js/views/BulkActions.js index d9f380b54..e02844723 100644 --- a/js/views/BulkActions.js +++ b/js/views/BulkActions.js @@ -1,96 +1,95 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import Tags from '../models/Tags' -import TagsSelectionView from './TagsSelection' -import templateString from '../templates/BulkActions.html' +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 +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() { - var that = this - this.selected.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) - }) - } -}) + 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.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); + }); + } +}); diff --git a/js/views/Content.js b/js/views/Content.js index 09fcffc3b..726684165 100644 --- a/js/views/Content.js +++ b/js/views/Content.js @@ -1,73 +1,72 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import Bookmarks from '../models/Bookmarks' -import MobileNavView from './MobileNav' -import BulkActionsView from './BulkActions' -import BookmarksView from './Bookmarks' -import BookmarkDetailView from './BookmarkDetail' -import templateString from '../templates/Content.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import Bookmarks from '../models/Bookmarks'; +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 +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.View.extend({ - template: _.template(templateString) -, regions: { - 'mobileNav': { - el: '#mobile-nav-slot' - , replaceElement: true - } - , 'bulkActions': { - el: '#bulk-actions-slot' - , replaceElement: true - } - , 'viewBookmarks': { - el: '#view-bookmarks-slot' - , replaceElement: true - } - , 'bookmarkDetail': { - el: '#bookmark-detail-slot' - , replaceElement: true - } - } -, initialize: function(options) { - this.app = options.app - this.bookmarks = this.app.bookmarks - this.selected = new Bookmarks - 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'), 'edit', this.onEditDetails) - this.listenTo(Radio.channel('details'), 'close', this.onCloseDetails) - } -, onRender: function() { - this.showChildView('mobileNav', new MobileNavView()) - this.showChildView('bulkActions', new BulkActionsView({selected: this.selected, app: this.app})) - this.showChildView('viewBookmarks', new BookmarksView({collection: this.bookmarks, app: this.app})); - } -, onSelect: function(model) { - if (this.selected.length == 0) { - this.$el.addClass('selection-active') - } - this.selected.add(model) - } -, onUnselect: function(model) { - if (this.selected.length == 1) { - this.$el.removeClass('selection-active') + template: _.template(templateString), + regions: { + 'mobileNav': { + el: '#mobile-nav-slot', + replaceElement: true + }, + 'bulkActions': { + el: '#bulk-actions-slot', + replaceElement: true + }, + 'viewBookmarks': { + el: '#view-bookmarks-slot', + replaceElement: true + }, + 'bookmarkDetail': { + el: '#bookmark-detail-slot', + replaceElement: true + } + }, + initialize: function(options) { + this.app = options.app; + this.bookmarks = this.app.bookmarks; + this.selected = new Bookmarks; + 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'), 'edit', this.onEditDetails); + this.listenTo(Radio.channel('details'), 'close', this.onCloseDetails); + }, + onRender: function() { + this.showChildView('mobileNav', new MobileNavView()); + this.showChildView('bulkActions', new BulkActionsView({selected: this.selected, app: this.app})); + this.showChildView('viewBookmarks', new BookmarksView({collection: this.bookmarks, app: this.app})); + }, + onSelect: function(model) { + if (this.selected.length == 0) { + this.$el.addClass('selection-active'); + } + this.selected.add(model); + }, + onUnselect: function(model) { + if (this.selected.length == 1) { + this.$el.removeClass('selection-active'); + } + this.selected.remove(model); + }, + onShowDetails: function(model) { + var view = new BookmarkDetailView({model: model, app: this.app}); + this.showChildView('bookmarkDetail', view); + }, + onEditDetails: function(model) { + this.onShowDetails(model); + this.getRegion('bookmarkDetail').currentView.setEditing(true); + }, + onCloseDetails: function() { + this.detachChildView('bookmarkDetail'); } - this.selected.remove(model) - } -, onShowDetails: function(model) { - var that = this - var view = new BookmarkDetailView({model: model, app: this.app}) - this.showChildView('bookmarkDetail', view) - } -, onEditDetails: function(model) { - this.onShowDetails(model) - this.getRegion('bookmarkDetail').currentView.setEditing(true) - } -, onCloseDetails: function() { - this.detachChildView('bookmarkDetail') - } -}) +}); diff --git a/js/views/EmptyBookmarks.js b/js/views/EmptyBookmarks.js index 73f3c756d..9f13305ef 100644 --- a/js/views/EmptyBookmarks.js +++ b/js/views/EmptyBookmarks.js @@ -1,11 +1,11 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import templateString from '../templates/EmptyBookmarks.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/EmptyBookmarks.html'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.View.extend({ - template: _.template(templateString) -, className: 'bookmarks-empty' -}) + template: _.template(templateString), + className: 'bookmarks-empty' +}); diff --git a/js/views/MobileNav.js b/js/views/MobileNav.js index cf0b37273..aa167c5f9 100644 --- a/js/views/MobileNav.js +++ b/js/views/MobileNav.js @@ -1,18 +1,18 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import templateString from '../templates/MobileNav.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/MobileNav.html'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +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') - } -}) + className: 'mobile-nav', + template: _.template(templateString), + events: { + 'click .toggle-menu': 'toggleMenu' + }, + toggleMenu: function(e) { + e.preventDefault(); + $('body').toggleClass('mobile-nav-open'); + } +}); diff --git a/js/views/Navigation.js b/js/views/Navigation.js index 2852f9b82..a943c4d7c 100644 --- a/js/views/Navigation.js +++ b/js/views/Navigation.js @@ -1,28 +1,28 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import templateString from '../templates/Navigation.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/Navigation.html'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.View.extend({ - className: 'navigation' -, tagName: 'ul' -, template: _.template(templateString) -, events: { - 'click .all': 'onClick' - , 'click .favorites': 'onClick' - , 'click .shared': 'onClick' - } -, initialize: function() { - this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate, this) - } -, onClick: function(e) { - e.preventDefault() - Backbone.history.navigate(e.target.parentNode.dataset.id, {trigger: true}) - } -, onNavigate: function(category) { - $('.active', this.$el).removeClass('active') - if (category && this.$('[data-id='+category+']').length) this.$('[data-id='+category+']').addClass('active') - } -}) + className: 'navigation', + tagName: 'ul', + template: _.template(templateString), + events: { + 'click .all': 'onClick', + 'click .favorites': 'onClick', + 'click .shared': 'onClick' + }, + initialize: function() { + this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate, this); + }, + onClick: function(e) { + e.preventDefault(); + Backbone.history.navigate(e.target.parentNode.dataset.id, {trigger: true}); + }, + onNavigate: function(category) { + $('.active', this.$el).removeClass('active'); + if (category && this.$('[data-id='+category+']').length) this.$('[data-id='+category+']').addClass('active'); + } +}); diff --git a/js/views/SearchController.js b/js/views/SearchController.js index b8d1b8dfa..53ad678cb 100644 --- a/js/views/SearchController.js +++ b/js/views/SearchController.js @@ -1,36 +1,36 @@ -import Backbone from 'backbone' +import Backbone from 'backbone'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +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 - OC.Plugins.register('OCA.Search', { attach: function(search) { - search.setFilter('bookmarks', function(query) { - that.submit(query) - }) - } - }); - 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}) - } - } -}) + el: '#searchbox', + initialize: function() { + var that = this; + // register a dummy search plugin + OC.Plugins.register('OCA.Search', { attach: function(search) { + search.setFilter('bookmarks', function(query) { + that.submit(query); + }); + } + }); + 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}); + } + } +}); diff --git a/js/views/Settings.js b/js/views/Settings.js index 12aad83de..5d2b09f52 100644 --- a/js/views/Settings.js +++ b/js/views/Settings.js @@ -1,97 +1,96 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import templateString from '../templates/Settings.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/Settings.html'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +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' - , 'import': '.import' - , 'form': '.import-form' - , 'iframe': '.upload' - , 'status': '.import-status' - } -, events: { - 'click .settings-button': 'open' - , 'click .import-facade': 'importTrigger' - , 'change @ui.import': 'importSubmit' - , 'load @ui.iframe': 'importResult' - , 'click .export': 'exportTrigger' - } -, open: function(e) { - e.preventDefault() - this.getUI('content').slideToggle() - } -, 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.getUI('status').text(t('bookmark', 'Uploading...')); - } -, importResult: function (data) { - var data; - try { - data = $.parseJSON(data); - } catch (e) { - this.getUI('status').text(t('bookmark', 'Import error')); - return; - } - if (data.status == 'error') { - var list = $("
        ").addClass('setting_error_list'); - console.log(data); - $.each(data.data, function (index, item) { - list.append($("
      • ").text(item)); - }); - this.getUI('status').html(list); - return - } - this.getUI('status').text(t('bookmark', 'Import completed successfully.')); - Backbone.history.navigate('all', {trigger: true}) - } -, exportTrigger: function() { - window.location = 'bookmark/export?requesttoken='+oc_requesttoken - } -}) + className: 'settings', + id: 'app-settings', + template: _.template(templateString), + ui: { + 'content': '#app-settings-content', + 'import': '.import', + 'form': '.import-form', + 'iframe': '.upload', + 'status': '.import-status' + }, + events: { + 'click .settings-button': 'open', + 'click .import-facade': 'importTrigger', + 'change @ui.import': 'importSubmit', + 'load @ui.iframe': 'importResult', + 'click .export': 'exportTrigger' + }, + open: function(e) { + e.preventDefault(); + this.getUI('content').slideToggle(); + }, + 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.getUI('status').text(t('bookmark', 'Uploading...')); + }, + importResult: function (data) { + try { + data = $.parseJSON(data); + } catch (e) { + this.getUI('status').text(t('bookmark', 'Import error')); + return; + } + if (data.status == 'error') { + var list = $('
          ').addClass('setting_error_list'); + console.log(data); + $.each(data.data, function (index, item) { + list.append($('
        • ').text(item)); + }); + this.getUI('status').html(list); + return; + } + this.getUI('status').text(t('bookmark', 'Import completed successfully.')); + Backbone.history.navigate('all', {trigger: true}); + }, + exportTrigger: function() { + window.location = 'bookmark/export?requesttoken='+oc_requesttoken; + } +}); diff --git a/js/views/TagsManagement.js b/js/views/TagsManagement.js index 8a2ec216c..15da35a55 100644 --- a/js/views/TagsManagement.js +++ b/js/views/TagsManagement.js @@ -1,66 +1,64 @@ -import Backbone from 'backbone' -import Tags from '../models/Tags' -import TagView from './TagsManagementTag' +import Backbone from 'backbone'; +import Tags from '../models/Tags'; +import TagView from './TagsManagementTag'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.CollectionView.extend({ - childView: TagView -, tagName: 'ul' -, className: 'tags-management' -, initialize: function(options) { - var that = this - - this.selected = new Tags - this.selected.comparator = 'name' + 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, 'add', this.onAdd) - this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate) - } -, 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 - } + this.listenTo(this.collection, 'select', this.onSelect); + this.listenTo(this.collection, 'unselect', this.onUnselect); + this.listenTo(this.collection, 'add', this.onAdd); + this.listenTo(Radio.channel('nav'), 'navigate', this.onNavigate); + }, + 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 + 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) - }) + tag.trigger('select', tag, true); + }); - // hack! - // this is for when the route is triggered before the tags are loaded - this.lastRouteTags = tags - } -, onAdd: function(model) { - if (~this.lastRouteTags.indexOf(model.get('name'))) { - // wait for the tag view to render, so it can receive the event - setTimeout(function() { - model.trigger('select', model, true) - }, 1) - return - } - } -, 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}) - } -}) + // hack! + // this is for when the route is triggered before the tags are loaded + this.lastRouteTags = tags; + }, + onAdd: function(model) { + if (~this.lastRouteTags.indexOf(model.get('name'))) { + // wait for the tag view to render, so it can receive the event + setTimeout(function() { + model.trigger('select', model, true); + }, 1); + return; + } + }, + 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}); + } +}); diff --git a/js/views/TagsManagementTag.js b/js/views/TagsManagementTag.js index 5f03cf310..87fde0195 100644 --- a/js/views/TagsManagementTag.js +++ b/js/views/TagsManagementTag.js @@ -1,94 +1,94 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import templateStringDefault from '../templates/TagsManagementTag_default.html' -import templateStringEditing from '../templates/TagsManagementTag_editing.html' +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 +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': '.popovermenu' - , '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() - } -}) + 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': '.popovermenu', + '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(); + } +}); diff --git a/js/views/TagsNavigation.js b/js/views/TagsNavigation.js index d3bfcaf13..0cdb65ee5 100644 --- a/js/views/TagsNavigation.js +++ b/js/views/TagsNavigation.js @@ -1,10 +1,10 @@ -import Backbone from 'backbone' -import TagView from '../views/TagsNavigationTag' +import Backbone from 'backbone'; +import TagView from '../views/TagsNavigationTag'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +const Marionette = Backbone.Marionette; +const Radio = Backbone.Radio; export default Marionette.CollectionView.extend({ - tagName: 'ul' -, childView: TagView -}) + tagName: 'ul', + childView: TagView +}); diff --git a/js/views/TagsNavigationTag.js b/js/views/TagsNavigationTag.js index fba7b7893..415d5932b 100644 --- a/js/views/TagsNavigationTag.js +++ b/js/views/TagsNavigationTag.js @@ -1,28 +1,28 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import templateString from '../templates/TagsNavigationTag.html' +import _ from 'underscore'; +import Backbone from 'backbone'; +import templateString from '../templates/TagsNavigationTag.html'; -const Marionette = Backbone.Marionette -const Radio = Backbone.Radio +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') - } - } -}) + 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'); + } + } +}); diff --git a/js/views/TagsSelection.js b/js/views/TagsSelection.js index 19213a9d1..90b04c6a4 100644 --- a/js/views/TagsSelection.js +++ b/js/views/TagsSelection.js @@ -1,61 +1,61 @@ -import _ from 'underscore' -import Backbone from 'backbone' -import Tag from '../models/Tag' -import Tags from '../models/Tags' +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 +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' + 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) - this.listenTo(this.collection, 'add', this.onAttach) - } -, onAttach: function() { - if (this.$el.hasClass("select2-hidden-accessible")) { - this.$el.select2('destroy') - } - this.$el.select2({ - placeholder: t('bookmarks', 'Set tags') - , allowClear: true - , 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 tag = this.app.tags.get(e.params.data.text) || new Tag({name: e.params.data.text}) - this.selected.add(tag) - } -, onRemoveByUser: function(e) { - var tag = this.app.tags.get(e.params.data.text) || new Tag({name: e.params.data.text}) - this.selected.remove(tag) - } -, onChangeByAlgo: function(e) { - this.$el - .val(this.selected.pluck('name')) - .trigger('change') - } -}) + this.listenTo(this.selected, 'add', this.onChangeByAlgo); + this.listenTo(this.selected, 'remove', this.onChangeByAlgo); + this.listenTo(this.selected, 'reset', this.onChangeByAlgo); + this.listenTo(this.collection, 'add', this.onAttach); + }, + onAttach: function() { + if (this.$el.hasClass('select2-hidden-accessible')) { + this.$el.select2('destroy'); + } + this.$el.select2({ + placeholder: t('bookmarks', 'Set tags'), + allowClear: true, + 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 tag = this.app.tags.get(e.params.data.text) || new Tag({name: e.params.data.text}); + this.selected.add(tag); + }, + onRemoveByUser: function(e) { + var tag = this.app.tags.get(e.params.data.text) || new Tag({name: e.params.data.text}); + this.selected.remove(tag); + }, + onChangeByAlgo: function(e) { + this.$el + .val(this.selected.pluck('name')) + .trigger('change'); + } +}); From 5a04f8f4c0ac7e2ce40ff391bb2bfc9dbaeea355 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 8 Feb 2018 21:15:25 +0100 Subject: [PATCH 067/110] Fix Settings template fix requesttoken interpolation allow l10n --- js/templates/Settings.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/templates/Settings.html b/js/templates/Settings.html index 6efd35500..15cd3ccad 100644 --- a/js/templates/Settings.html +++ b/js/templates/Settings.html @@ -1,13 +1,13 @@
          - +
          - - + +
          - +
          From ea73367a718d0380bfa8a0bbde18a9b9c02e57e3 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 8 Feb 2018 21:23:20 +0100 Subject: [PATCH 068/110] Fix TagsManagementTag templates --- js/templates/TagsManagementTag_default.html | 40 ++++++++++++++++++++- js/templates/TagsManagementTag_editing.html | 14 +++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/js/templates/TagsManagementTag_default.html b/js/templates/TagsManagementTag_default.html index 7b0558277..321deab06 100644 --- a/js/templates/TagsManagementTag_default.html +++ b/js/templates/TagsManagementTag_default.html @@ -1 +1,39 @@ -<%- name %>
          • <%- count > 999 ? "999+" :count %>
          + + <%- name %> +
          +
            +
          • <%- count > 999 ? "999+" :count %>
          • +
          • + +
          • +
          +
          +
          +
          +
            +
          • + +
          • +
          • + +
          • +
          • + +
          • +
          • + +
          • +
          +
          diff --git a/js/templates/TagsManagementTag_editing.html b/js/templates/TagsManagementTag_editing.html index 8b48d1e62..28ededa3f 100644 --- a/js/templates/TagsManagementTag_editing.html +++ b/js/templates/TagsManagementTag_editing.html @@ -1 +1,13 @@ -
          + + +
          +
            +
          • + +
          • +
          • + +
          • +
          +
          +
          From 9e85d901d5578251bfe2626ea866c55cc8ffdb6b Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 9 Feb 2018 23:15:37 +0100 Subject: [PATCH 069/110] Fix select2.css --- css/select2.css | 636 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 636 insertions(+) diff --git a/css/select2.css b/css/select2.css index 8b1378917..2506a3bdf 100644 --- a/css/select2.css +++ b/css/select2.css @@ -1 +1,637 @@ +.select2-container { + box-sizing: border-box; + display: inline-block; + margin: 0; + position: relative; + vertical-align: middle; +} +.select2-container .select2-selection--single { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 28px; + user-select: none; + -webkit-user-select: none; +} + +.select2-container .select2-selection--single .select2-selection__rendered { + display: block; + padding-left: 8px; + padding-right: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.select2-container .select2-selection--single .select2-selection__clear { + position: relative; +} + +.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 8px; + padding-left: 20px; +} + +.select2-container .select2-selection--multiple { + box-sizing: border-box; + cursor: pointer; + display: block; + min-height: 32px; + user-select: none; + -webkit-user-select: none; +} + +.select2-container .select2-selection--multiple .select2-selection__rendered { + display: inline-block; + overflow: hidden; + padding-left: 8px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.select2-container .select2-search--inline { + float: left; +} + +.select2-container .select2-search--inline .select2-search__field { + box-sizing: border-box; + border: none; + font-size: 100%; + margin-top: 5px; + padding: 0; +} + +.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +.select2-dropdown { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + box-sizing: border-box; + display: block; + position: absolute; + left: -100000px; + width: 100%; + z-index: 1051; +} + +.select2-results { + display: block; +} + +.select2-results__options { + list-style: none; + margin: 0; + padding: 0; +} + +.select2-results__option { + padding: 6px; + user-select: none; + -webkit-user-select: none; +} + +.select2-results__option[aria-selected] { + cursor: pointer; +} + +.select2-container--open .select2-dropdown { + left: 0; +} + +.select2-container--open .select2-dropdown--above { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.select2-container--open .select2-dropdown--below { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.select2-search--dropdown { + display: block; + padding: 4px; +} + +.select2-search--dropdown .select2-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; +} + +.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +.select2-search--dropdown.select2-search--hide { + display: none; +} + +.select2-close-mask { + border: 0; + margin: 0; + padding: 0; + display: block; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 99; + background-color: #fff; + filter: alpha(opacity=0); +} + +.select2-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; +} + +.select2-container--default .select2-selection--single { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; +} + +.select2-container--default .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; +} + +.select2-container--default .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; +} + +.select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #999; +} + +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; +} + +.select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; +} + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; +} + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; +} + +.select2-container--default.select2-container--disabled .select2-selection--single { + background-color: #eee; + cursor: default; +} + +.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; +} + +.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; +} + +.select2-container--default .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; +} + +.select2-container--default .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 5px; + width: 100%; +} + +.select2-container--default .select2-selection--multiple .select2-selection__rendered li { + list-style: none; +} + +.select2-container--default .select2-selection--multiple .select2-selection__placeholder { + color: #999; + margin-top: 5px; + float: left; +} + +.select2-container--default .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-top: 5px; + margin-right: 10px; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + color: #999; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; +} + +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #333; +} + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; +} + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; +} + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; +} + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border: solid black 1px; + outline: 0; +} + +.select2-container--default.select2-container--disabled .select2-selection--multiple { + background-color: #eee; + cursor: default; +} + +.select2-container--default.select2-container--disabled .select2-selection__choice__remove { + display: none; +} + +.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, +.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, +.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; +} + +.select2-container--default .select2-search--inline .select2-search__field { + background: transparent; + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; +} + +.select2-container--default .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; +} + +.select2-container--default .select2-results__option[role=group] { + padding: 0; +} + +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; +} + +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; +} + +.select2-container--default .select2-results__option .select2-results__option { + padding-left: 1em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; +} + +.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; +} + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5897fb; + color: white; +} + +.select2-container--default .select2-results__group { + cursor: default; + display: block; + padding: 6px; +} + +.select2-container--classic .select2-selection--single { + background-color: #f7f7f7; + border: 1px solid #aaa; + border-radius: 4px; + outline: 0; + background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); +} + +.select2-container--classic .select2-selection--single:focus { + border: 1px solid #5897fb; +} + +.select2-container--classic .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; +} + +.select2-container--classic .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-right: 10px; +} + +.select2-container--classic .select2-selection--single .select2-selection__placeholder { + color: #999; +} + +.select2-container--classic .select2-selection--single .select2-selection__arrow { + background-color: #ddd; + border: none; + border-left: 1px solid #aaa; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); +} + +.select2-container--classic .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; +} + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; +} + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { + border: none; + border-right: 1px solid #aaa; + border-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + left: 1px; + right: auto; +} + +.select2-container--classic.select2-container--open .select2-selection--single { + border: 1px solid #5897fb; +} + +.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { + background: transparent; + border: none; +} + +.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; +} + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); +} + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); +} + +.select2-container--classic .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; + outline: 0; +} + +.select2-container--classic .select2-selection--multiple:focus { + border: 1px solid #5897fb; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__rendered { + list-style: none; + margin: 0; + padding: 0 5px; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__clear { + display: none; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { + color: #888; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; +} + +.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #555; +} + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + float: right; + margin-left: 5px; + margin-right: auto; +} + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; +} + +.select2-container--classic.select2-container--open .select2-selection--multiple { + border: 1px solid #5897fb; +} + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.select2-container--classic .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; + outline: 0; +} + +.select2-container--classic .select2-search--inline .select2-search__field { + outline: 0; + box-shadow: none; +} + +.select2-container--classic .select2-dropdown { + background-color: white; + border: 1px solid transparent; +} + +.select2-container--classic .select2-dropdown--above { + border-bottom: none; +} + +.select2-container--classic .select2-dropdown--below { + border-top: none; +} + +.select2-container--classic .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; +} + +.select2-container--classic .select2-results__option[role=group] { + padding: 0; +} + +.select2-container--classic .select2-results__option[aria-disabled=true] { + color: grey; +} + +.select2-container--classic .select2-results__option--highlighted[aria-selected] { + background-color: #3875d7; + color: white; +} + +.select2-container--classic .select2-results__group { + cursor: default; + display: block; + padding: 6px; +} + +.select2-container--classic.select2-container--open .select2-dropdown { + border-color: #5897fb; +} From 87d8f2de7bf78a60df99eaaa15ad276ed1bd2990 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 9 Feb 2018 23:16:51 +0100 Subject: [PATCH 070/110] Add ImageService --- controller/lib/imageservice.php | 112 ++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 controller/lib/imageservice.php diff --git a/controller/lib/imageservice.php b/controller/lib/imageservice.php new file mode 100644 index 000000000..98b309903 --- /dev/null +++ b/controller/lib/imageservice.php @@ -0,0 +1,112 @@ +. + * + */ + +namespace OCA\Bookmarks\Controller\Lib; + +use OCP\ICache; +use OCP\ICacheFactory; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\ClientException; + +class ImageService { + + // Cache for one month + const CACHE_TTL = 4 * 7 * 24 * 60 * 60; + + /** @var ICache */ + private $cache; + + /** + * @param ICacheFactory $cacheFactory + */ + public function __construct(ICacheFactory $cacheFactory) { + $this->cache = $cacheFactory->create('bookmarks.images'); + } + + private function buildKey($url) { + return base64_encode($url); + } + + /** + * @param string $url + * @return string|null image data + */ + public function getImage($url) { + $key = $this->buildKey($url); + // Try cache first + if ($image = $this->cache->get($key)) { + $image = json_decode($image, true); + return [ + 'contentType' => $image['contentType'], + 'data' => base64_decode($image['data']) + ]; + } + + // Fetch image from remote server + $image = $this->fetchImage($url); + + if (is_null($image)) { + return null; + } + + // Store in cache for next time + $json = json_encode([ + 'contentType' => $image['contentType'], + 'data' => base64_encode($image['data']) + ]); + $this->cache->set($key, $json, self::CACHE_TTL); + + return $image; + } + + /** + * @param string $url + * @return string|null fetched image data + */ + private function fetchImage($url) { + $body = $contentType = ''; + try { + $client = new \GuzzleHTTP\Client(); + $request = $client->get($url); + $body = $request->getBody(); + $contentType = $request->getHeader('Content-Type'); + } catch (\GuzzleHttp\Exception\RequestException $e) { + return null; + } catch (\Exception $e) { + throw $e; + } + + // Some HTPP Error occured :/ + if (200 != $request->getStatusCode()) { + return null; + } + + // It's not actually an image, doh. + if (!$contentType || stripos($contentType, 'image') !== 0) { + return null; + } + + return [ + 'contentType' => $contentType, + 'data' => $body + ]; + } +} + From f7638b18d678bd6c5804056757bcebbbf7f20929 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 9 Feb 2018 23:24:29 +0100 Subject: [PATCH 071/110] Fix bookmarks.css --- css/bookmarks.css | 428 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 428 insertions(+) diff --git a/css/bookmarks.css b/css/bookmarks.css index 8b1378917..87f6c879f 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -1 +1,429 @@ +#content > div { + height: 100%; +} +#app-navigation { + height: 100%; + position: relative; +} +#app-navigation > ul { + height: auto; + overflow: hidden; +} +.navigation a > span { + display: inline-block; + vertical-align: text-bottom; + margin-right: 12px; +} +#app-navigation h3 { + padding-top: .7cm; + padding-left: 12px; + border-top: 1px solid #ededed; +} +#app-content { + transition: transform .5s ease; + transform: none; +} +.mobile-nav { + padding: .3cm; + display: none; +} +.mobile-nav .toggle-menu { + display: inline-block; +} +@media only screen and (max-width: 768px) { + .mobile-nav-open #app-content { + transform: translate(250px, 0); + } + .mobile-nav { + display: block; + } +} + +.add-bookmark input { + margin: 0; + width: 100%; + padding: 12px 40px 12px 12px; + box-sizing: border-box; + background-color: transparent; + border: none; + border-bottom: 1px solid #eee; + border-radius: 0; +} +.add-bookmark button { + position: absolute; + right: 0; + top: 0; + min-height: 34px; + padding: 10px 20px; + background-color: transparent; + border: none; + opacity: .5; + font-weight: bold; +} +.add-bookmark li.link a span { + margin-left: 28px; +} +.add-bookmark li.link a span::before { + position: absolute; + left: 12px; + content: '+'; + font-weight: bold; + font-size: 28px; +} +.add-bookmark .form { + display: none; +} + +.bulk-actions { + display: none; + position: fixed; + top: 45px; + left: 250px; + right: 0; + border-bottom: 1px solid #ebebeb; + padding: .3cm .5cm; + background: #e9f7ff; + z-index: 3; +} +.bulk-actions + * { + transition: margin-top .5s ease; +} +.bulk-actions.active { + display: block; +} +.bulk-actions.active + * { + margin-top: 2.5cm; +} +@media only screen and (max-width: 768px) { + .bulk-actions { + position: relative; + top: 0; right: 0; bottom: 0; left: 0; + } + .bulk-actions.active + * { + margin-top: 0; + } +} +.bulk-actions > button { + vertical-align: top; +} +.bulk-actions .tags { + display: inline-block; + width: 60%; + margin: 3px 3px 3px 0; + position: relative; +} +.bulk-actions .tags-selection { + display: block; + width: 100%; +} +.bulk-actions .selection-tools { + float: right; +} +.bulk-actions .selection-tools .close { + display: inline-block; + cursor: pointer; + margin: 3px 3px 3px 0; + padding: 6px 12px; + width: auto; + min-height: 34px; + cursor: pointer; + box-sizing: border-box; +} +.bulk-actions .selection-tools .close span { + display: inline-block; + vertical-align: text-top; +} +button span, +.bulk-actions .selection-tools .close span { + cursor: pointer; +} + +.bookmarks-empty { + width: 5cm; + margin: 20% auto; +} + +.bookmarks { + display: flex; + flex-wrap: wrap; + overflow: hidden; + padding-right: .6cm; +} +.bookmark-card { + position: relative; + box-sizing: border-box; /*necessary for hover effect goodness to work*/ + flex-grow: 1; + width: 6cm; + height: 6cm; + margin: .6cm 0 0 .6cm; + padding: .5cm; + border: 1px solid #ddd; + overflow: hidden; + background-color: rgb(210, 210, 210); + background-size: cover; + background-position: center; + transition: background-color .3s ease; + background-blend-mode: overlay; +} +.bookmark-card, +.bookmark-card * { + cursor: pointer; +} +.bookmark-card:hover { + background-color: rgb(180, 180, 180); +} +.bookmark-card.active, +.bookmark-card.active:hover { + transform: none; + border: 1px solid #0082c9; + box-shadow: inset 0 0 10px #0082c9; +} +.bookmark-card .panel { + position: absolute; + top: 0; left: 0; right: 0; + padding: .5cm .5cm .1cm .6cm; + background: rgba(255, 255, 255, .8); + box-shadow: #aaa 0 0 5px; +} +.bookmark-card:hover .panel { + background: rgba(255, 255, 255, .90); +} +.bookmark-card .selectbox { + display: none; + position: absolute; + left: .1cm; + top: .5cm; +} +.selection-active .bookmark-card .selectbox, +.bookmark-card:hover .selectbox, +.bookmark-card.active .selectbox { + display: inline-block; +} +.bookmark-card h1 { + font-size: .5cm; + padding-right: .5cm; +} +.bookmark-card h2 { + margin-top: .1cm; + font-size: .4cm; + opacity: .4; +} +.bookmark-card h2 :link span { + display: inline-block; + vertical-align: text-bottom; + margin-right: 3px; +} +.bookmark-card h2 :link:hover { + text-decoration: underline; +} +.bookmark-card .actions { + display: none; + position: absolute; + top: .5cm; + right: .5cm; +} +.bookmark-card:hover .actions, +.bookmark-card.actions-open .actions { + display: block; +} +.bookmark-card .actions .popovermenu { + top: 24px; + right: -14px; +} +.bookmark-card .actions .action-select, +.bookmark-card.active .actions .action-unselect { + display: block; +} +.bookmark-card .actions .action-unselect, +.bookmark-card.active .actions .action-select { + display: none; +} +.bookmark-card .tags { + max-height: 2.5cm; + overflow: hidden; + position: absolute; + bottom: .5cm; +} +.bookmark-card .tag-nav-item { + border-radius: .2cm; + background: #ebebeb; + display: inline-block; + padding: 0 .2cm; + margin: .1cm; +} +.bookmark-card .tag-nav-item:hover { + background: #e0e0e0; +} + +.bookmark-detail { + position: fixed; + top: 45px; + right: 0; + z-index: 2; + width: 500px; + height: 100%; + box-sizing: border-box; + padding: 1cm; + background: white; + border-left: 1px solid #ebebeb; +} +@media only screen and (max-width: 768px) { + .bookmark-detail { + width: auto; + left: 0; + } +} +.bookmark-detail h1 { + font-size: .8cm; + line-height: 1; + padding-right: 2.3cm; +} +.bookmark-detail h2 { + margin-top: .3cm; + opacity: .4; +} +.bookmark-detail h2 :link:hover { + text-decoration: underline; +} +.bookmark-detail h2 :link span { + display: inline-block; + vertical-align: middle; + margin-right: 3px; +} +.bookmark-detail > .close { + position: absolute; + top: .1cm; + right: .1cm; + width: 1cm; + height: 1cm; + cursor: pointer; +} +.bookmark-detail .actions { + position: absolute; + right: 1cm; +} +.bookmark-detail .actions button { + height: 1cm; + width: 1cm; +} +.bookmark-detail .select2-container--default .select2-selection--multiple .select2-selection__choice, +.bulk-actions .select2-container--default .select2-selection--multiple .select2-selection__choice, +.bookmark-detail .tag-nav-item { + border-radius: .2cm; + border: none; + background: #ebebeb; + display: inline-block; + padding: .1cm .2cm; + margin: .1cm; +} +.bookmark-detail .select2-container--default .select2-selection--multiple .select2-selection__choice:hover, +.bulk-actions .select2-container--default .select2-selection--multiple .select2-selection__choice:hover, +.bookmark-detail .tag-nav-item:hover { + background: #e0e0e0; +} +.bookmark-detail .description { + margin-top: .5cm; +} + +.bookmark-detail.editing h1 { + padding-right: 0; +} +.bookmark-detail.editing .actions button { + height: auto; + width: auto; +} +.bookmark-detail .input-url, +.bookmark-detail .input-title, +.bookmark-detail .input-desc, +.bookmark-detail .tags-selection { + width: 100%; + font-size: inherit; + box-sizing: border-box; + padding: 0 !important; +} +.bookmark-detail .input-url { + padding-left: 25px !important; + background-position: 5px; +} +.bookmark-detail .input-desc { + height: 300px; +} + +.tags-management { + float: left; + width: 250px; + overflow: auto !important; +} +@media only screen and (max-width: 768px) { + .tags-management { + width: auto; + float: none; + border-bottom: 2px solid #bbb; + } +} +.tags-management > ul:first-child > li:last-child { + border-bottom: 2px solid #ebebeb; +} +.tags-management .tag-man-item { + /*padding: .3cm;*/ + cursor: pointer; +} +.tags-management .tag-man-item.active { + background: #f3f3f3; +} +.tags-management .tag-man-item > a { + padding-left: .2cm; +} +.tags-management .tag-man-item .actions { + float: right; +} + +.tags-management .tag-man-item .actions ul { + display: block !important; +} +.tags-management .tag-man-item .actions .action { + display: inline-block; + width: auto !important; +} +.tags-management .tag-man-item .action button { + border: none; + background-color: transparent; + opacity: .45; + padding: 3px 12px; +} +.tags-management .tag-man-item .action button:hover { + opacity: 1; +} +.tags-management .tag-man-item input[type="text"] { + min-height: .7cm; + padding: 0 .1cm; + margin: 0; + width: 70%; +} + +/* make checkbox look like an icon */ +.menu-item-checkbox span:first-child { + padding: 0 10px; +} + +/* display 'remove from filter' when selected, and 'add to ...' when unselected*/ +.tags-management .tag-man-item.active .menu-filter-add { + display: none; +} +.tags-management .tag-man-item .menu-filter-remove { + display: none; +} +.tags-management .tag-man-item.active .menu-filter-remove { + display: block; +} + +.settings .import-form { + display: inline-block; +} +.settings .import { + display: none; +} +.settings .upload { + display: none; +} From 32f00764a0ef66cea926bb113db5de499ab27ce8 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sun, 11 Feb 2018 17:32:03 +0100 Subject: [PATCH 072/110] Fix bulk deletion: Would iterate on mutable collection --- js/views/BulkActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/views/BulkActions.js b/js/views/BulkActions.js index e02844723..8acaa926d 100644 --- a/js/views/BulkActions.js +++ b/js/views/BulkActions.js @@ -57,7 +57,7 @@ export default Marionette.View.extend({ this.updateTags(); }, delete: function() { - this.selected.forEach(function(model) { + this.selected.slice().forEach(function(model) { model.trigger('unselect', model); model.destroy({ error: function() { From 1ab70b296d6c5d0dbfe69437d0af4adddb71837e Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sun, 11 Feb 2018 17:35:18 +0100 Subject: [PATCH 073/110] Move meta fetching to bm lib allows import to use it, too --- controller/lib/bookmarks.php | 57 +++++++++++++++++++-- controller/rest/bookmarkcontroller.php | 70 ++++---------------------- 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/controller/lib/bookmarks.php b/controller/lib/bookmarks.php index b0eab5e02..f1c925759 100644 --- a/controller/lib/bookmarks.php +++ b/controller/lib/bookmarks.php @@ -470,10 +470,49 @@ public function editBookmark($userid, $id, $url, $title, $tags = [], $descriptio */ public function addBookmark($userid, $url, $title, $tags = array(), $description = '', $isPublic = false, $image = null) { $public = $isPublic ? 1 : 0; - $urlWithoutPrefix = trim(substr($url, strpos($url, "://") + 3)); // Removes everything from the url before the "://" pattern (included) - if($urlWithoutPrefix === '') { - throw new \InvalidArgumentException('Bookmark URL is missing'); + + // do some meta tag inspection of the link... + + // allow only http(s) and (s)ftp + $protocols = '/^(https?|s?ftp)\:\/\//i'; + try { + if (preg_match($protocols, $url)) { + $data = $this->getURLMetadata($url); + } else { + // if no allowed protocol is given, evaluate https and https + foreach(['https://', 'http://'] as $protocol) { + $testUrl = $protocol . $url; + $data = $this->getURLMetadata($testUrl); + if(isset($data['title'])) { + break; + } + } + } + } catch (\Exception $e) { + // only because the server cannot reach a certain URL it does not + // mean the user's browser cannot. + \OC::$server->getLogger()->logException($e, ['app' => 'bookmarks']); + } + if (isset($data['url'])) { + $url = $data['url']; + } + if ((!isset($title) || trim($title) === '')) { + $title = isset($data['title'])? $data['title'] : $url; + } + if (isset($data['description']) && (!isset($description) || trim($description) === '')) { + $description = $data['description']; } + if (isset($data['image']) && !isset($image)) { + $image = $data['image']; + } + + // Check if it is a valid URL (after adding http(s) prefix) + $urlData = parse_url($url); + if(!$this->isProperURL($urlData)) { + throw new \InvalidArgumentException('Invalid URL supplied'); + } + + $urlWithoutPrefix = trim(substr($url, strpos($url, "://") + 3)); // Removes everything from the url before the "://" pattern (included) $decodedUrlNoPrefix = htmlspecialchars_decode($urlWithoutPrefix); $decodedUrl = htmlspecialchars_decode($url); @@ -704,4 +743,16 @@ public function analyzeTagRequest($line) { return $filterTag; } + /** + * Checks whether parse_url was able to return proper URL data + * + * @param bool|array $urlData result of parse_url + * @return bool + */ + protected function isProperURL($urlData) { + if ($urlData === false || !isset($urlData['scheme']) || !isset($urlData['host'])) { + return false; + } + return true; + } } diff --git a/controller/rest/bookmarkcontroller.php b/controller/rest/bookmarkcontroller.php index 3e828e750..545dd035a 100644 --- a/controller/rest/bookmarkcontroller.php +++ b/controller/rest/bookmarkcontroller.php @@ -184,53 +184,15 @@ public function getBookmarks( public function newBookmark($url = "", $item = array(), $title = "", $is_public = false, $description = "") { $title = trim($title); $image = null; - - // do some meta tag inspection of the link... - - // allow only http(s) and (s)ftp - $protocols = '/^(https?|s?ftp)\:\/\//i'; - try { - if (preg_match($protocols, $url)) { - $data = $this->bookmarks->getURLMetadata($url); - } else { - // if no allowed protocol is given, evaluate https and https - foreach(['https://', 'http://'] as $protocol) { - $testUrl = $protocol . $url; - $data = $this->bookmarks->getURLMetadata($testUrl); - if(isset($data['title'])) { - break; - } - } - } - } catch (\Exception $e) { - // only because the server cannot reach a certain URL it does not - // mean the user's browser cannot. - \OC::$server->getLogger()->logException($e, ['app' => 'bookmarks']); - } - if (isset($data['url'])) { - $url = $data['url']; - } - if ((!isset($title) || trim($title) === '')) { - $title = isset($data['title'])? $data['title'] : $url; - } - if (isset($data['description']) && (!isset($description) || trim($description) === '')) { - $description = $data['description']; - } - if (isset($data['image'])) { - $image = $data['image']; - } - - // Check if it is a valid URL (after adding http(s) prefix) - $urlData = parse_url($url); - if(!$this->isProperURL($urlData)) { - return new JSONResponse(array('status' => 'error'), Http::STATUS_BAD_REQUEST); - } - $tags = isset($item['tags']) ? $item['tags'] : array(); - $id = $this->bookmarks->addBookmark($this->userId, $url, $title, $tags, $description, $is_public, $image); - $bm = $this->bookmarks->findUniqueBookmark($id, $this->userId); - return new JSONResponse(array('item' => $bm, 'status' => 'success')); + try { + $id = $this->bookmarks->addBookmark($this->userId, $url, $title, $tags, $description, $is_public, $image); + } catch(\InvalidArgumentException $e) { + return new JSONResponse(array('status' => 'error', 'data' => [$e->getMessage()]), Http::STATUS_BAD_REQUEST); + } + $bm = $this->bookmarks->findUniqueBookmark($id, $this->userId); + return new JSONResponse(array('item' => $bm, 'status' => 'success')); } /** @@ -298,7 +260,7 @@ public function editBookmark($id = null, $url = null, $item = null, $title = nul // Check if url and id are valid $urlData = parse_url($bookmark['url']); - if(!$this->isProperURL($urlData) || !is_numeric($bookmark['id'])) { + if(!$this->bookmarks->isProperURL($urlData) || !is_numeric($bookmark['id'])) { return new JSONResponse(array(), Http::STATUS_BAD_REQUEST); } @@ -348,7 +310,7 @@ public function deleteBookmark($id = -1) { public function clickBookmark($url = "") { $url = urldecode($url); $urlData = parse_url($url); - if(!$this->isProperURL($urlData)) { + if(!$this->bookmarks->isProperURL($urlData)) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); } // prepare url for query @@ -441,18 +403,4 @@ public function exportBookmark() { return new ExportResponse($file); } - - /** - * Checks whether parse_url was able to return proper URL data - * - * @param bool|array $urlData result of parse_url - * @return bool - */ - protected function isProperURL($urlData) { - if ($urlData === false || !isset($urlData['scheme']) || !isset($urlData['host'])) { - return false; - } - return true; - } - } From f6a9b2c4ad193038bfcf981b5ab1172c186b817c Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sun, 11 Feb 2018 17:44:09 +0100 Subject: [PATCH 074/110] Reload bookmarks after import --- js/views/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/views/Settings.js b/js/views/Settings.js index 5d2b09f52..b147170cd 100644 --- a/js/views/Settings.js +++ b/js/views/Settings.js @@ -88,7 +88,7 @@ export default Marionette.View.extend({ return; } this.getUI('status').text(t('bookmark', 'Import completed successfully.')); - Backbone.history.navigate('all', {trigger: true}); + Backbone.history.navigate('', {trigger: true}); // reload app }, exportTrigger: function() { window.location = 'bookmark/export?requesttoken='+oc_requesttoken; From a495bce209c7100e576efb56578cbf13760d48cb Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Sun, 11 Feb 2018 20:38:49 +0100 Subject: [PATCH 075/110] Fix Bookmarks#isProperURL --- controller/lib/bookmarks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/lib/bookmarks.php b/controller/lib/bookmarks.php index f1c925759..4cbb75298 100644 --- a/controller/lib/bookmarks.php +++ b/controller/lib/bookmarks.php @@ -749,7 +749,7 @@ public function analyzeTagRequest($line) { * @param bool|array $urlData result of parse_url * @return bool */ - protected function isProperURL($urlData) { + public function isProperURL($urlData) { if ($urlData === false || !isset($urlData['scheme']) || !isset($urlData['host'])) { return false; } From 52bf384744551cba7477651c61ae9a6101b2b12d Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Mon, 12 Feb 2018 13:34:51 +0100 Subject: [PATCH 076/110] Fix styles for NCv13 --- css/bookmarks.css | 13 ------------- js/templates/AddBookmark.html | 6 ++---- js/templates/Navigation.html | 2 +- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/css/bookmarks.css b/css/bookmarks.css index 87f6c879f..7133d0c00 100644 --- a/css/bookmarks.css +++ b/css/bookmarks.css @@ -61,19 +61,6 @@ opacity: .5; font-weight: bold; } -.add-bookmark li.link a span { - margin-left: 28px; -} -.add-bookmark li.link a span::before { - position: absolute; - left: 12px; - content: '+'; - font-weight: bold; - font-size: 28px; -} -.add-bookmark .form { - display: none; -} .bulk-actions { display: none; diff --git a/js/templates/AddBookmark.html b/js/templates/AddBookmark.html index f9e3434d7..5ee872c5d 100644 --- a/js/templates/AddBookmark.html +++ b/js/templates/AddBookmark.html @@ -1,9 +1,7 @@ -
        • +
        • diff --git a/js/templates/Navigation.html b/js/templates/Navigation.html index fee537b79..c31bfe632 100644 --- a/js/templates/Navigation.html +++ b/js/templates/Navigation.html @@ -1,5 +1,5 @@
        • - <%- t('bookmarks', 'All bookmarks') %> + <%- t('bookmarks', 'All bookmarks') %>