var saku, postForm; var UPLOADING_MSG = 'Uploading...'; var PLACEHOLDER = '〉〉〉'; connSM.on('synced', postSM.feeder('sync')); connSM.on('dropped', postSM.feeder('desync')); connSM.on('desynced', postSM.feeder('desync')); postSM.act('* + desync -> none', function () { if (postForm) { postForm.$el.removeClass('editing'); postForm.$input.val(''); postForm.finish(); } $('aside').remove(); }); postSM.act('none + sync, draft, alloc + done -> ready', function () { if (postForm) { postForm.remove(); postForm = null; saku = null; } insert_pbs(); var m = window.location.hash.match(/^#q(\d+)$/); if (m) { var id = parseInt(m[1], 10); if ($('#' + id).hasClass('highlight')) { window.location.hash = '#' + id; open_post_box(id); postForm.add_ref(id); } } }); postSM.act('ready + new -> draft', function (aside) { var op = null; var $sec = aside.closest('section'); if ($sec.length) { op = extract_num($sec); } else { $sec = $(''); } saku = new Saku({op: op}); postForm = new ComposerView({model: saku, dest: aside, thread: $sec}); }); postSM.preflight('draft', function (aside) { return aside.is('aside'); }); postSM.act('draft + alloc -> alloc', function (msg) { postForm.on_allocation(msg); }); $DOC.on('click', 'aside a', _.wrap(function () { postSM.feed('new', $(this).parent()); }, with_dom)); $DOC.on('keydown', handle_shortcut); var vapor = 0, wombo = 0, eject = 0; menuHandlers.Eject = function () { vapor = wombo = eject = 0; ComposerView.prototype.word_filter = function (w) { return w; }; flash_bg('white'); }; function handle_shortcut(event) { var k = event.which; if (vapor < 0 || wombo < 0) { if (event.shiftKey && k == [69,74,69,67,84,49][eject]) { if (++eject >= 6) { menuHandlers.Eject(); event.stopImmediatePropagation(); event.preventDefault(); } } else eject = 0; } else if (event.shiftKey && k > 85 && k < 88) { if (k == 86 && ++vapor > 10) { menuHandlers.Vapor(); event.stopImmediatePropagation(); event.preventDefault(); } if (k == 87 && ++wombo > 10) { wombo = -1; $.getScript(mediaURL + 'js/wordfilter.js'); } } else vapor = wombo = 0; if (!event.altKey) return; var used = false; switch (event.which) { case shortcutKeys['new']: var $aside = THREAD ? $('aside') : $ceiling.next(); if ($aside.is('aside') && $aside.length == 1) { with_dom(function () { postSM.feed('new', $aside); }); used = true; } break; case shortcutKeys.togglespoiler: if (postForm) { postForm.on_toggle(event); used = true; } break; case shortcutKeys.done: if (postForm) { if (!postForm.submit.attr('disabled')) { postForm.finish_wrapped(); used = true; } } break; case shortcutKeys.flip: menuHandlers.Flip(); used = true; break; } if (used) { event.stopImmediatePropagation(); event.preventDefault(); } } function open_post_box(num) { var a = $('#' + num); postSM.feed('new', a.is('section') ? a.children('aside') : a.siblings('aside')); } function make_reply_box() { return $(''); } function insert_pbs() { if (hot.get('readOnly') || readOnly.indexOf(BOARD) >= 0) return; if (THREAD ? $('aside').length : $ceiling.next().is('aside')) return; make_reply_box().appendTo('section'); if (!nashi.upload && BUMP) $ceiling.after(''); } function get_nonces() { var nonces; if (window.localStorage) { try { nonces = JSON.parse(localStorage.postNonces); } catch (e) {} } else { nonces = ComposerView.nonces; } return nonces || {}; } function save_nonces(nonces) { if (window.localStorage) localStorage.postNonces = JSON.stringify(nonces); else ComposerView.nonces = nonces; } function today_id() { return Math.floor(new Date().getTime() / (1000*60*60*24)); } function create_nonce() { var nonces = get_nonces(); var nonce = random_id(); nonces[nonce] = { tab: TAB_ID, day: today_id(), }; save_nonces(nonces); return nonce; } function expire_nonces() { if (!window.localStorage) return; // we need a lock on postNonces really var nonces = get_nonces(); // people messing with their system clock will mess with expiry, doh var changed = false; var yesterday = today_id() - 1; for (var nonce in nonces) { if (nonces[nonce].day >= yesterday) continue; delete nonces[nonce]; changed = true; } if (changed) save_nonces(nonces); } setTimeout(expire_nonces, Math.floor(Math.random()*5000)); function destroy_nonce(nonce) { var nonces = get_nonces(); if (!nonces[nonce]) return; delete nonces[nonce]; save_nonces(nonces); } var Saku = Backbone.Model.extend({ idAttribute: 'num', }); var ComposerView = Backbone.View.extend({ events: { 'input #subject': model_link('subject'), 'keydown #trans': 'on_key_down', 'click #done': 'finish_wrapped', 'click #toggle': 'on_toggle', }, initialize: function (dest) { this.listenTo(this.model, 'change', this.render_buttons); this.listenTo(this.model, 'change:spoiler', this.render_spoiler_pane); this.listenTo(this.model, 'change:floop', this.render_floop); var attrs = this.model.attributes; var op = attrs.op; var post = op ? $('') : this.options.thread; this.setElement(post[0]); this.buffer = $('
'); this.line_buffer = $(''); this.meta = $('!?
'));
oneeSama.trigger('fillMyName', $b);
var email = $email.val().trim();
if (is_noko(email))
email = '';
var tag = meta.children('a:first');
if (email)
tag.attr({href: 'mailto:' + email, target: '_blank',
'rel': 'nofollow noopener noreferrer', 'class': 'email'});
else
tag.removeAttr('href').removeAttr('target').attr('class',
'nope');
},
on_allocation: function (msg) {
var num = msg.num;
ownPosts[num] = true;
this.model.set({num: num});
this.flush_pending();
var header = $(flatten(oneeSama.atama(msg)).join(''));
this.meta.replaceWith(header);
this.meta = header;
var op = this.model.get('op');
if (op)
this.$el.addClass('editing');
else
spill_page();
this.$el.attr('id', num);
if (msg.image)
this.insert_uploaded(msg.image);
if (num == MILLION)
this.add_own_gravitas(msg);
if (this.uploadForm)
this.uploadForm.append(this.submit);
else
this.blockquote.after(this.submit);
if (!op) {
this.$subject.siblings('label').andSelf().remove();
this.blockquote.show();
this.resize_input();
this.$input.focus();
}
window.onbeforeunload = function () {
return "You have an unfinished post.";
};
},
on_image_alloc: function (msg) {
var attrs = this.model.attributes;
if (attrs.cancelled)
return;
if (!this.committed()) {
send([INSERT_POST, this.make_alloc_request(null, msg)]);
this.model.set({sentAllocRequest: true});
}
else {
send([INSERT_IMAGE, msg]);
}
},
entry_scroll_lock: function () {
/* NOPE */
if (lockTarget == PAGE_BOTTOM) {
/* Special keyup<->down case */
var height = $DOC.height();
if (height > lockKeyHeight)
window.scrollBy(0, height - lockKeyHeight + 1);
}
},
on_key_down: function (event) {
if (lockTarget == PAGE_BOTTOM) {
lockKeyHeight = $DOC.height();
_.defer($.proxy(this, 'entry_scroll_lock'));
}
switch (event.which) {
case 13:
event.preventDefault();
/* fall-through */
case 32:
var c = event.which == 13 ? '\n' : ' ';
// predict result
var input = this.$input[0];
var val = this.$input.val();
val = val.slice(0, input.selectionStart) + c +
val.slice(input.selectionEnd);
if (vapor >= 0 || c == '\n')
this.on_input(val);
break;
default:
handle_shortcut(event);
}
},
on_input: function (val) {
var $input = this.$input;
var start = $input[0].selectionStart, end = $input[0].selectionEnd;
if (val === undefined)
val = $input.val();
// dirty flag for writing back to the text box
var changed = false;
// character range we should not mangle
var ward = 0, ward_len = 0;
/* Turn YouTube links into proper refs */
while (true) {
var m = val.match(youtube_url_re);
if (!m)
break;
/* Substitute */
var t = m[4] || '';
t = this.find_time_arg(m[3]) || this.find_time_arg(m[1]) || t;
if (t[0] == '?')
t = '#' + t.substr(1);
var v = '>>>/watch?v=' + m[2] + t;
var old = m[0].length;
val = val.substr(0, m.index) + v + val.substr(m.index + old);
changed = true;
ward = m.index;
ward_len = v.length;
/* Compensate caret position */
if (m.index < start) {
var diff = old - v.length;
start -= diff;
end -= diff;
}
}
/* and SoundCloud links */
while (true) {
var m = val.match(soundcloud_url_re);
if (!m)
break;
var sc = '>>>/soundcloud/' + m[1];
var old = m[0].length;
val = val.substr(0, m.index) + sc + val.substr(m.index + old);
changed = true;
ward = m.index;
ward_len = sc.length;
if (m.index < start) {
var diff = old - sc.length;
start -= diff;
end -= diff;
}
}
/* and internal links */
while (true) {
var m = val.match(internal_url_re);
if (!m)
break;
var sc = '>>>/s/' + m[1];
var old = m[0].length;
val = val.substr(0, m.index) + sc + val.substr(m.index + old);
changed = true;
ward = m.index;
ward_len = sc.length;
if (m.index < start) {
var diff = old - sc.length;
start -= diff;
end -= diff;
}
}
/* and Twitter links */
while (true) {
var m = val.match(twitter_url_re);
if (!m)
break;
var tw = '>>>/@' + m[1] + '/' + m[2];
var old = m[0].length;
val = val.substr(0, m.index) + tw + val.substr(m.index + old);
changed = true;
ward = m.index;
ward_len = tw.length;
if (m.index < start) {
var diff = old - tw.length;
start -= diff;
end -= diff;
}
}
if (vapor < 0) {
if (!ward_len) {
// may have already converted from URL to >>ref, ward that too
var m = val.match(ref_re);
if (m) {
ward = m.index;
ward_len = m[0].length;
}
}
var vaped = this.vaporize(val, ward, ward+ward_len);
if (vaped != val) {
val = vaped;
changed = true;
}
}
if (changed)
$input.val(val);
if (this.$input.prop('placeholder'))
this.$input.prop('placeholder', '');
var len = val.length, lim = 0;
var nl = val.lastIndexOf('\n');
if (nl >= 0) {
var ok = val.substr(0, nl);
ok = this.word_filter(ok);
val = val.substr(nl+1);
$input.val(val);
if (this.model.get('sentAllocRequest') || /[^ ]/.test(ok))
this.commit(ok + '\n');
}
else if (vapor < 0 && !ward_len) {
// try to not break apart ##bigtext marker
if (len < 6 && /^##/.test(val))
lim = 0;
else if (len > 3)
lim = len - 3;
if (lim > 0) {
// don't break surrogate pairs apart
// (javascript uses UCS-2... how terrible)
var u = val.charCodeAt(lim - 1);
if (0xd800 <= u && u < 0xdc00)
lim--;
// don't cut off variation selectors
// (hack; we need a grapheme library...)
u = val.charCodeAt(lim);
if (0xfe00 <= u && u < 0xfe10)
lim--;
}
}
else {
var rev = val.split('').reverse().join('');
var m = rev.match(/^(\s*\S+\s+\S+)\s+(?=\S)/);
if (m)
lim = len - m[1].length;
}
if (lim > 0) {
var destiny = val.substr(0, lim);
destiny = this.word_filter(destiny);
this.commit(destiny);
val = val.substr(lim);
start -= lim;
end -= lim;
$input.val(val);
$input[0].setSelectionRange(start, end);
}
$input.attr('maxlength', MAX_POST_CHARS - this.char_count);
this.resize_input(val);
},
vaporize: function (text, ward_start, ward_end) {
var aesthetic = '';
for (var i = 0; i < text.length; i++) {
var c = text.charCodeAt(i);
if (i >= ward_start && i < ward_end) {
}
else if (c > 32 && c < 127)
c += 0xfee0;
else if (c == 32)
c = 0x3000;
aesthetic += String.fromCharCode(c);
}
return aesthetic;
},
word_filter: function (words) {
return words;
},
add_ref: function (num) {
/* If a >>link exists, put this one on the next line */
var $input = this.$input;
var val = $input.val();
if (/^>>\d+$/.test(val)) {
$input.val(val + '\n');
this.on_input();
val = $input.val();
}
$input.val(val + '>>' + num);
$input[0].selectionStart = $input.val().length;
this.on_input();
$input.focus();
},
find_time_arg: function (params) {
if (!params || params.indexOf('t=') < 0)
return false;
params = params.split('&');
for (var i = 0; i < params.length; i++) {
var pair = '#' + params[i];
if (youtube_time_re.test(pair))
return pair;
}
return false;
},
resize_input: function (val) {
var $input = this.$input;
if (typeof val != 'string')
val = $input.val();
this.$sizer.text(val);
var left = $input.offset().left - this.$el.offset().left;
var size = this.$sizer.width() + INPUT_ROOM;
size = Math.max(size, inputMinSize - left);
$input.css('width', size + 'px');
},
show_placeholder: function () {
var ph = PLACEHOLDER;
if (this.char_count * 2 > MAX_POST_CHARS)
ph = ' ' + this.char_count + '/' + MAX_POST_CHARS;
var $input = this.$input;
if ($input.prop('placeholder') != ph) {
$input.prop('placeholder', ph);
// make sure placeholder shows up immediately
if (!$input.val()) {
$input.val(' ');
$input.val('');
}
}
},
on_blur: function () {
var self = this;
// minor delay to avoid flashing when finishing posts
setTimeout(function () {
if (!self.$input.is(':focus'))
self.show_placeholder();
}, 500);
},
dispatch: function (msg) {
var a = msg.arg;
switch (msg.t) {
case 'alloc':
this.on_image_alloc(a);
break;
case 'error':
this.upload_error(a);
break;
case 'status':
this.upload_status(a);
break;
}
},
upload_status: function (msg) {
if (this.model.get('cancelled'))
return;
this.model.set('uploadStatus', msg);
},
upload_error: function (msg) {
if (this.model.get('cancelled'))
return;
this.model.set({uploadStatus: msg, uploading: false});
if (this.uploadForm)
this.uploadForm.find('input[name=alloc]').remove();
},
upload_finished_fallback: function () {
// this is just a fallback message for when we can't tell
// if there was an error due to cross-origin restrictions
var a = this.model.attributes;
var stat = a.uploadStatus;
if (!a.cancelled && a.uploading && (!stat || stat == UPLOADING_MSG))
this.model.set('uploadStatus', 'Unknown result.');
},
insert_uploaded: function (info) {
var form = this.uploadForm, op = this.model.get('op');
insert_image(info, form.siblings('header'), !op);
this.$imageInput.siblings('strong').andSelf().add(this.$cancel
).remove();
form.find('#toggle').remove();
this.flush_pending();
this.model.set({uploading: false, uploaded: true,
sentAllocRequest: true});
/* Stop obnoxious wrap-around-image behaviour */
var $img = this.$el.find('img');
this.blockquote.css({
'margin-left': $img.css('margin-right'),
'padding-left': $img.width(),
});
this.resize_input();
},
make_alloc_request: function (text, image) {
var msg = {nonce: create_nonce()};
function opt(key, val) {
if (val)
msg[key] = val;
}
opt('name', $name.val().trim());
opt('email', $email.val().trim());
opt('subject', this.$subject.val().trim());
opt('frag', text);
opt('image', image);
opt('op', this.model.get('op'));
if (this.model.get('floop'))
msg.flavor = 'floop';
return msg;
},
commit: function (text) {
var lines;
if (text.indexOf('\n') >= 0) {
lines = text.split('\n');
this.line_count += lines.length - 1;
var breach = this.line_count - MAX_POST_LINES + 1;
if (breach > 0) {
for (var i = 0; i < breach; i++)
lines.pop();
text = lines.join('\n');
this.line_count = MAX_POST_LINES;
}
}
var left = MAX_POST_CHARS - this.char_count;
if (left < text.length)
text = text.substr(0, left);
if (!text)
return;
this.char_count += text.length;
/* Either get an allocation or send the committed text */
var attrs = this.model.attributes;
if (!this.committed()) {
send([INSERT_POST, this.make_alloc_request(text, null)]);
this.model.set({sentAllocRequest: true});
}
else if (attrs.num)
send(text);
else
this.pending += text;
/* Add it to the user's display */
var line_buffer = this.line_buffer;
if (lines) {
lines[0] = line_buffer.text() + lines[0];
line_buffer.text(lines.pop());
for (var i = 0; i < lines.length; i++)
this.imouto.fragment(lines[i] + '\n');
}
else {
line_buffer.append(document.createTextNode(text));
line_buffer[0].normalize();
}
},
committed: function () {
var a = this.model.attributes;
return !!(a.num || a.sentAllocRequest);
},
flush_pending: function () {
if (this.pending) {
send(this.pending);
this.pending = '';
}
},
cancel: function () {
if (this.model.get('uploading')) {
this.$iframe.remove();
this.$iframe = $('', {
src: '', name: 'upload', id: 'hidden-upload',
}).appendTo('body');
this.upload_error('');
this.model.set({cancelled: true});
}
else
this.finish_wrapped();
},
finish: function () {
if (this.model.get('num')) {
this.flush_pending();
this.commit(this.word_filter(this.$input.val()));
this.$input.remove();
this.submit.remove();
if (this.uploadForm)
this.uploadForm.remove();
if (this.$iframe) {
this.$iframe.remove();
this.$iframe = null;
}
this.imouto.fragment(this.line_buffer.text());
this.buffer.replaceWith(this.buffer.contents());
this.line_buffer.remove();
this.blockquote.css({'margin-left': '', 'padding-left': ''});
send([FINISH_POST]);
this.preserve = true;
}
postSM.feed('done');
this.$el.removeClass('mine');
},
remove: function () {
if (!this.preserve) {
if (!this.model.get('op'))
this.$el.next('hr').remove();
this.$el.remove();
}
this.$sizer.remove();
if (this.$iframe) {
this.$iframe.remove();
this.$iframe = null;
}
this.stopListening();
window.onbeforeunload = null;
},
render_buttons: function () {
var attrs = this.model.attributes;
var allocWait = attrs.sentAllocRequest && !attrs.num;
var d = attrs.uploading || allocWait;
var self = this;
with_dom(function () {
/* Beware of undefined! */
self.submit.prop('disabled', !!d);
if (attrs.uploaded)
self.submit.css({'margin-left': '0'});
self.$cancel.prop('disabled', !!allocWait);
self.$cancel.toggle(!!(!attrs.num || attrs.uploading));
self.$imageInput.prop('disabled', !!attrs.uploading);
self.$uploadStatus.text(attrs.uploadStatus);
var auto = options.get('autocomplete') ? 'on' : 'off';
self.$input.attr({autocapitalize: auto, autocomplete: auto,
autocorrect: auto, spellcheck: auto == 'on'});
});
},
prep_upload: function () {
this.model.set('uploadStatus', UPLOADING_MSG);
this.$input.focus();
var attrs = this.model.attributes;
return {spoiler: attrs.spoiler, op: attrs.op || 0};
},
notify_uploading: function () {
this.model.set({uploading: true, cancelled: false});
this.$input.focus();
},
make_upload_form: function () {
var form = $('');
this.$cancel = $('', {
type: 'button', value: 'Cancel',
click: $.proxy(this, 'cancel'),
});
var opts = {
type: 'file', id: 'image', name: 'image',
change: $.proxy(this, 'on_image_chosen'),
};
if (!imagerConfig.VIDEO)
opts.accept = 'image/*';
this.$imageInput = $('', opts);
this.$toggle = $('', {
type: 'button', id: 'toggle',
});
this.$uploadStatus = $('');
form.append(this.$cancel, this.$imageInput, this.$toggle, ' ',
this.$uploadStatus);
this.$iframe = $('', {
src: '', name: 'upload', id: 'hidden-upload',
}).appendTo('body');
if (nashi.upload) {
this.$imageInput.hide();
this.$toggle.hide();
}
this.model.set({spoiler: 0, nextSpoiler: -1});
return form;
},
on_image_chosen: function () {
if (this.model.get('uploading') || this.model.get('uploaded'))
return;
if (!this.$imageInput.val()) {
this.model.set('uploadStatus', '');
return;
}
var extra = this.prep_upload();
for (var k in extra)
$('').attr('name', k).val(extra[k]
).appendTo(this.uploadForm);
this.uploadForm.prop('action', image_upload_url());
this.uploadForm.submit();
this.$iframe.load(function (event) {
if (!postForm)
return;
var doc = this.contentWindow || this.contentDocument;
if (!doc)
return;
try {
var error = $(doc.document || doc).text();
// if it's a real response, it'll postMessage to us,
// so we don't have to do anything.
if (/legitimate imager response/.test(error))
return;
// sanity check for weird browser responses
if (error.length < 5 || error.length > 100)
error = 'Unknown upload error.';
postForm.upload_error(error);
}
catch (e) {
// likely cross-origin restriction
// wait before erroring in case the message shows up
setTimeout(function () {
postForm.upload_finished_fallback();
}, 500);
}
});
this.notify_uploading();
},
on_toggle: function (event) {
var attrs = this.model.attributes;
if (!attrs.uploading && !attrs.uploaded) {
event.preventDefault();
event.stopImmediatePropagation();
if (attrs.spoiler) {
this.model.set({spoiler: 0});
return;
}
var pick = pick_spoiler(attrs.nextSpoiler);
this.model.set({spoiler: pick.index, nextSpoiler: pick.next});
}
},
render_spoiler_pane: function (model, sp) {
var img = sp ? spoiler_pane_url(sp) : mediaURL + 'css/ui/pane.png';
this.$toggle.css('background-image', 'url("' + img + '")');
},
render_floop: function (model, floop) {
this.$el.toggleClass('floop', floop);
},
});
menuHandlers.Flip = function () {
var floop = !window.lastFloop;
window.lastFloop = floop;
if (floop)
$('', {
id: 'floop-aside-right',
text: 'section.full.floop aside { margin: -26px 0 2px auto; }',
}).appendTo('head');
else
$('#floop-aside-right').remove();
if (postForm && !postForm.committed())
postForm.model.set('floop', floop);
};
menuHandlers.Vapor = function () {
vapor = -1;
flash_bg('#f98aa5');
if (postForm && /^\s*V+$/.test(postForm.$input.val()))
postForm.$input.val('');
};
oneeSama.hook('menuOptions', function (info) {
if (!info.model && info.mine && !postForm.committed()) {
var $sec = info.$button.closest('section.floop');
if ($sec.length || !THREAD) {
var i = info.options.indexOf('Focus');
if (i >= 0)
info.options.splice(i, 1);
info.options.unshift('Flip');
}
}
if (info.mine) {
var active = vapor < 0 || wombo < 0;
info.options.push(active ? 'Eject' : 'Vapor');
}
});
function image_upload_url() {
var url = imagerConfig.UPLOAD_URL || '../upload/';
return url + '?id=' + CONN_ID
}
dispatcher[IMAGE_STATUS] = function (msg) {
if (postForm)
postForm.dispatch(msg[0]);
};
window.addEventListener('message', function (event) {
var uploadOrigin = imagerConfig.UPLOAD_ORIGIN;
if (uploadOrigin && uploadOrigin != '*') {
if (event.origin && event.origin !== uploadOrigin)
return;
}
var msg = event.data;
if (msg == 'OK')
return;
else if (postForm)
postForm.upload_error(msg);
}, false);
function spoiler_pane_url(sp) {
return mediaURL + 'kana/spoil' + sp + '.png';
}
function preload_panes() {
var all = spoilerImages.normal.concat(spoilerImages.trans);
for (var i = 0; i < all.length; i++)
new Image().src = spoiler_pane_url(all[i]);
}
(function () {
var CV = ComposerView.prototype;
CV.finish_wrapped = _.wrap(CV.finish, with_dom);
})();