From c77e012a52f6a2f97debe0a0c18687b885cce4ff Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Mon, 18 Feb 2013 00:17:00 +0100 Subject: [PATCH] Add Keybinds. --- 4chan_x.user.js | 318 ++++++++++++++++++++++++++++++++++++++++---- src/config.coffee | 45 ++++--- src/features.coffee | 191 ++++++++++++++++++++++++++ src/main.coffee | 1 + src/qr.coffee | 3 +- 5 files changed, 509 insertions(+), 49 deletions(-) diff --git a/4chan_x.user.js b/4chan_x.user.js index cf6b7e2e6..e5971a661 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -20,7 +20,7 @@ // @icon data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAIALAAAAAAQABAAAAIxlI+pq+D9DAgUoFkPDlbs7lGiI2bSVnKglnJMOL6omczxVZK3dH/41AG6Lh7i6qUoAAA7 // ==/UserScript== -/* 4chan X Alpha - Version 3.0.0 - 2013-02-17 +/* 4chan X Alpha - Version 3.0.0 - 2013-02-18 * http://mayhemydg.github.com/4chan-x/ * * Copyright (c) 2009-2011 James Campos @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Nav, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, d, doc, g, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Keybinds, Main, Menu, Nav, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, d, doc, g, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; @@ -141,28 +141,29 @@ fileInfo: '%l (%p%s, %r)', favicon: 'ferongr', hotkeys: { - 'open QR': ['q', 'Open QR with post number inserted.'], - 'open empty QR': ['Q', 'Open QR without post number inserted.'], - 'open options': ['alt+o', 'Open Options.'], - 'close': ['Esc', 'Close Options or QR.'], - 'spoiler tags': ['ctrl+s', 'Insert spoiler tags.'], - 'code tags': ['alt+c', 'Insert code tags.'], - 'submit QR': ['alt+s', 'Submit post.'], - 'watch': ['w', 'Watch thread.'], - 'update': ['u', 'Update the thread now.'], - 'expand image': ['E', 'Expand selected image.'], - 'expand images': ['e', 'Expand all images.'], - 'front page': ['0', 'Jump to page 0.'], - 'next page': ['Right', 'Jump to the next page.'], - 'previous page': ['Left', 'Jump to the previous page.'], - 'next thread': ['Down', 'See next thread.'], - 'previous thread': ['Up', 'See previous thread.'], - 'expand thread': ['ctrl+e', 'Expand thread.'], - 'open thread': ['o', 'Open thread in current tab.'], - 'open thread tab': ['O', 'Open thread in new tab.'], - 'next reply': ['j', 'Select next reply.'], - 'previous reply': ['k', 'Select previous reply.'], - 'hide': ['x', 'Hide thread.'] + 'Open empty QR': ['q', 'Open QR without post number inserted.'], + 'Open QR': ['Shift+q', 'Open QR with post number inserted.'], + 'Open options': ['Alt+o', 'Open Options.'], + 'Close': ['Esc', 'Close Settings, Notifications or QR.'], + 'Spoiler tags': ['Ctrl+s', 'Insert spoiler tags.'], + 'Code tags': ['Alt+c', 'Insert code tags.'], + 'Submit QR': ['Alt+s', 'Submit post.'], + 'Watch': ['w', 'Watch thread.'], + 'Update': ['u', 'Update the thread now.'], + 'Expand image': ['Shift+e', 'Expand selected image.'], + 'Expand images': ['e', 'Expand all images.'], + 'Front page': ['0', 'Jump to page 0.'], + 'Open front page': ['Shift+0', 'Open page 0 in a new tab.'], + 'Next page': ['Right', 'Jump to the next page.'], + 'Previous page': ['Left', 'Jump to the previous page.'], + 'Next thread': ['Down', 'See next thread.'], + 'Previous thread': ['Up', 'See previous thread.'], + 'Expand thread': ['Ctrl+e', 'Expand thread.'], + 'Open thread': ['o', 'Open thread in current tab.'], + 'Open thread tab': ['Shift+o', 'Open thread in new tab.'], + 'Next reply': ['j', 'Select next reply.'], + 'Previous reply': ['k', 'Select previous reply.'], + 'Hide': ['x', 'Hide thread.'] }, updater: { checkbox: { @@ -2233,6 +2234,271 @@ } }; + Keybinds = { + init: function() { + if (g.VIEW === 'catalog' || !Conf['Keybinds']) { + return; + } + $.on(d, 'keydown', Keybinds.keydown); + return $.on(d, '4chanXInitFinished', function() { + var node, _i, _len, _ref, _results; + _ref = $$('[accesskey]'); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + _results.push(node.removeAttribute('accesskey')); + } + return _results; + }); + }, + keydown: function(e) { + var form, key, notification, notifications, target, thread, threadRoot, _i, _len, _ref; + if (!(key = Keybinds.keyCode(e))) { + return; + } + target = e.target; + if ((_ref = target.nodeName) === 'INPUT' || _ref === 'TEXTAREA') { + if (!((key === 'Esc') || (/(Alt|Ctrl|Meta)\+/.test(key)))) { + return; + } + } + threadRoot = Nav.getThread(); + thread = Get.postFromNode($('.op', threadRoot)).thread; + switch (key) { + case Conf['Open empty QR']: + Keybinds.qr(threadRoot); + break; + case Conf['Open QR']: + Keybinds.qr(threadRoot, true); + break; + case Conf['Open options']: + Settings.open(); + break; + case Conf['Close']: + if ($.id('settings')) { + Options.close(); + } else if ((notifications = $$('.notification')).length) { + for (_i = 0, _len = notifications.length; _i < _len; _i++) { + notification = notifications[_i]; + $('.close', notification).click(); + } + } else if (QR.el) { + QR.close(); + } + break; + case Conf['Spoiler tags']: + if (target.nodeName !== 'TEXTAREA') { + return; + } + Keybinds.tags('spoiler', target); + break; + case Conf['Code tags']: + if (target.nodeName !== 'TEXTAREA') { + return; + } + Keybinds.tags('code', target); + break; + case Conf['Submit QR']: + if (QR.el && !QR.status()) { + QR.submit(); + } + break; + case Conf['Watch']: + ThreadWatcher.toggle(thread); + break; + case Conf['Update']: + ThreadUpdater.update(); + break; + case Conf['Expand image']: + Keybinds.img(threadRoot); + break; + case Conf['Expand images']: + Keybinds.img(threadRoot, true); + break; + case Conf['Front page']: + window.location = "/" + g.BOARD + "/0#delform"; + break; + case Conf['Open front page']: + $.open(url("/" + g.BOARD + "/#delform")); + break; + case Conf['Next page']: + if (form = $('.next form')) { + window.location = form.action; + } + break; + case Conf['Previous page']: + if (form = $('.prev form')) { + window.location = form.action; + } + break; + case Conf['Next thread']: + if (g.VIEW === 'thread') { + return; + } + Nav.scroll(+1); + break; + case Conf['Previous thread']: + if (g.VIEW === 'thread') { + return; + } + Nav.scroll(-1); + break; + case Conf['Expand thread']: + ExpandThread.toggle(thread); + break; + case Conf['Open thread']: + Keybinds.open(thread); + break; + case Conf['Open thread tab']: + Keybinds.open(thread, true); + break; + case Conf['Next reply']: + Keybinds.hl(+1, threadRoot); + break; + case Conf['Previous reply']: + Keybinds.hl(-1, threadRoot); + break; + case Conf['Hide']: + ThreadHiding.toggle(thread); + break; + default: + return; + } + return e.preventDefault(); + }, + keyCode: function(e) { + var kc, key; + key = (function() { + switch (kc = e.keyCode) { + case 8: + return ''; + case 13: + return 'Enter'; + case 27: + return 'Esc'; + case 37: + return 'Left'; + case 38: + return 'Up'; + case 39: + return 'Right'; + case 40: + return 'Down'; + default: + if ((48 <= kc && kc <= 57) || (65 <= kc && kc <= 90)) { + return String.fromCharCode(kc).toLowerCase(); + } else { + return null; + } + } + })(); + if (key) { + if (e.altKey) { + key = 'Alt+' + key; + } + if (e.ctrlKey) { + key = 'Ctrl+' + key; + } + if (e.metaKey) { + key = 'Meta+' + key; + } + if (e.shiftKey) { + key = 'Shift+' + key; + } + } + return key; + }, + qr: function(thread, quote) { + if (!Conf['Quick Reply']) { + return; + } + QR.open(); + if (quote) { + QR.quote.call($('input', $('.post.highlight', thread) || thread)); + } + return $('textarea', QR.el).focus(); + }, + tags: function(tag, ta) { + var range, selEnd, selStart, value; + value = ta.value; + selStart = ta.selectionStart; + selEnd = ta.selectionEnd; + ta.value = value.slice(0, selStart) + ("[" + tag + "]") + value.slice(selStart, selEnd) + ("[/" + tag + "]") + value.slice(selEnd); + range = ("[" + tag + "]").length + selEnd; + ta.setSelectionRange(range, range); + return $.event('input', null, ta); + }, + img: function(thread, all) { + var input, post; + if (all) { + input = ImageExpand.expandAllInput; + input.checked = !input.checked; + return ImageExpand.cb.all.call(input); + } else { + post = Get.postFromNode($('.post.highlight', thread) || $('.op', thread)); + return ImageExpand.toggle(post); + } + }, + open: function(thread, tab) { + var url; + if (g.VIEW !== 'index') { + return; + } + url = "//boards.4chan.org/" + thread.board + "/res/" + thread; + if (tab) { + return $.open(url); + } else { + return location.href = url; + } + }, + hl: function(delta, thread) { + var headRect, next, postEl, rect, replies, reply, root, topMargin, _i, _len; + headRect = $.id('header-bar').getBoundingClientRect(); + topMargin = headRect.top + headRect.height; + if (postEl = $('.reply.highlight', thread)) { + $.rmClass(postEl, 'highlight'); + rect = postEl.getBoundingClientRect(); + if (rect.bottom >= topMargin && rect.top <= d.documentElement.clientHeight) { + root = postEl.parentNode; + next = $.x('child::div[contains(@class,"post reply")]', delta === +1 ? root.nextElementSibling : root.previousElementSibling); + if (!next) { + this.focus(postEl); + return; + } + if (!(g.VIEW === 'thread' || $.x('ancestor::div[parent::div[@class="board"]]', next) === thread)) { + return; + } + rect = next.getBoundingClientRect(); + if (rect.top < 0 || rect.bottom > d.documentElement.clientHeight) { + if (delta === -1) { + window.scrollBy(0, rect.top - topMargin); + } else { + next.scrollIntoView(false); + } + } + this.focus(next); + return; + } + } + replies = $$('.reply', thread); + if (delta === -1) { + replies.reverse(); + } + for (_i = 0, _len = replies.length; _i < _len; _i++) { + reply = replies[_i]; + rect = reply.getBoundingClientRect(); + if (delta === +1 && rect.top >= topMargin || delta === -1 && rect.bottom <= d.documentElement.clientHeight) { + this.focus(reply); + return; + } + } + }, + focus: function(post) { + $.addClass(post, 'highlight'); + return $('a[title="Highlight this post"]', post).focus(); + } + }; + Nav = { init: function() { var next, prev, span; @@ -3818,6 +4084,7 @@ switch (type) { case 'Expand all': $.on(input, 'change', ImageExpand.cb.all); + ImageExpand.expandAllInput = input; break; case 'Fit width': case 'Fit height': @@ -5044,7 +5311,7 @@ text = ""; sel = d.getSelection(); selectionRoot = $.x('ancestor::div[contains(@class,"postContainer")][1]', sel.anchorNode); - post = Get.postFromRoot($.x('ancestor::div[contains(@class,"postContainer")][1]', this)); + post = Get.postFromNode(this); thread = g.BOARD.posts[Get.contextFromLink(this).thread]; if ((s = sel.toString().trim()) && post.nodes.root === selectionRoot) { s = s.replace(/\n/g, '\n>'); @@ -6148,6 +6415,7 @@ initFeature('Thread Updater', ThreadUpdater); initFeature('Thread Watcher', ThreadWatcher); initFeature('Index Navigation', Nav); + initFeature('Keybinds', Keybinds); console.timeEnd('All initializations'); $.on(d, '4chanMainInit', Main.initStyle); return $.ready(Main.initReady); diff --git a/src/config.coffee b/src/config.coffee index 30c8dcfa1..49a571dae 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -130,33 +130,34 @@ Config = favicon: 'ferongr' hotkeys: # QR & Options - 'open QR': ['q', 'Open QR with post number inserted.'] - 'open empty QR': ['Q', 'Open QR without post number inserted.'] - 'open options': ['alt+o', 'Open Options.'] - 'close': ['Esc', 'Close Options or QR.'] - 'spoiler tags': ['ctrl+s', 'Insert spoiler tags.'] - 'code tags': ['alt+c', 'Insert code tags.'] - 'submit QR': ['alt+s', 'Submit post.'] + 'Open empty QR': ['q', 'Open QR without post number inserted.'] + 'Open QR': ['Shift+q', 'Open QR with post number inserted.'] + 'Open options': ['Alt+o', 'Open Options.'] + 'Close': ['Esc', 'Close Settings, Notifications or QR.'] + 'Spoiler tags': ['Ctrl+s', 'Insert spoiler tags.'] + 'Code tags': ['Alt+c', 'Insert code tags.'] + 'Submit QR': ['Alt+s', 'Submit post.'] # Thread related - 'watch': ['w', 'Watch thread.'] - 'update': ['u', 'Update the thread now.'] + 'Watch': ['w', 'Watch thread.'] + 'Update': ['u', 'Update the thread now.'] # Images - 'expand image': ['E', 'Expand selected image.'] - 'expand images': ['e', 'Expand all images.'] + 'Expand image': ['Shift+e', 'Expand selected image.'] + 'Expand images': ['e', 'Expand all images.'] # Board Navigation - 'front page': ['0', 'Jump to page 0.'] - 'next page': ['Right', 'Jump to the next page.'] - 'previous page': ['Left', 'Jump to the previous page.'] + 'Front page': ['0', 'Jump to page 0.'] + 'Open front page': ['Shift+0', 'Open page 0 in a new tab.'] + 'Next page': ['Right', 'Jump to the next page.'] + 'Previous page': ['Left', 'Jump to the previous page.'] # Thread Navigation - 'next thread': ['Down', 'See next thread.'] - 'previous thread': ['Up', 'See previous thread.'] - 'expand thread': ['ctrl+e', 'Expand thread.'] - 'open thread': ['o', 'Open thread in current tab.'] - 'open thread tab': ['O', 'Open thread in new tab.'] + 'Next thread': ['Down', 'See next thread.'] + 'Previous thread': ['Up', 'See previous thread.'] + 'Expand thread': ['Ctrl+e', 'Expand thread.'] + 'Open thread': ['o', 'Open thread in current tab.'] + 'Open thread tab': ['Shift+o', 'Open thread in new tab.'] # Reply Navigation - 'next reply': ['j', 'Select next reply.'] - 'previous reply': ['k', 'Select previous reply.'] - 'hide': ['x', 'Hide thread.'] + 'Next reply': ['j', 'Select next reply.'] + 'Previous reply': ['k', 'Select previous reply.'] + 'Hide': ['x', 'Hide thread.'] updater: checkbox: 'Beep': [false, 'Beep on new post to completely read thread.'] diff --git a/src/features.coffee b/src/features.coffee index b3bcb03ae..f3aa91c68 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -1025,6 +1025,196 @@ ArchiveLink = open: open } +Keybinds = + init: -> + return if g.VIEW is 'catalog' or !Conf['Keybinds'] + + $.on d, 'keydown', Keybinds.keydown + $.on d, '4chanXInitFinished', -> + for node in $$ '[accesskey]' + node.removeAttribute 'accesskey' + + keydown: (e) -> + return unless key = Keybinds.keyCode e + {target} = e + if target.nodeName in ['INPUT', 'TEXTAREA'] + return unless (key is 'Esc') or (/(Alt|Ctrl|Meta)\+/.test key) + + threadRoot = Nav.getThread() + thread = Get.postFromNode($('.op', threadRoot)).thread + switch key + # QR & Options + when Conf['Open empty QR'] + Keybinds.qr threadRoot + when Conf['Open QR'] + Keybinds.qr threadRoot, true + when Conf['Open options'] + Settings.open() + when Conf['Close'] + if $.id 'settings' + Options.close() + else if (notifications = $$ '.notification').length + for notification in notifications + $('.close', notification).click() + else if QR.el + QR.close() + when Conf['Spoiler tags'] + return if target.nodeName isnt 'TEXTAREA' + Keybinds.tags 'spoiler', target + when Conf['Code tags'] + return if target.nodeName isnt 'TEXTAREA' + Keybinds.tags 'code', target + when Conf['Submit QR'] + QR.submit() if QR.el and !QR.status() + # Thread related + when Conf['Watch'] + ThreadWatcher.toggle thread + when Conf['Update'] + ThreadUpdater.update() + # Images + when Conf['Expand image'] + Keybinds.img threadRoot + when Conf['Expand images'] + Keybinds.img threadRoot, true + # Board Navigation + when Conf['Front page'] + window.location = "/#{g.BOARD}/0#delform" + when Conf['Open front page'] + $.open url "/#{g.BOARD}/#delform" + when Conf['Next page'] + if form = $ '.next form' + window.location = form.action + when Conf['Previous page'] + if form = $ '.prev form' + window.location = form.action + # Thread Navigation + when Conf['Next thread'] + return if g.VIEW is 'thread' + Nav.scroll +1 + when Conf['Previous thread'] + return if g.VIEW is 'thread' + Nav.scroll -1 + when Conf['Expand thread'] + ExpandThread.toggle thread + when Conf['Open thread'] + Keybinds.open thread + when Conf['Open thread tab'] + Keybinds.open thread, true + # Reply Navigation + when Conf['Next reply'] + Keybinds.hl +1, threadRoot + when Conf['Previous reply'] + Keybinds.hl -1, threadRoot + when Conf['Hide'] + ThreadHiding.toggle thread + else + return + e.preventDefault() + + keyCode: (e) -> + key = switch kc = e.keyCode + when 8 # return + '' + when 13 + 'Enter' + when 27 + 'Esc' + when 37 + 'Left' + when 38 + 'Up' + when 39 + 'Right' + when 40 + 'Down' + else + if 48 <= kc <= 57 or 65 <= kc <= 90 # 0-9, A-Z + String.fromCharCode(kc).toLowerCase() + else + null + if key + if e.altKey then key = 'Alt+' + key + if e.ctrlKey then key = 'Ctrl+' + key + if e.metaKey then key = 'Meta+' + key + if e.shiftKey then key = 'Shift+' + key + key + + qr: (thread, quote) -> + return unless Conf['Quick Reply'] + QR.open() + if quote + QR.quote.call $ 'input', $('.post.highlight', thread) or thread + $('textarea', QR.el).focus() + + tags: (tag, ta) -> + value = ta.value + selStart = ta.selectionStart + selEnd = ta.selectionEnd + + ta.value = + value[...selStart] + + "[#{tag}]" + value[selStart...selEnd] + "[/#{tag}]" + + value[selEnd..] + + # Move the caret to the end of the selection. + range = "[#{tag}]".length + selEnd + ta.setSelectionRange range, range + + # Fire the 'input' event + $.event 'input', null, ta + + img: (thread, all) -> + if all + input = ImageExpand.expandAllInput + input.checked = !input.checked + ImageExpand.cb.all.call input + else + post = Get.postFromNode $('.post.highlight', thread) or $ '.op', thread + ImageExpand.toggle post + + open: (thread, tab) -> + return if g.VIEW isnt 'index' + url = "//boards.4chan.org/#{thread.board}/res/#{thread}" + if tab + $.open url + else + location.href = url + + hl: (delta, thread) -> + headRect = $.id('header-bar').getBoundingClientRect() + topMargin = headRect.top + headRect.height + if postEl = $ '.reply.highlight', thread + $.rmClass postEl, 'highlight' + rect = postEl.getBoundingClientRect() + if rect.bottom >= topMargin and rect.top <= d.documentElement.clientHeight # We're at least partially visible + root = postEl.parentNode + next = $.x 'child::div[contains(@class,"post reply")]', + if delta is +1 then root.nextElementSibling else root.previousElementSibling + unless next + @focus postEl + return + return unless g.VIEW is 'thread' or $.x('ancestor::div[parent::div[@class="board"]]', next) is thread + rect = next.getBoundingClientRect() + if rect.top < 0 or rect.bottom > d.documentElement.clientHeight + if delta is -1 + window.scrollBy 0, rect.top - topMargin + else + next.scrollIntoView false + @focus next + return + + replies = $$ '.reply', thread + replies.reverse() if delta is -1 + for reply in replies + rect = reply.getBoundingClientRect() + if delta is +1 and rect.top >= topMargin or delta is -1 and rect.bottom <= d.documentElement.clientHeight + @focus reply + return + + focus: (post) -> + $.addClass post, 'highlight' + $('a[title="Highlight this post"]', post).focus() + Nav = init: -> return if g.VIEW isnt 'index' or !Conf['Index Navigation'] @@ -2388,6 +2578,7 @@ ImageExpand = switch type when 'Expand all' $.on input, 'change', ImageExpand.cb.all + ImageExpand.expandAllInput = input when 'Fit width', 'Fit height' $.on input, 'change', ImageExpand.cb.setFitness if config diff --git a/src/main.coffee b/src/main.coffee index f55c2dd29..6b15618be 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -345,6 +345,7 @@ Main = initFeature 'Thread Updater', ThreadUpdater initFeature 'Thread Watcher', ThreadWatcher initFeature 'Index Navigation', Nav + initFeature 'Keybinds', Keybinds console.timeEnd 'All initializations' $.on d, '4chanMainInit', Main.initStyle diff --git a/src/qr.coffee b/src/qr.coffee index b82a9529a..2779f77ce 100644 --- a/src/qr.coffee +++ b/src/qr.coffee @@ -231,7 +231,7 @@ QR = sel = d.getSelection() selectionRoot = $.x 'ancestor::div[contains(@class,"postContainer")][1]', sel.anchorNode - post = Get.postFromRoot $.x 'ancestor::div[contains(@class,"postContainer")][1]', @ + post = Get.postFromNode @ thread = g.BOARD.posts[Get.contextFromLink(@).thread] if (s = sel.toString().trim()) and post.nodes.root is selectionRoot @@ -249,7 +249,6 @@ QR = ta = $ 'textarea', QR.el if QR.threadSelector and !ta.value and g.BOARD.ID isnt 'f' QR.threadSelector.value = thread.ID - # Make sure we get the correct number, even with XXX censors caretPos = ta.selectionStart # Replace selection for text.