Merge pull request #4480 from Raven24/publisher_backbone
Publisher - now with even more Backbone
This commit is contained in:
commit
72e421b38f
13 changed files with 569 additions and 409 deletions
|
|
@ -5,6 +5,7 @@
|
|||
* Build a color palette to uniform color usage [#4437](https://github.com/diaspora/diaspora/pull/4437) [#4469](https://github.com/diaspora/diaspora/pull/4469) [#4479](https://github.com/diaspora/diaspora/pull/4479)
|
||||
* Rename bitcoin_wallet_id setting to bitcoin_address [#4485](https://github.com/diaspora/diaspora/pull/4485)
|
||||
* Batch insert posts into stream collection for a small speedup [#4341](https://github.com/diaspora/diaspora/pull/4351)
|
||||
* Ported fileuploader to Backbone and refactored publisher views [#4480](https://github.com/diaspora/diaspora/pull/4480)
|
||||
|
||||
## Bug fixes
|
||||
* Highlight down arrow at the user menu on hover [#4441](https://github.com/diaspora/diaspora/pull/4441)
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ app.Router = Backbone.Router.extend({
|
|||
|
||||
app.page = new app.views.Stream({model : app.stream});
|
||||
app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.items});
|
||||
app.publisher.updateAspectsSelector(ids);
|
||||
app.publisher.setSelectedAspects(ids);
|
||||
|
||||
var streamFacesView = new app.views.StreamFaces({collection : app.stream.items});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
/* Copyright (c) 2010-2012, Diaspora Inc. This file is
|
||||
* licensed under the Affero General Public License version 3 or later. See
|
||||
* the COPYRIGHT file.
|
||||
*/
|
||||
|
||||
// Aspects view for the publisher.
|
||||
// Provides the ability to specify the visibility of posted content as public
|
||||
// or limited to selected aspects
|
||||
app.views.PublisherAspectSelector = Backbone.View.extend({
|
||||
|
||||
events: {
|
||||
"click .dropdown_list > li": "toggleAspect"
|
||||
},
|
||||
|
||||
// event handler for aspect selection
|
||||
toggleAspect: function(evt) {
|
||||
var el = $(evt.target);
|
||||
var btn = el.parent('.dropdown').find('.button');
|
||||
|
||||
// visually toggle the aspect selection
|
||||
if( el.is('.radio') ) {
|
||||
AspectsDropdown.toggleRadio(el);
|
||||
} else {
|
||||
AspectsDropdown.toggleCheckbox(el);
|
||||
}
|
||||
|
||||
// update the selection summary
|
||||
this._updateAspectsNumber(el);
|
||||
|
||||
this._updateSelectedAspectIds();
|
||||
},
|
||||
|
||||
// select a (list of) aspects in the dropdown selector by the given list of ids
|
||||
updateAspectsSelector: function(ids){
|
||||
var el = this.$("ul.dropdown_list");
|
||||
this.$('.dropdown_list > li').each(function(){
|
||||
var el = $(this);
|
||||
var aspectId = el.data('aspect_id');
|
||||
if (_.contains(ids, aspectId)) {
|
||||
el.addClass('selected');
|
||||
}
|
||||
else {
|
||||
el.removeClass('selected');
|
||||
}
|
||||
});
|
||||
|
||||
this._updateAspectsNumber(el);
|
||||
this._updateSelectedAspectIds();
|
||||
},
|
||||
|
||||
// take care of the form fields that will indicate the selected aspects
|
||||
_updateSelectedAspectIds: function() {
|
||||
var self = this;
|
||||
|
||||
// remove previous selection
|
||||
this.options.form.find('input[name="aspect_ids[]"]').remove();
|
||||
|
||||
// create fields for current selection
|
||||
this.$('.dropdown_list li.selected').each(function() {
|
||||
var el = $(this);
|
||||
var aspectId = el.data('aspect_id');
|
||||
|
||||
self._addHiddenAspectInput(aspectId);
|
||||
|
||||
// close the dropdown when a radio item was selected
|
||||
if( el.is('.radio') ) {
|
||||
el.closest('.dropdown').removeClass('active');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_updateAspectsNumber: function(el){
|
||||
AspectsDropdown.updateNumber(
|
||||
el.closest(".dropdown_list"),
|
||||
null,
|
||||
el.parent().find('li.selected').length,
|
||||
''
|
||||
);
|
||||
},
|
||||
|
||||
_addHiddenAspectInput: function(id) {
|
||||
var uid = _.uniqueId('aspect_ids_');
|
||||
this.options.form.append(
|
||||
'<input id="'+uid+'" name="aspect_ids[]" type="hidden" value="'+id+'">'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/* Copyright (c) 2010-2012, Diaspora Inc. This file is
|
||||
* licensed under the Affero General Public License version 3 or later. See
|
||||
* the COPYRIGHT file.
|
||||
*/
|
||||
|
||||
(function(){
|
||||
// mixin-object, used in conjunction with the publisher to provide the
|
||||
// functionality for selecting aspects
|
||||
app.views.PublisherAspectsSelector = {
|
||||
|
||||
// event handler for aspect selection
|
||||
toggleAspect: function(evt) {
|
||||
var el = $(evt.target);
|
||||
var btn = el.parent('.dropdown').find('.button');
|
||||
|
||||
// visually toggle the aspect selection
|
||||
if( el.is('.radio') ) {
|
||||
AspectsDropdown.toggleRadio(el);
|
||||
} else {
|
||||
AspectsDropdown.toggleCheckbox(el);
|
||||
}
|
||||
|
||||
// update the selection summary
|
||||
this._updateAspectsNumber(el);
|
||||
|
||||
this._updateSelectedAspectIds();
|
||||
},
|
||||
|
||||
updateAspectsSelector: function(ids){
|
||||
var el = this.$("ul.dropdown_list");
|
||||
this.$('.dropdown_list > li').each(function(){
|
||||
var el = $(this);
|
||||
var aspectId = el.data('aspect_id');
|
||||
if (_.contains(ids, aspectId)) {
|
||||
el.addClass('selected');
|
||||
}
|
||||
else {
|
||||
el.removeClass('selected');
|
||||
}
|
||||
});
|
||||
|
||||
this._updateAspectsNumber(el);
|
||||
this._updateSelectedAspectIds();
|
||||
},
|
||||
|
||||
// take care of the form fields that will indicate the selected aspects
|
||||
_updateSelectedAspectIds: function() {
|
||||
var self = this;
|
||||
|
||||
// remove previous selection
|
||||
this.$('input[name="aspect_ids[]"]').remove();
|
||||
|
||||
// create fields for current selection
|
||||
this.$('.dropdown .dropdown_list li.selected').each(function() {
|
||||
var el = $(this);
|
||||
var aspectId = el.data('aspect_id');
|
||||
|
||||
self._addHiddenAspectInput(aspectId);
|
||||
|
||||
// close the dropdown when a radio item was selected
|
||||
if( el.is('.radio') ) {
|
||||
el.closest('.dropdown').removeClass('active');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_updateAspectsNumber: function(el){
|
||||
AspectsDropdown.updateNumber(
|
||||
el.closest(".dropdown_list"),
|
||||
null,
|
||||
el.parent().find('li.selected').length,
|
||||
''
|
||||
);
|
||||
},
|
||||
|
||||
_addHiddenAspectInput: function(id) {
|
||||
var uid = _.uniqueId('aspect_ids_');
|
||||
this.$('.content_creation form').append(
|
||||
'<input id="'+uid+'" name="aspect_ids[]" type="hidden" value="'+id+'">'
|
||||
);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
/* Copyright (c) 2010-2012, Diaspora Inc. This file is
|
||||
* licensed under the Affero General Public License version 3 or later. See
|
||||
* the COPYRIGHT file.
|
||||
*/
|
||||
|
||||
(function(){
|
||||
// mixin-object, used in conjunction with the publisher to provide the
|
||||
// functionality for displaying 'getting-started' information
|
||||
app.views.PublisherGettingStarted = {
|
||||
|
||||
// initiate all the popover message boxes
|
||||
triggerGettingStarted: function() {
|
||||
this._addPopover(this.el_input, {
|
||||
trigger: 'manual',
|
||||
offset: 30,
|
||||
id: 'first_message_explain',
|
||||
placement: 'right',
|
||||
html: true
|
||||
}, 600);
|
||||
this._addPopover(this.$('.dropdown'), {
|
||||
trigger: 'manual',
|
||||
offset: 10,
|
||||
id: 'message_visibility_explain',
|
||||
placement: 'bottom',
|
||||
html: true
|
||||
}, 1000);
|
||||
this._addPopover($('#gs-shim'), {
|
||||
trigger: 'manual',
|
||||
offset: -5,
|
||||
id: 'stream_explain',
|
||||
placement: 'left',
|
||||
html: true
|
||||
}, 1400);
|
||||
|
||||
// hide some popovers when a post is created
|
||||
this.$('.button.creation').click(function() {
|
||||
this.$('.dropdown').popover('hide');
|
||||
this.el_input.popover('hide');
|
||||
});
|
||||
},
|
||||
|
||||
_addPopover: function(el, opts, timeout) {
|
||||
el.popover(opts);
|
||||
el.click(function() {
|
||||
el.popover('hide');
|
||||
});
|
||||
|
||||
// show the popover after the given timeout
|
||||
setTimeout(function() {
|
||||
el.popover('show');
|
||||
|
||||
// disable 'getting started' when the last popover is closed
|
||||
var popup = el.data('popover').$tip[0];
|
||||
var close = $(popup).find('.close');
|
||||
|
||||
close.click(function() {
|
||||
if( $('.popover').length==1 ) {
|
||||
$.get('/getting_started_completed');
|
||||
}
|
||||
el.popover('hide');
|
||||
});
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/* Copyright (c) 2010-2012, Diaspora Inc. This file is
|
||||
* licensed under the Affero General Public License version 3 or later. See
|
||||
* the COPYRIGHT file.
|
||||
*/
|
||||
|
||||
// Getting started view for the publisher.
|
||||
// Provides "getting started" popups around the elements of the publisher
|
||||
// for describing their use to new users.
|
||||
app.views.PublisherGettingStarted = Backbone.View.extend({
|
||||
|
||||
// initiate all the popover message boxes
|
||||
show: function() {
|
||||
this._addPopover(this.options.el_first_msg, {
|
||||
trigger: 'manual',
|
||||
offset: 30,
|
||||
id: 'first_message_explain',
|
||||
placement: 'right',
|
||||
html: true
|
||||
}, 600);
|
||||
this._addPopover(this.options.el_visibility, {
|
||||
trigger: 'manual',
|
||||
offset: 10,
|
||||
id: 'message_visibility_explain',
|
||||
placement: 'bottom',
|
||||
html: true
|
||||
}, 1000);
|
||||
this._addPopover(this.options.el_stream, {
|
||||
trigger: 'manual',
|
||||
offset: -5,
|
||||
id: 'stream_explain',
|
||||
placement: 'left',
|
||||
html: true
|
||||
}, 1400);
|
||||
|
||||
// hide some popovers when a post is created
|
||||
this.$('.button.creation').click(function() {
|
||||
this.options.el_visibility.popover('hide');
|
||||
this.options.el_first_msg.popover('hide');
|
||||
});
|
||||
},
|
||||
|
||||
_addPopover: function(el, opts, timeout) {
|
||||
el.popover(opts);
|
||||
el.click(function() {
|
||||
el.popover('hide');
|
||||
});
|
||||
|
||||
// show the popover after the given timeout
|
||||
setTimeout(function() {
|
||||
el.popover('show');
|
||||
|
||||
// disable 'getting started' when the last popover is closed
|
||||
var popup = el.data('popover').$tip[0];
|
||||
var close = $(popup).find('.close');
|
||||
|
||||
close.click(function() {
|
||||
if( $('.popover').length==1 ) {
|
||||
$.get('/getting_started_completed');
|
||||
}
|
||||
el.popover('hide');
|
||||
return false;
|
||||
});
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/* Copyright (c) 2010-2012, Diaspora Inc. This file is
|
||||
* licensed under the Affero General Public License version 3 or later. See
|
||||
* the COPYRIGHT file.
|
||||
*/
|
||||
|
||||
(function(){
|
||||
// mixin-object, used in conjunction with the publisher to provide the
|
||||
// functionality for selecting services for cross-posting
|
||||
app.views.PublisherServices = {
|
||||
|
||||
// visually toggle the icon and kick-off all other actions for cross-posting
|
||||
toggleService: function(evt) {
|
||||
var el = $(evt.target);
|
||||
var provider = el.attr('id');
|
||||
|
||||
el.toggleClass("dim");
|
||||
|
||||
this._createCounter();
|
||||
this._toggleServiceField(provider);
|
||||
},
|
||||
|
||||
// keep track of character count
|
||||
_createCounter: function() {
|
||||
// remove obsolete counter
|
||||
this.$('.counter').remove();
|
||||
|
||||
// create new counter
|
||||
var min = 40000;
|
||||
var a = this.$('.service_icon:not(.dim)');
|
||||
if(a.length > 0){
|
||||
$.each(a, function(index, value){
|
||||
var num = parseInt($(value).attr('maxchar'));
|
||||
if (min > num) { min = num; }
|
||||
});
|
||||
this.el_input.charCount({allowed: min, warning: min/10 });
|
||||
}
|
||||
},
|
||||
|
||||
// add or remove the input containing the selected service
|
||||
_toggleServiceField: function(provider) {
|
||||
var hidden_field = this.$('input[name="services[]"][value="'+provider+'"]');
|
||||
if(hidden_field.length > 0){
|
||||
hidden_field.remove();
|
||||
} else {
|
||||
var uid = _.uniqueId('services_');
|
||||
this.$(".content_creation form").append(
|
||||
'<input id="'+uid+'" name="services[]" type="hidden" value="'+provider+'">');
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
60
app/assets/javascripts/app/views/publisher/services_view.js
Normal file
60
app/assets/javascripts/app/views/publisher/services_view.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* Copyright (c) 2010-2012, Diaspora Inc. This file is
|
||||
* licensed under the Affero General Public License version 3 or later. See
|
||||
* the COPYRIGHT file.
|
||||
*/
|
||||
|
||||
// Services view for the publisher.
|
||||
// Provides the ability for selecting services for cross-posting
|
||||
app.views.PublisherServices = Backbone.View.extend({
|
||||
|
||||
events: {
|
||||
'click .service_icon': 'toggleService'
|
||||
},
|
||||
|
||||
tooltipSelector: '.service_icon',
|
||||
|
||||
initialize: function() {
|
||||
// init tooltip plugin
|
||||
this.$(this.tooltipSelector).tooltip();
|
||||
},
|
||||
|
||||
// visually toggle the icon and handle all other actions for cross-posting
|
||||
toggleService: function(evt) {
|
||||
var el = $(evt.target);
|
||||
var provider = el.attr('id');
|
||||
|
||||
el.toggleClass("dim");
|
||||
|
||||
this._createCounter();
|
||||
this._toggleServiceField(provider);
|
||||
},
|
||||
|
||||
// keep track of character count
|
||||
_createCounter: function() {
|
||||
// remove any obsolete counters
|
||||
this.options.input.siblings('.counter').remove();
|
||||
|
||||
// create new counter
|
||||
var min = 40000;
|
||||
var a = this.$('.service_icon:not(.dim)');
|
||||
if(a.length > 0){
|
||||
$.each(a, function(index, value){
|
||||
var num = parseInt($(value).attr('maxchar'));
|
||||
if (min > num) { min = num; }
|
||||
});
|
||||
this.options.input.charCount({allowed: min, warning: min/10 });
|
||||
}
|
||||
},
|
||||
|
||||
// add or remove the input containing the selected service
|
||||
_toggleServiceField: function(provider) {
|
||||
var hidden_field = this.options.form.find('input[name="services[]"][value="'+provider+'"]');
|
||||
if(hidden_field.length > 0){
|
||||
hidden_field.remove();
|
||||
} else {
|
||||
var uid = _.uniqueId('services_');
|
||||
this.options.form.append(
|
||||
'<input id="'+uid+'" name="services[]" type="hidden" value="'+provider+'">');
|
||||
}
|
||||
}
|
||||
});
|
||||
124
app/assets/javascripts/app/views/publisher/uploader_view.js
Normal file
124
app/assets/javascripts/app/views/publisher/uploader_view.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
|
||||
// Uploader view for the publisher.
|
||||
// Initializes the file uploader plugin and handles callbacks for the upload
|
||||
// progress. Attaches previews of finished uploads to the publisher.
|
||||
|
||||
app.views.PublisherUploader = Backbone.View.extend({
|
||||
|
||||
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'tif', 'tiff'],
|
||||
sizeLimit: 4194304, // bytes
|
||||
|
||||
initialize: function() {
|
||||
this.uploader = new qq.FileUploaderBasic({
|
||||
element: this.el,
|
||||
button: this.el,
|
||||
|
||||
//debug: true,
|
||||
|
||||
action: '/photos',
|
||||
params: { photo: { pending: true }},
|
||||
allowedExtensions: this.allowedExtensions,
|
||||
sizeLimit: this.sizeLimit,
|
||||
messages: {
|
||||
typeError: Diaspora.I18n.t('photo_uploader.invalid_ext'),
|
||||
sizeError: Diaspora.I18n.t('photo_uploader.size_error'),
|
||||
emptyError: Diaspora.I18n.t('photo_uploader.empty')
|
||||
},
|
||||
onProgress: _.bind(this.progressHandler, this),
|
||||
onSubmit: _.bind(this.submitHandler, this),
|
||||
onComplete: _.bind(this.uploadCompleteHandler, this)
|
||||
|
||||
});
|
||||
|
||||
this.el_info = $('<div id="fileInfo" />');
|
||||
this.options.publisher.el_wrapper.before(this.el_info);
|
||||
|
||||
this.options.publisher.el_photozone.on('click', '.x', _.bind(this._removePhoto, this));
|
||||
},
|
||||
|
||||
progressHandler: function(id, fileName, loaded, total) {
|
||||
var progress = Math.round(loaded / total * 100);
|
||||
this.el_info.text(fileName + ' ' + progress + '%').fadeTo(200, 1);
|
||||
},
|
||||
|
||||
submitHandler: function(id, fileName) {
|
||||
this.$el.addClass('loading');
|
||||
this._addPhotoPlaceholder();
|
||||
},
|
||||
|
||||
// add photo placeholders to the publisher to indicate an upload in progress
|
||||
_addPhotoPlaceholder: function() {
|
||||
var publisher = this.options.publisher;
|
||||
publisher.setButtonsEnabled(false);
|
||||
|
||||
publisher.el_wrapper.addClass('with_attachments');
|
||||
publisher.el_photozone.append(
|
||||
'<li class="publisher_photo loading" style="position:relative;">' +
|
||||
' <img src="'+Handlebars.helpers.imageUrl('ajax-loader2.gif')+'" alt="" />'+
|
||||
'</li>'
|
||||
);
|
||||
},
|
||||
|
||||
uploadCompleteHandler: function(id, fileName, response) {
|
||||
this.el_info.text(Diaspora.I18n.t('photo_uploader.completed', {file: fileName})).fadeTo(2000, 0);
|
||||
|
||||
var id = response.data.photo.id,
|
||||
url = response.data.photo.unprocessed_image.url;
|
||||
|
||||
this._addFinishedPhoto(id, url);
|
||||
},
|
||||
|
||||
// replace the first photo placeholder with the finished uploaded image and
|
||||
// add the id to the publishers form
|
||||
_addFinishedPhoto: function(id, url) {
|
||||
var publisher = this.options.publisher;
|
||||
|
||||
// add form input element
|
||||
publisher.$('.content_creation form').append(
|
||||
'<input type="hidden", value="'+id+'" name="photos[]" />'
|
||||
);
|
||||
|
||||
// replace placeholder
|
||||
var placeholder = publisher.el_photozone.find('li.loading').first();
|
||||
placeholder
|
||||
.removeClass('loading')
|
||||
.append(
|
||||
'<div class="x">X</div>'+
|
||||
'<div class="circle"></div>'
|
||||
)
|
||||
.find('img').attr({'src': url, 'data-id': id});
|
||||
|
||||
// no more placeholders? enable buttons
|
||||
if( publisher.el_photozone.find('li.loading').length == 0 ) {
|
||||
this.$el.removeClass('loading');
|
||||
publisher.setButtonsEnabled(true);
|
||||
}
|
||||
},
|
||||
|
||||
// remove an already uploaded photo
|
||||
_removePhoto: function(evt) {
|
||||
var self = this;
|
||||
var photo = $(evt.target).parents('.publisher_photo')
|
||||
var img = photo.find('img');
|
||||
|
||||
photo.addClass('dim');
|
||||
$.ajax({
|
||||
url: '/photos/'+img.attr('data-id'),
|
||||
dataType: 'json',
|
||||
type: 'DELETE',
|
||||
success: function() {
|
||||
$.when(photo.fadeOut(400)).then(function(){
|
||||
photo.remove();
|
||||
|
||||
if( self.options.publisher.$('.publisher_photo').length == 0 ) {
|
||||
// no more photos left...
|
||||
self.options.publisher.el_wrapper.removeClass('with_attachments');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -3,15 +3,13 @@
|
|||
* the COPYRIGHT file.
|
||||
*/
|
||||
|
||||
//= require ./publisher/services
|
||||
//= require ./publisher/aspects_selector
|
||||
//= require ./publisher/getting_started
|
||||
//= require ./publisher/services_view
|
||||
//= require ./publisher/aspect_selector_view
|
||||
//= require ./publisher/getting_started_view
|
||||
//= require ./publisher/uploader_view
|
||||
//= require jquery.textchange
|
||||
|
||||
app.views.Publisher = Backbone.View.extend(_.extend(
|
||||
app.views.PublisherServices,
|
||||
app.views.PublisherAspectsSelector,
|
||||
app.views.PublisherGettingStarted, {
|
||||
app.views.Publisher = Backbone.View.extend({
|
||||
|
||||
el : "#publisher",
|
||||
|
||||
|
|
@ -21,16 +19,12 @@ app.views.Publisher = Backbone.View.extend(_.extend(
|
|||
"click #hide_publisher" : "clear",
|
||||
"submit form" : "createStatusMessage",
|
||||
"click .post_preview_button" : "createPostPreview",
|
||||
"click .service_icon": "toggleService",
|
||||
"textchange #status_message_fake_text": "handleTextchange",
|
||||
"click .dropdown .dropdown_list li": "toggleAspect",
|
||||
"click #locator" : "showLocation",
|
||||
"click #hide_location" : "destroyLocation",
|
||||
"keypress #location_address" : "avoidEnter"
|
||||
},
|
||||
|
||||
tooltipSelector: ".service_icon",
|
||||
|
||||
initialize : function(){
|
||||
// init shortcut references to the various elements
|
||||
this.el_input = this.$('#status_message_fake_text');
|
||||
|
|
@ -46,9 +40,6 @@ app.views.Publisher = Backbone.View.extend(_.extend(
|
|||
// init autoresize plugin
|
||||
this.el_input.autoResize({ 'extraSpace' : 10, 'maxHeight' : Infinity });
|
||||
|
||||
// init tooltip plugin
|
||||
this.$(this.tooltipSelector).tooltip();
|
||||
|
||||
// sync textarea content
|
||||
if( this.el_hiddenInput.val() == "" ) {
|
||||
this.el_hiddenInput.val( this.el_input.val() );
|
||||
|
|
@ -73,10 +64,48 @@ app.views.Publisher = Backbone.View.extend(_.extend(
|
|||
}
|
||||
});
|
||||
|
||||
this.initSubviews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
initSubviews: function() {
|
||||
var form = this.$('.content_creation form');
|
||||
|
||||
this.view_services = new app.views.PublisherServices({
|
||||
el: this.$('#publisher_service_icons'),
|
||||
input: this.el_input,
|
||||
form: form
|
||||
});
|
||||
|
||||
this.view_aspect_selector = new app.views.PublisherAspectSelector({
|
||||
el: this.$('.public_toggle > .dropdown'),
|
||||
form: form
|
||||
});
|
||||
|
||||
this.view_getting_started = new app.views.PublisherGettingStarted({
|
||||
el_first_msg: this.el_input,
|
||||
el_visibility: this.$('.public_toggle > .dropdown'),
|
||||
el_stream: $('#gs-shim')
|
||||
});
|
||||
|
||||
this.view_uploader = new app.views.PublisherUploader({
|
||||
el: this.$('#file-upload'),
|
||||
publisher: this
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
// set the selected aspects in the dropdown by their ids
|
||||
setSelectedAspects: function(ids) {
|
||||
this.view_aspect_selector.updateAspectsSelector(ids);
|
||||
},
|
||||
|
||||
// show the "getting started" popups around the publisher
|
||||
triggerGettingStarted: function() {
|
||||
this.view_getting_started.show();
|
||||
},
|
||||
|
||||
createStatusMessage : function(evt) {
|
||||
if(evt){ evt.preventDefault(); }
|
||||
|
||||
|
|
@ -120,14 +149,15 @@ app.views.Publisher = Backbone.View.extend(_.extend(
|
|||
showLocation: function(){
|
||||
if($('#location').length == 0){
|
||||
$('#publisher_textarea_wrapper').after('<div id="location"></div>');
|
||||
app.views.location = new app.views.Location();
|
||||
this.view_locator = new app.views.Location();
|
||||
}
|
||||
},
|
||||
|
||||
// destroys the location
|
||||
destroyLocation: function(){
|
||||
if(app.views.location){
|
||||
app.views.location.remove();
|
||||
if(this.view_locator){
|
||||
this.view_locator.remove();
|
||||
delete this.view_locator;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -260,11 +290,11 @@ app.views.Publisher = Backbone.View.extend(_.extend(
|
|||
},
|
||||
|
||||
tryClose : function(){
|
||||
// if it is not submittable, close it.
|
||||
// if it is not submittable, close it.
|
||||
if( !this._submittable() ){
|
||||
this.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
open : function() {
|
||||
// visually 'open' the publisher
|
||||
|
|
@ -286,14 +316,18 @@ app.views.Publisher = Backbone.View.extend(_.extend(
|
|||
|
||||
checkSubmitAvailability: function() {
|
||||
if( this._submittable() ) {
|
||||
this.el_submit.removeAttr('disabled');
|
||||
this.el_preview.removeAttr('disabled');
|
||||
this.setButtonsEnabled(true);
|
||||
} else {
|
||||
this.el_submit.attr('disabled','disabled');
|
||||
this.el_preview.attr('disabled','disabled');
|
||||
this.setButtonsEnabled(false);
|
||||
}
|
||||
},
|
||||
|
||||
setButtonsEnabled: function(bool) {
|
||||
bool = !bool;
|
||||
this.el_submit.prop({disabled: bool});
|
||||
this.el_preview.prop({disabled: bool});
|
||||
},
|
||||
|
||||
// determine submit availability
|
||||
_submittable: function() {
|
||||
var onlyWhitespaces = ($.trim(this.el_input.val()) === ''),
|
||||
|
|
@ -311,7 +345,7 @@ app.views.Publisher = Backbone.View.extend(_.extend(
|
|||
});
|
||||
}
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
// jQuery helper for serializing a <form> into JSON
|
||||
$.fn.serializeObject = function()
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
-# Copyright (c) 2010-2011, Diaspora Inc. This file is
|
||||
-# licensed under the Affero General Public License version 3 or later. See
|
||||
-# the COPYRIGHT file.
|
||||
|
||||
:javascript
|
||||
function createUploader(){
|
||||
|
||||
var aspectIds = "#{aspect_ids}".split(',');
|
||||
|
||||
var uploader = new qq.FileUploaderBasic({
|
||||
element: document.getElementById('file-upload'),
|
||||
params: {'photo' : {'pending' : 'true', 'aspect_ids' : aspectIds}, 'set_profile_image' : "#{set_profile_image if defined?(set_profile_image)}"},
|
||||
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'tiff'],
|
||||
action: "#{photos_path}",
|
||||
debug: true,
|
||||
button: document.getElementById('file-upload'),
|
||||
sizeLimit: 4194304,
|
||||
|
||||
onProgress: function(id, fileName, loaded, total){
|
||||
var progress = Math.round(loaded / total * 100 );
|
||||
$('#fileInfo').text(fileName + ' ' + progress + '%').fadeTo(200, 1);
|
||||
},
|
||||
|
||||
messages: {
|
||||
typeError: "#{t('.invalid_ext')}",
|
||||
sizeError: "#{t('.size_error')}",
|
||||
emptyError: "#{t('.empty')}"
|
||||
},
|
||||
|
||||
onSubmit: function(id, fileName){
|
||||
$('#file-upload').addClass("loading");
|
||||
$('#publisher').find("input[type='submit']").attr('disabled','disabled');
|
||||
$('#publisher').find("button.post_preview_button").attr('disabled','disabled');
|
||||
|
||||
app.publisher.el_wrapper.addClass("with_attachments");
|
||||
$('#photodropzone').append(
|
||||
"<li class='publisher_photo loading' style='position:relative;'>" +
|
||||
"#{escape_javascript(image_tag('ajax-loader2.gif'))}" +
|
||||
"</li>"
|
||||
);
|
||||
},
|
||||
|
||||
onComplete: function(id, fileName, responseJSON) {
|
||||
$('#fileInfo').text(Diaspora.I18n.t("photo_uploader.completed", file=fileName)).fadeTo(2000, 0);
|
||||
var id = responseJSON.data.photo.id,
|
||||
url = responseJSON.data.photo.unprocessed_image.url,
|
||||
currentPlaceholder = $('li.loading').first();
|
||||
|
||||
app.publisher.el_wrapper.addClass("with_attachments");
|
||||
$('#new_status_message').append("<input type='hidden' value='" + id + "' name='photos[]' />");
|
||||
|
||||
// replace image placeholders
|
||||
var img = currentPlaceholder.find('img');
|
||||
img.attr('src', url);
|
||||
img.attr('data-id', id);
|
||||
currentPlaceholder.removeClass('loading');
|
||||
currentPlaceholder.append("<div class='x'>X</div>" +
|
||||
"<div class='circle'></div>");
|
||||
////
|
||||
|
||||
var publisher = $('#publisher'),
|
||||
textarea = publisher.find('textarea');
|
||||
|
||||
publisher.find("input[type='submit']").removeAttr('disabled');
|
||||
publisher.find("button.post_preview_button").removeAttr('disabled');
|
||||
|
||||
$('.x').bind('click', function(){
|
||||
var photo = $(this).closest('.publisher_photo');
|
||||
photo.addClass("dim");
|
||||
$.ajax({url: "/photos/" + photo.children('img').attr('data-id'),
|
||||
dataType: 'json',
|
||||
type: 'DELETE',
|
||||
success: function() {
|
||||
photo.fadeOut(400, function(){
|
||||
photo.remove();
|
||||
if ( $('.publisher_photo').length == 0){
|
||||
app.publisher.el_wrapper.removeClass("with_attachments");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onAllComplete: function(completed_files){
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
createUploader();
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
= status.error_messages
|
||||
%div
|
||||
%params
|
||||
#fileInfo
|
||||
#publisher_textarea_wrapper
|
||||
= link_to(content_tag(:div, nil, :class => 'icons-deletelabel'), "#", :id => "hide_publisher", :title => t('.discard_post'))
|
||||
%ul#photodropzone
|
||||
|
|
@ -90,5 +89,3 @@
|
|||
|
||||
|
||||
#publisher_photo_upload
|
||||
= render 'photos/new_photo', :aspect_ids => aspect_ids.join(',')
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ describe("app.views.Publisher", function() {
|
|||
it("hides the close button in standalone mode", function() {
|
||||
expect(this.view.$('#hide_publisher').is(':visible')).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
it("hides the post preview button in standalone mode", function() {
|
||||
expect(this.view.$('.post_preview_button').is(':visible')).toBeFalsy();
|
||||
});
|
||||
|
|
@ -65,7 +65,7 @@ describe("app.views.Publisher", function() {
|
|||
this.view.clear($.Event());
|
||||
expect(this.view.close).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
|
||||
it("calls removePostPreview", function(){
|
||||
spyOn(this.view, "removePostPreview");
|
||||
|
||||
|
|
@ -121,18 +121,18 @@ describe("app.views.Publisher", function() {
|
|||
var form = this.view.$("form")
|
||||
var submitCallback = jasmine.createSpy().andReturn(false);
|
||||
form.submit(submitCallback);
|
||||
|
||||
|
||||
var e = $.Event("keydown", { keyCode: 13 });
|
||||
e.ctrlKey = true;
|
||||
this.view.keyDown(e);
|
||||
|
||||
|
||||
expect(submitCallback).toHaveBeenCalled();
|
||||
expect($(this.view.el)).not.toHaveClass("closed");
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
context("#toggleService", function(){
|
||||
context("services", function(){
|
||||
beforeEach( function(){
|
||||
spec.loadFixture('aspects_index_services');
|
||||
this.view = new app.views.Publisher();
|
||||
|
|
@ -156,52 +156,42 @@ describe("app.views.Publisher", function() {
|
|||
expect(second.hasClass('dim')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("#_createCounter", function() {
|
||||
it("gets called in when you toggle service icons", function(){
|
||||
spyOn(this.view, '_createCounter');
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect(this.view._createCounter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes the 'old' .counter span", function(){
|
||||
spyOn($.fn, "remove");
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect($.fn.remove).toHaveBeenCalled();
|
||||
});
|
||||
it("creates a counter element", function(){
|
||||
expect(this.view.$('.counter').length).toBe(0);
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect(this.view.$('.counter').length).toBe(1);
|
||||
});
|
||||
|
||||
describe("#_toggleServiceField", function() {
|
||||
it("gets called when you toggle service icons", function(){
|
||||
spyOn(this.view, '_toggleServiceField');
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect(this.view._toggleServiceField).toHaveBeenCalled();
|
||||
});
|
||||
it("removes any old counters", function(){
|
||||
spyOn($.fn, "remove");
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect($.fn.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("toggles the hidden input field", function(){
|
||||
expect($('input[name="services[]"]').length).toBe(0);
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect($('input[name="services[]"]').length).toBe(1);
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect($('input[name="services[]"]').length).toBe(0);
|
||||
});
|
||||
it("toggles the hidden input field", function(){
|
||||
expect(this.view.$('input[name="services[]"]').length).toBe(0);
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect(this.view.$('input[name="services[]"]').length).toBe(1);
|
||||
$(".service_icon").first().trigger('click');
|
||||
expect(this.view.$('input[name="services[]"]').length).toBe(0);
|
||||
});
|
||||
|
||||
it("toggles the correct input", function() {
|
||||
var first = $(".service_icon").eq(0);
|
||||
var second = $(".service_icon").eq(1);
|
||||
it("toggles the correct input", function() {
|
||||
var first = $(".service_icon").eq(0);
|
||||
var second = $(".service_icon").eq(1);
|
||||
|
||||
first.trigger('click');
|
||||
second.trigger('click');
|
||||
first.trigger('click');
|
||||
second.trigger('click');
|
||||
|
||||
expect($('input[name="services[]"]').length).toBe(2);
|
||||
expect(this.view.$('input[name="services[]"]').length).toBe(2);
|
||||
|
||||
first.trigger('click');
|
||||
first.trigger('click');
|
||||
|
||||
var prov1 = first.attr('id');
|
||||
var prov2 = second.attr('id');
|
||||
var prov1 = first.attr('id');
|
||||
var prov2 = second.attr('id');
|
||||
|
||||
expect($('input[name="services[]"][value="'+prov1+'"]').length).toBe(0);
|
||||
expect($('input[name="services[]"][value="'+prov2+'"]').length).toBe(1);
|
||||
});
|
||||
expect(this.view.$('input[name="services[]"][value="'+prov1+'"]').length).toBe(0);
|
||||
expect(this.view.$('input[name="services[]"][value="'+prov2+'"]').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -236,18 +226,12 @@ describe("app.views.Publisher", function() {
|
|||
expect(this.check_els.last().hasClass('selected')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("#_updateSelectedAspectIds", function(){
|
||||
describe("hidden form elements", function(){
|
||||
beforeEach(function(){
|
||||
this.li = $('<li data-aspect_id="42" />');
|
||||
this.view.$('.dropdown_list').append(this.li);
|
||||
});
|
||||
|
||||
it("gets called when aspects are selected", function(){
|
||||
spyOn(this.view, "_updateSelectedAspectIds");
|
||||
this.check_els.last().trigger('click');
|
||||
expect(this.view._updateSelectedAspectIds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes a previous selection and inserts the current one", function() {
|
||||
var selected = this.view.$('input[name="aspect_ids[]"]');
|
||||
expect(selected.length).toBe(1);
|
||||
|
|
@ -261,13 +245,13 @@ describe("app.views.Publisher", function() {
|
|||
});
|
||||
|
||||
it("toggles the same item", function() {
|
||||
expect(this.view.$('input[name="aspect_ids[]"]').length).toBe(1);
|
||||
expect(this.view.$('input[name="aspect_ids[]"][value="42"]').length).toBe(0);
|
||||
|
||||
this.li.trigger('click');
|
||||
expect(this.view.$('input[name="aspect_ids[]"]').length).toBe(1);
|
||||
expect(this.view.$('input[name="aspect_ids[]"][value="42"]').length).toBe(1);
|
||||
|
||||
this.li.trigger('click');
|
||||
expect(this.view.$('input[name="aspect_ids[]"]').length).toBe(0);
|
||||
expect(this.view.$('input[name="aspect_ids[]"][value="42"]').length).toBe(0);
|
||||
});
|
||||
|
||||
it("keeps other fields with different values", function() {
|
||||
|
|
@ -275,30 +259,13 @@ describe("app.views.Publisher", function() {
|
|||
this.view.$('.dropdown_list').append(li2);
|
||||
|
||||
this.li.trigger('click');
|
||||
expect(this.view.$('input[name="aspect_ids[]"]').length).toBe(1);
|
||||
|
||||
li2.trigger('click');
|
||||
expect(this.view.$('input[name="aspect_ids[]"]').length).toBe(2);
|
||||
|
||||
expect(this.view.$('input[name="aspect_ids[]"][value="42"]').length).toBe(1);
|
||||
expect(this.view.$('input[name="aspect_ids[]"][value="99"]').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_addHiddenAspectInput", function(){
|
||||
it("gets called when aspects are selected", function(){
|
||||
spyOn(this.view, "_addHiddenAspectInput");
|
||||
this.check_els.last().trigger('click');
|
||||
expect(this.view._addHiddenAspectInput).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds a hidden input to the form", function(){
|
||||
var id = 42;
|
||||
|
||||
this.view._addHiddenAspectInput(id);
|
||||
var input = this.view.$('input[name="aspect_ids[]"][value="'+id+'"]');
|
||||
|
||||
expect(input.length).toBe(1);
|
||||
expect(input.val()).toBe('42');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context("locator", function() {
|
||||
|
|
@ -314,9 +281,9 @@ describe("app.views.Publisher", function() {
|
|||
it("Show location", function(){
|
||||
|
||||
// inserts location to the DOM; it is the location's view element
|
||||
setFixtures('<div id="publisher_textarea_wrapper"></div>');
|
||||
setFixtures('<div id="publisher_textarea_wrapper"></div>');
|
||||
|
||||
// creates a fake Locator
|
||||
// creates a fake Locator
|
||||
OSM = {};
|
||||
OSM.Locator = function(){return { getAddress:function(){}}};
|
||||
|
||||
|
|
@ -333,26 +300,11 @@ describe("app.views.Publisher", function() {
|
|||
|
||||
describe('#destroyLocation', function(){
|
||||
it("Destroy location if exists", function(){
|
||||
|
||||
// inserts location to the DOM; it is the location's view element
|
||||
setFixtures('<div id="location"></div>');
|
||||
|
||||
//Backup original view
|
||||
var original_location = app.views.Location;
|
||||
|
||||
// creates a new Location view with the #location element
|
||||
app.views.Location = new Backbone.View({el:"#location"});
|
||||
|
||||
// creates the mock
|
||||
app.views.location = sinon.mock(app.views.Location).object;
|
||||
|
||||
// calls the destroy function and test the expected result
|
||||
setFixtures('<div id="location"></div>');
|
||||
this.view.view_locator = new app.views.Location({el: "#location"});
|
||||
this.view.destroyLocation();
|
||||
|
||||
expect($("#location").length).toBe(0);
|
||||
|
||||
//Restore view
|
||||
app.views.Location = original_location;
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -368,5 +320,135 @@ describe("app.views.Publisher", function() {
|
|||
});
|
||||
});
|
||||
|
||||
context('uploader', function() {
|
||||
beforeEach(function() {
|
||||
jQuery.fx.off = true;
|
||||
setFixtures(
|
||||
'<div id="publisher">'+
|
||||
' <div class="content_creation"><form>'+
|
||||
' <div id="publisher_textarea_wrapper"></div>'+
|
||||
' <div id="photodropzone"></div>'+
|
||||
' <input type="submit" />'+
|
||||
' <button class="post_preview_button" />'+
|
||||
' </form></div>'+
|
||||
'</div>'
|
||||
);
|
||||
});
|
||||
|
||||
it('initializes the file uploader plugin', function() {
|
||||
spyOn(qq, 'FileUploaderBasic');
|
||||
var publisher = new app.views.Publisher();
|
||||
|
||||
expect(qq.FileUploaderBasic).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
context('event handlers', function() {
|
||||
beforeEach(function() {
|
||||
this.view = new app.views.Publisher();
|
||||
|
||||
// replace the uploader plugin with a dummy object
|
||||
var upload_view = this.view.view_uploader;
|
||||
this.uploader = {
|
||||
onProgress: _.bind(upload_view.progressHandler, upload_view),
|
||||
onSubmit: _.bind(upload_view.submitHandler, upload_view),
|
||||
onComplete: _.bind(upload_view.uploadCompleteHandler, upload_view)
|
||||
};
|
||||
upload_view.uploader = this.uploader;
|
||||
});
|
||||
|
||||
context('progress', function() {
|
||||
it('shows progress in percent', function() {
|
||||
this.uploader.onProgress(null, 'test.jpg', 20, 100);
|
||||
|
||||
var info = this.view.view_uploader.el_info;
|
||||
expect(info.text()).toContain('test.jpg');
|
||||
expect(info.text()).toContain('20%');
|
||||
});
|
||||
});
|
||||
|
||||
context('submitting', function() {
|
||||
beforeEach(function() {
|
||||
this.uploader.onSubmit(null, 'test.jpg');
|
||||
});
|
||||
|
||||
it('adds a placeholder', function() {
|
||||
expect(this.view.el_wrapper.attr('class')).toContain('with_attachments');
|
||||
expect(this.view.el_photozone.find('li').length).toBe(1);
|
||||
});
|
||||
|
||||
it('disables the publisher buttons', function() {
|
||||
expect(this.view.el_submit.prop('disabled')).toBeTruthy();
|
||||
expect(this.view.el_preview.prop('disabled')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
context('completion', function() {
|
||||
beforeEach(function() {
|
||||
Diaspora.I18n.loadLocale({ photo_uploader: { completed: '<%= file %> completed' }});
|
||||
$('#photodropzone').html('<li class="publisher_photo loading"><img src="" /></li>');
|
||||
|
||||
this.uploader.onComplete(null, 'test.jpg', { data: { photo: {
|
||||
id: '987',
|
||||
unprocessed_image: { url: 'test.jpg' }
|
||||
}}});
|
||||
});
|
||||
|
||||
it('shows it in text form', function() {
|
||||
var info = this.view.view_uploader.el_info;
|
||||
expect(info.text()).toBe(Diaspora.I18n.t('photo_uploader.completed', {file: 'test.jpg'}))
|
||||
});
|
||||
|
||||
it('adds a hidden input to the publisher', function() {
|
||||
var input = this.view.$('input[type="hidden"][value="987"][name="photos[]"]');
|
||||
expect(input.length).toBe(1);
|
||||
});
|
||||
|
||||
it('replaces the placeholder', function() {
|
||||
var li = this.view.el_photozone.find('li');
|
||||
var img = li.find('img');
|
||||
|
||||
expect(li.attr('class')).not.toContain('loading');
|
||||
expect(img.attr('src')).toBe('test.jpg');
|
||||
expect(img.attr('data-id')).toBe('987');
|
||||
});
|
||||
|
||||
it('re-enables the buttons', function() {
|
||||
expect(this.view.el_submit.prop('disabled')).toBeFalsy();
|
||||
expect(this.view.el_preview.prop('disabled')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('photo removal', function() {
|
||||
beforeEach(function() {
|
||||
this.view = new app.views.Publisher();
|
||||
this.view.el_wrapper.addClass('with_attachments');
|
||||
this.view.el_photozone.html(
|
||||
'<li class="publisher_photo">.'+
|
||||
' <img data-id="444" />'+
|
||||
' <div class="x">X</div>'+
|
||||
' <div class="circle"></div>'+
|
||||
'</li>'
|
||||
);
|
||||
|
||||
spyOn(jQuery, 'ajax').andCallFake(function(opts) { opts.success(); });
|
||||
this.view.el_photozone.find('.x').click();
|
||||
});
|
||||
|
||||
it('removes the element', function() {
|
||||
var photo = this.view.el_photozone.find('li.publisher_photo');
|
||||
expect(photo.length).toBe(0);
|
||||
});
|
||||
|
||||
it('sends an ajax request', function() {
|
||||
expect($.ajax).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes class on wrapper element', function() {
|
||||
expect(this.view.el_wrapper.attr('class')).not.toContain('with_attachments');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue