· 9 years ago · Apr 01, 2017, 06:06 PM
1// ==UserScript==
2// @name better_better_booru
3// @namespace https://greasyfork.org/scripts/3575-better-better-booru
4// @author otani, modified by Jawertae, A Pseudonymous Coder & Moebius Strip.
5// @description Several changes to make Danbooru much better. Including the viewing of hidden/censored images on non-upgraded accounts and more.
6// @version 7.2.5
7// @updateURL https://greasyfork.org/scripts/3575-better-better-booru/code/better_better_booru.meta.js
8// @downloadURL https://greasyfork.org/scripts/3575-better-better-booru/code/better_better_booru.user.js
9// @match *://*.donmai.us/*
10// @run-at document-end
11// @grant none
12// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAAA9klEQVQ4y2NgGBgQu/Dau1/Pt/rhVPAfCkpwKXhUZ8Al2vT//yu89vDjV8AkP/P//zY0K//+eHVmoi5YyB7I/VDGiKYADP60wRT8P6aKTcH//0lgQcHS//+PYFdwFu7Ib8gKGBgYOQ22glhfGO7mqbEpzv///xyqAiAQAbGewIz8aoehQArEWsyQsu7O549XJiowoCpg4rM9CGS8V8UZ9GBwy5wBr4K/teL4Ffz//8mHgIL/v82wKgA6kkXE+zKIuRaHAhDQATFf4lHABmL+xKPAFhKUOBQwSyU+AzFXEvDFf3sCCnrxh8O3Ujwh+fXZvjoZ+udTAERqR5IgKEBRAAAAAElFTkSuQmCC
13// ==/UserScript==
14
15// Have a nice day. - A Pseudonymous Coder
16
17function bbbScript() { // This is needed to make this script work in Chrome.
18 /*
19 * NOTE: You no longer need to edit this script to change settings!
20 * Use the "BBB Settings" button in the menu instead.
21 */
22
23 // If Danbooru's JS isn't available, assume we're somewhere this script isn't needed and stop.
24 if (typeof(Danbooru) === "undefined")
25 return;
26
27 /* Helper Prototypes */
28 // Don't get hoisted so they should be declared at the top to simplify things.
29 String.prototype.bbbSpacePad = function() {
30 // Add a leading and trailing space.
31 return (this.length ? " " + this + " " : "");
32 };
33
34 String.prototype.bbbSpaceClean = function() {
35 // Remove leading, trailing, and multiple spaces.
36 return this.replace(/\s+/g, " ").replace(/^\s|\s$/g, "");
37 };
38
39 String.prototype.bbbTagClean = function() {
40 // Remove extra commas along with leading, trailing, and multiple spaces.
41 return this.replace(/[\s,]*(%\))\s*|\s*([~-]*\(%)[\s,]*/g, " $& ").replace(/[\s,]*,[\s,]*/g, ", ").replace(/[\s,]+$|^[\s,]+/g, "").replace(/\s+/g, " ");
42 };
43
44 String.prototype.bbbHash = function() {
45 // Turn a string into a hash using the current Danbooru hash method.
46 var hash = 5381;
47 var i = this.length;
48
49 while(i)
50 hash = (hash * 33) ^ this.charCodeAt(--i);
51
52 return hash >>> 0;
53 };
54
55 Element.prototype.bbbGetPadding = function() {
56 // Get all the padding measurements of an element including the total width and height.
57 if (window.getComputedStyle) {
58 var computed = window.getComputedStyle(this, null);
59 var paddingLeft = parseFloat(computed.paddingLeft);
60 var paddingRight = parseFloat(computed.paddingRight);
61 var paddingTop = parseFloat(computed.paddingTop);
62 var paddingBottom = parseFloat(computed.paddingBottom);
63 var paddingHeight = paddingTop + paddingBottom;
64 var paddingWidth = paddingLeft + paddingRight;
65 return {width: paddingWidth, height: paddingHeight, top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight};
66 }
67 };
68
69 Element.prototype.bbbHasClass = function() {
70 // Test an element for one or more collections of classes.
71 var classList = this.classList;
72
73 for (var i = 0, il = arguments.length; i < il; i++) {
74 var classes = arguments[i].bbbSpaceClean();
75
76 if (!classes)
77 continue;
78
79 var classArray = classes.split(" ");
80 var hasClass = true;
81
82 for (var j = 0, jl = classArray.length; j < jl; j++) {
83 if (!classList.contains(classArray[j])) {
84 hasClass = false;
85 break;
86 }
87 }
88
89 if (hasClass)
90 return true;
91 }
92
93 return false;
94 };
95
96 Element.prototype.bbbAddClass = function(classString) {
97 // Add one or more classes to an element.
98 var classes = classString.bbbSpaceClean();
99
100 if (!classes)
101 return;
102
103 var classList = this.classList;
104 var classArray = classes.split(" ");
105
106 for (var i = 0, il = classArray.length; i < il; i++)
107 classList.add(classArray[i]);
108 };
109
110 Element.prototype.bbbRemoveClass = function(classString) {
111 // Remove one or more classes from an element.
112 var classes = classString.bbbSpaceClean();
113
114 if (!classes)
115 return;
116
117 var classList = this.classList;
118 var classArray = classes.split(" ");
119
120 for (var i = 0, il = classArray.length; i < il; i++)
121 classList.remove(classArray[i]);
122 };
123
124 Element.prototype.bbbWatchNodes = function(func) {
125 // Watch for new nodes.
126 var observer = window.MutationObserver || window.WebKitMutationObserver;
127
128 if (observer) {
129 observer = new observer(func);
130 observer.observe(this, {childList: true, subtree: true});
131 }
132 else
133 this.addEventListener("DOMNodeInserted", func, false);
134 };
135
136 Element.prototype.bbbOverrideClick = function(func) {
137 // Override Danbooru's click event listeners by capturing clicks on the parent node and stopping them.
138 var target = this;
139
140 var wrapperFunc = function(event) {
141 if (event.target !== target || event.button !== 0)
142 return;
143
144 func(event);
145 event.stopPropagation();
146 };
147
148 target.parentNode.addEventListener("click", wrapperFunc, true);
149 };
150
151 Storage.prototype.bbbSetItem = function(key, value) {
152 // Store a value in storage and warn if it is full.
153 try {
154 this.setItem(key, value);
155 }
156 catch (error) {
157 if (error.code === 22 || error.code === 1014) {
158 if (this === localStorage) {
159 if (!bbb.flags.local_storage_full) {
160 if (localStorage.length > 2000) {
161 // Try clearing out autocomplete if that appears to be the problem.
162 cleanLocalStorage("autocomplete");
163
164 try {
165 localStorage.setItem(key, value);
166 }
167 catch (localError) {
168 bbb.flags.local_storage_full = true;
169 }
170 }
171 else
172 bbb.flags.local_storage_full = true;
173
174 // Store the local storage value until it can be retried.
175 if (bbb.flags.local_storage_full) {
176 bbb.local_storage_queue = {};
177 bbb.local_storage_queue[key] = value;
178 localStorageDialog();
179 }
180 }
181 else {
182 // Temporarily store additional local storage values until they can be retried.
183 if (sessionStorage.getItem("bbb_local_storage_queue")) {
184 var sessLocal = JSON.parse(sessionStorage.getItem("bbb_local_storage_queue"));
185
186 sessLocal[key] = value;
187 sessionStorage.bbbSetItem("bbb_local_storage_queue", JSON.stringify(sessLocal));
188 }
189 else
190 bbb.local_storage_queue[key] = value;
191 }
192 }
193 else {
194 // Keep only a few values in session storage.
195 for (var i = sessionStorage.length - 1; i >= 0; i--) {
196 var keyName = sessionStorage.key(i);
197
198 if (keyName !== "bbb_endless_default" && keyName !== "bbb_quick_search")
199 sessionStorage.removeItem(keyName);
200 }
201
202 try {
203 sessionStorage.setItem(key, value);
204 }
205 catch (sessionError) {
206 bbbNotice("Your settings/data could not be saved/updated. The browser's session storage is full.", -1);
207 }
208 }
209 }
210 else
211 bbbNotice("Unexpected error while attempting to save/update settings. (Error: " + error.message + ")", -1);
212 }
213 };
214
215 /* Global Variables */
216 var bbb = { // Container for script info.
217 blacklist: {
218 entries: [],
219 match_list: {},
220 smart_view_target: undefined
221 },
222 cache: { // Thumbnail info cache.
223 current: {
224 history: [],
225 names: {}
226 },
227 save_enabled: false,
228 stored: {}
229 },
230 custom_tag: {
231 searches: [],
232 style_list: {}
233 },
234 dialog: {
235 queue: []
236 },
237 drag_scroll: {
238 lastX: undefined,
239 lastY: undefined,
240 moved: false,
241 target: undefined
242 },
243 el: { // Script elements.
244 menu: {} // Menu elements.
245 },
246 endless: {
247 append_page: false,
248 enabled: false,
249 fill_first_page: false,
250 last_paginator: undefined,
251 new_paginator: undefined,
252 no_thumb_count: 0,
253 pages: [],
254 paused: false,
255 posts: {}
256 },
257 fixed_paginator_space: 0,
258 fixed_sidebar: {
259 content: undefined,
260 left: undefined,
261 sidebar: undefined,
262 top: undefined
263 },
264 flags: {},
265 hotkeys: {
266 other: { // Hotkeys for misc locations.
267 66: {func: openMenu} // B
268 },
269 post: { // Post hotkeys.
270 49: {func: resizeHotkey, custom_handler: true}, // 1
271 50: {func: resizeHotkey, custom_handler: true}, // 2
272 51: {func: resizeHotkey, custom_handler: true}, // 3
273 52: {func: resizeHotkey, custom_handler: true}, // 4
274 66: {func: openMenu}, // B
275 86: {func: swapPost} // V
276 }
277 },
278 post: { // Post content info and status.
279 info: {}, // Post information object.
280 resize: {
281 mode: "none",
282 ratio: 1
283 },
284 swapped: false // Whether the post content has been changed between the original and sample versions.
285 },
286 options: { // Setting options and data.
287 bbb_version: "7.2.5",
288 alternate_image_swap: newOption("checkbox", false, "Alternate Image Swap", "Switch between the sample and original image by clicking the image. <tiphead>Note</tiphead>Notes can be toggled by using the link in the sidebar options section."),
289 arrow_nav: newOption("checkbox", false, "Arrow Navigation", "Allow the use of the left and right arrow keys to navigate pages. <tiphead>Note</tiphead>This option has no effect on individual posts."),
290 autohide_sidebar: newOption("dropdown", "none", "Auto-hide Sidebar", "Hide the sidebar for posts, favorites listings, and/or searches until the mouse comes close to the left side of the window or the sidebar gains focus.<tiphead>Tips</tiphead>By using Danbooru's hotkey for the letter \"Q\" to place focus on the search box, you can unhide the sidebar.<br><br>Use the \"thumbnail count\" option to get the most out of this feature on search listings.", {txtOptions:["Disabled:none", "Favorites:favorites", "Posts:post", "Searches:search", "Favorites & Posts:favorites post", "Favorites & Searches:favorites search", "Posts & Searches:post search", "All:favorites post search"]}),
291 autoscroll_post: newOption("dropdown", "none", "Auto-scroll Post", "Automatically scroll a post to a particular point. <tipdesc>Below Header:</tipdesc> Scroll the window down until the header is no longer visible or scrolling is no longer possible. <tipdesc>Post Content:</tipdesc> Position the post content as close as possible to the left and top edges of the window viewport when initially loading a post. Using this option will also scroll past any notices above the content.", {txtOptions:["Disabled:none", "Below Header:header", "Post Content:post"]}),
292 blacklist_add_bars: newOption("checkbox", false, "Additional Bars", "Add a blacklist bar to the comment search listing and individually linked comments so that blacklist entries can be toggled as needed."),
293 blacklist_highlight_color: newOption("text", "#CCCCCC", "Highlight Color", "When using highlighting for \"thumbnail marking\", you may set the color here. <tiphead>Notes</tiphead>Leaving this field blank will result in the default color being used. <br><br>For easy color selection, use one of the many free tools on the internet like <a target=\"_blank\" href=\"http://www.quackit.com/css/css_color_codes.cfm\">this one</a>. Hex RGB color codes (#000000, #FFFFFF, etc.) are the recommended values."),
294 blacklist_thumb_controls: newOption("checkbox", false, "Thumbnail Controls", "Allow control over individual blacklisted thumbnails and access to blacklist toggle links from blacklisted thumbnails. <tiphead>Directions</tiphead>For blacklisted thumbnails that have been revealed, hovering over them will reveal a clickable \"X\" icon that can hide them again. <br><br>If using \"hidden\" or \"replaced\" for the \"post display\" option, clicking on the area of a blacklisted thumbnail will pop up a menu that displays what blacklist entries it matches. Clicking the thumbnail area a second time while that menu is open will reveal that single thumbnail. <br><br>The menu that pops up on the first click also allows for toggling any listed blacklist entry for the entire page and navigating to the post without revealing its thumbnail. <tiphead>Note</tiphead>Toggling blacklist entries will have no effect on posts that have been changed via their individual controls."),
295 blacklist_post_display: newOption("dropdown", "disabled", "Post Display", "Set how the display of blacklisted posts in thumbnail listings and the comments section is handled. <tipdesc>Removed:</tipdesc> Posts and the space they take up are completely removed. <tipdesc>Hidden:</tipdesc> Post space is preserved, but thumbnails are hidden. <tipdesc>Replaced:</tipdesc> Thumbnails are replaced by \"blacklisted\" thumbnail placeholders.", {txtOptions:["Disabled:disabled", "Removed:removed", "Hidden:hidden", "Replaced:replaced"]}),
296 blacklist_smart_view: newOption("checkbox", false, "Smart View", "When navigating to a blacklisted post by using its thumbnail, if the thumbnail has already been revealed, the post content will temporarily be exempt from any blacklist checks for 1 minute and be immediately visible. <tiphead>Note</tiphead>Thumbnails in the parent/child notices of posts with exempt content will still be affected by the blacklist."),
297 blacklist_session_toggle: newOption("checkbox", false, "Session Toggle", "When toggling an individual blacklist entry on and off, the mode it's toggled to will persist across other pages in the same browsing session until it ends.<tiphead>Note</tiphead>For blacklists with many entries, this option can cause unexpected behavior (ex: getting logged out) if too many entries are toggled off at the same time."),
298 blacklist_thumb_mark: newOption("dropdown", "none", "Thumbnail Marking", "Mark the thumbnails of blacklisted posts that have been revealed to make them easier to distinguish from other thumbnails. <tipdesc>Highlight:</tipdesc> Change the background color of blacklisted thumbnails. <tipdesc>Icon Overlay:</tipdesc> Add an icon to the lower right corner of blacklisted thumbnails.", {txtOptions:["Disabled:none", "Highlight:highlight", "Icon Overlay:icon"]}),
299 border_spacing: newOption("dropdown", 0, "Border Spacing", "Set the amount of blank space between a border and thumbnail and between a custom tag border and status border. <tiphead>Note</tiphead>Even when set to 0, status borders and custom tag borders will always have a minimum value of 1 between them. <tiphead>Tip</tiphead>Use this option if you often have trouble distinguishing a border from the thumbnail image.", {txtOptions:["0 (Default):0", "1:1", "2:2", "3:3"]}),
300 border_width: newOption("dropdown", 2, "Border Width", "Set the width of thumbnail borders.", {txtOptions:["1:1", "2 (Default):2", "3:3", "4:4", "5:5"]}),
301 bypass_api: newOption("checkbox", false, "Automatic API Bypass", "When logged out and API only features are enabled, do not warn about needing to be logged in. Instead, automatically bypass those features."),
302 clean_links: newOption("checkbox", false, "Clean Links", "Remove the extra information after the post ID in thumbnail links.<tiphead>Note</tiphead>Enabling this option will disable Danbooru's search navigation and active pool/favorite group detection for posts."),
303 collapse_sidebar: newOption("checkbox", false, "Collapsible Sidebar", "Allow sections in the sidebar to be expanded and collapsed via clicking their header titles.<tiphead>Note</tiphead>Sections can be set to default to expanded or collapsed by right clicking their titles."),
304 comment_score: newOption("checkbox", false, "Comment Scores", "Make comment scores visible by adding them as direct links to their respective comments."),
305 custom_status_borders: newOption("checkbox", false, "Custom Status Borders", "Override Danbooru's thumbnail borders for deleted, flagged, pending, parent, and child images."),
306 custom_tag_borders: newOption("checkbox", true, "Custom Tag Borders", "Add thumbnail borders to posts with specific tags."),
307 direct_downloads: newOption("checkbox", false, "Direct Downloads", "Allow download managers to download the posts displayed in the favorites, search, pool, popular, and favorite group listings. <tiphead>Note</tiphead>Posts filtered out by the blacklist or quick search will not provide direct downloads until the blacklist entry or quick search affecting them is disabled."),
308 disable_embedded_notes: newOption("checkbox", false, "Disable Embedded Notes", "Force posts with embedded notes to display with the original note styling. <tiphead>Notes</tiphead>While notes will display with the original styling, the actual post settings will still have embedded notes set to enabled. <br><br>Due to the actual settings, users that may wish to edit notes will have to edit the notes with the embedded note styling so that nothing ends up breaking in unexpected ways. When toggling translation mode or opening the edit note dialog box, the notes will automatically revert back to the original embedded notes until the page is reloaded. <br><br>Note resizing and moving will be allowed without the reversion to embedded notes since this ability is sometimes necessary for badly positioned notes. Any note resizing or moving done as a part of intended note editing should be done <b>after</b> triggering the embedded note reversion since any changes before it will be lost."),
309 enable_status_message: newOption("checkbox", true, "Enable Status Message", "When requesting information from Danbooru, display the request status in the lower right corner."),
310 endless_default: newOption("dropdown", "disabled", "Default", "Enable endless pages on the favorites, search, pool, notes, and favorite group listings. <tipdesc>Off:</tipdesc> Start up with all features off. <tipdesc>On:</tipdesc> Start up with all features on.<tipdesc>Paused:</tipdesc> Start up with all features on, but do not append new pages until the \"load more\" button is clicked. <tiphead>Note</tiphead>When not set to disabled, endless pages can be toggled between off and on/paused by using the \"E\" hotkey or the \"endless\" link next to the \"listing\" link in the page submenu. <tiphead>Tip</tiphead>The \"new tab/window\" and \"fixed paginator\" options can provide additional customization for endless pages.", {txtOptions:["Disabled:disabled", "Off:off", "On:on", "Paused:paused"]}),
311 endless_fill: newOption("checkbox", false, "Fill Pages", "When appending pages with missing thumbnails caused by hidden posts or removed duplicate posts, retrieve thumbnails from the following pages and add them to the new page until the desired number of thumbnails is reached. <tiphead>Note</tiphead>If using page separators, the displayed page number for appended pages composed of thumbnails from multiple Danbooru pages will be replaced by a range consisting of the first and last pages from which thumbnails were retrieved."),
312 endless_pause_interval: newOption("dropdown", 0, "Pause Interval", "Pause endless pages each time the number of pages reaches a multiple of the selected amount.", {txtOptions:["Disabled:0"], numRange:[1,100]}),
313 endless_preload: newOption("checkbox", false, "Preload Next Page", "Start loading the next page as soon as possible.<tiphead>Note</tiphead>A preloaded page will not be appended until the scroll limit is reached."),
314 endless_remove_dup: newOption("checkbox", false, "Remove Duplicates", "When appending new pages, remove posts that already exist in the listing from the new page.<tiphead>Note</tiphead>Duplicate posts are caused by the addition of new posts to the beginning of a listing or changes to the order of the posts."),
315 endless_scroll_limit: newOption("dropdown", 500, "Scroll Limit", "Set the minimum amount of pixels that the window can have left to vertically scroll before it starts appending the next page.", {numList:[0,50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000,1050,1100,1150,1200,1250,1300,1350,1400,1450,1500]}),
316 endless_separator: newOption("dropdown", "divider", "Page Separator", "Distinguish pages from each other by marking them with a separator.<tipdesc>Marker:</tipdesc> Place a thumbnail sized marker before the first thumbnail of each page.<tipdesc>Divider:</tipdesc> Completely separate pages by placing a horizontal line between them.", {txtOptions:["None:none", "Marker:marker", "Divider:divider"]}),
317 endless_session_toggle: newOption("checkbox", false, "Session Toggle", "When toggling endless pages on and off, the mode it's toggled to will override the default and persist across other pages in the same browsing session for that tab until it ends."),
318 fixed_paginator: newOption("dropdown", "disabled", "Fixed Paginator", "Make the paginator always visible for the favorites, search, pool, notes, and favorite group listings by fixing it to the bottom of the window when it would normally start scrolling out of view. <tipdesc>Endless:</tipdesc> Only change the paginator during endless pages browsing. <tipdesc>Normal:</tipdesc> Only change the paginator during normal browsing. <tipdesc>Always:</tipdesc> Change the paginator during normal and endless pages browsing. <tiphead>Note</tiphead>Options labeled with \"minimal\" will also make the fixed paginator smaller by removing most of the blank space within it.", {txtOptions:["Disabled:disabled", "Endless:endless", "Endless (Minimal):endless minimal", "Normal:normal", "Normal (Minimal):normal minimal", "Always:endless normal", "Always (Minimal):endless normal minimal"]}),
319 fixed_sidebar: newOption("dropdown", "none", "Fixed Sidebar", "Make the sidebar never completely vertically scroll out of view for posts, favorites listings, and/or searches by fixing it to the top or bottom of the window when it would normally start scrolling out of view. <tiphead>Note</tiphead>The \"auto-hide sidebar\" option will override this option if both try to modify the same page. <tiphead>Tip</tiphead>Depending on the available height in the browser window and the Danbooru location being modified, the \"tag scrollbars\", \"collapsible sidebar\", and/or \"remove tag headers\" options may be needed for best results.", {txtOptions:["Disabled:none", "Favorites:favorites", "Posts:post", "Searches:search", "Favorites & Posts:favorites post", "Favorites & Searches:favorites search", "Posts & Searches:post search", "All:favorites post search"]}),
320 hide_ban_notice: newOption("checkbox", false, "Hide Ban Notice", "Hide the Danbooru ban notice."),
321 hide_comment_notice: newOption("checkbox", false, "Hide Comment Guide Notice", "Hide the Danbooru comment guide notice."),
322 hide_pool_notice: newOption("checkbox", false, "Hide Pool Guide Notice", "Hide the Danbooru pool guide notice."),
323 hide_sign_up_notice: newOption("checkbox", false, "Hide Sign Up Notice", "Hide the Danbooru account sign up notice."),
324 hide_tag_notice: newOption("checkbox", false, "Hide Tag Guide Notice", "Hide the Danbooru tag guide notice."),
325 hide_tos_notice: newOption("checkbox", false, "Hide TOS Notice", "Hide the Danbooru terms of service agreement notice."),
326 hide_upgrade_notice: newOption("checkbox", false, "Hide Upgrade Notice", "Hide the Danbooru upgrade account notice."),
327 hide_upload_notice: newOption("checkbox", false, "Hide Upload Guide Notice", "Hide the Danbooru upload guide notice."),
328 image_swap_mode: newOption("dropdown", "load", "Image Swap Mode", "Set how swapping between the sample and original image is done.<tipdesc>Load First:</tipdesc> Display the image being swapped in after it has finished downloading. <tipdesc>View While Loading:</tipdesc> Immediately display the image being swapped in while it is downloading.", {txtOptions:["Load First (Default):load", "View While Loading:view"]}),
329 search_tag_scrollbars: newOption("dropdown", 0, "Search Tag Scrollbars", "Limit the length of the sidebar tag list for the search listing by restricting it to a set height in pixels. When the list exceeds the set height, a scrollbar will be added to allow the rest of the list to be viewed.", {txtOptions:["Disabled:0"], numList:[50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000,1050,1100,1150,1200,1250,1300,1350,1400,1450,1500]}),
330 load_sample_first: newOption("checkbox", true, "Load Sample First", "Load sample images first when viewing a post.<tiphead>Note</tiphead>When logged in, the account's \"default image width\" setting will override this option. This behavior can be changed with the \"override sample setting\" option under the preferences tab."),
331 manage_cookies: newOption("checkbox", false, "Manage Notice Cookies", "When using the \"hide upgrade notice\", \"hide sign up notice\", and/or \"hide TOS notice\" options, also create cookies to disable these notices at the server level.<tiphead>Tip</tiphead>Use this feature if the notices keep flashing on your screen before being removed."),
332 minimize_status_notices: newOption("checkbox", false, "Minimize Status Notices", "Hide the Danbooru deleted, banned, flagged, appealed, and pending notices. When you want to see a hidden notice, you can click the appropriate status link in the information section of the sidebar."),
333 move_save_search: newOption("checkbox", false, "Move Save Search", "Move the \"save this search\" button into the related section in the sidebar."),
334 override_blacklist: newOption("dropdown", "logged_out", "Override Blacklist", "Allow the \"blacklist\" setting to override the default blacklist for logged out users and/or account blacklist for logged in users. <tipdesc>Logged out:</tipdesc> Override the default blacklist for logged out users. <tipdesc>Always:</tipdesc> Override the default blacklist for logged out users and account blacklist for logged in users.", {txtOptions:["Disabled:disabled", "Logged out:logged_out", "Always:always"]}),
335 override_resize: newOption("checkbox", false, "Override Resize Setting", "Allow the \"resize post\" setting to override the account \"fit images to window\" setting when logged in."),
336 override_sample: newOption("checkbox", false, "Override Sample Setting", "Allow the \"load sample first\" setting to override the account \"default image width\" setting when logged in. <tiphead>Note</tiphead>When using this option, your Danbooru account settings should have \"default image width\" set to the corresponding value of the \"load sample first\" script setting. Not doing so will cause your browser to always download both the sample and original image. If you often change the \"load sample first\" setting, leaving your account to always load the sample/850px image first is your best option."),
337 page_counter: newOption("checkbox", false, "Page Counter", "Add a page counter and \"go to page #\" input field near the top of listing pages. <tiphead>Note</tiphead>The total number of pages will not be displayed if the pages are using the \"previous & next\" paging system or the total number of pages exceeds the maximum amount allowed by your user account level."),
338 post_drag_scroll: newOption("checkbox", false, "Post Drag Scrolling", "While holding down left click on a post's content, mouse movement can be used to scroll the whole page and reposition the content.<tiphead>Note</tiphead>This option is automatically disabled when translation mode is active."),
339 post_link_new_window: newOption("dropdown", "none", "New Tab/Window", "Force post links in the search, pool, popular, favorites, notes, and favorite group listings to open in a new tab/window. <tipdesc>Endless:</tipdesc> Only use new tabs/windows during endless pages browsing. <tipdesc>Normal:</tipdesc> Only use new tabs/windows during normal browsing. <tipdesc>Always:</tipdesc> Use new tabs/windows during normal and endless pages browsing. <tiphead>Notes</tiphead>When this option is active, holding down the control and shift keys while clicking a post link will open the post in the current tab/window.<br><br>Whether the post opens in a new tab or a new window depends upon your browser configuration. <tiphead>Tip</tiphead>This option can be useful as a safeguard to keep accidental left clicks from disrupting endless pages.", {txtOptions:["Disabled:disabled", "Endless:endless", "Normal:normal", "Always:endless normal"]}),
340 post_resize: newOption("checkbox", true, "Resize Post", "Shrink large post content to fit the browser window when initially loading a post.<tiphead>Note</tiphead>When logged in, the account's \"fit images to window\" setting will override this option. This behavior can be changed with the \"override resize setting\" option under the preferences tab."),
341 post_resize_mode: newOption("dropdown", "width", "Resize Mode", "Choose how to shrink large post content to fit the browser window when initially loading a post.", {txtOptions:["Width (Default):width", "Height:height", "Width & Height:all"]}),
342 post_tag_scrollbars: newOption("dropdown", 0, "Post Tag Scrollbars", "Limit the length of the sidebar tag lists for posts by restricting them to a set height in pixels. For lists that exceed the set height, a scrollbar will be added to allow the rest of the list to be viewed.<tiphead>Note</tiphead>When using \"remove tag headers\", this option will limit the overall length of the combined list.", {txtOptions:["Disabled:0"], numList:[50,100,150,200,250,300,350,400,450,500,550,600,650,700,750,800,850,900,950,1000,1050,1100,1150,1200,1250,1300,1350,1400,1450,1500]}),
343 post_tag_titles: newOption("checkbox", false, "Post Tag Titles", "Change the page titles for posts to a full list of the post tags."),
344 quick_search: newOption("dropdown", "disabled", "Quick Search", "Add a new search box to the upper right corner of the window viewport that allows searching through the current thumbnails for specific posts. <tipdesc>Fade:</tipdesc> Fade all posts that don't match in the thumbnail listing. <tipdesc>Remove:</tipdesc> Remove all posts that don't match from the thumbnail listing. <tiphead>Directions</tiphead>Please read the \"thumbnail matching rules\" section under the help tab for information about creating searches. <br><br>The search starts minimized in the upper right corner. Left clicking the main icon will open and close the search. Right clicking the main icon will completely reset the search. Holding down shift while left clicking the main icon will toggle an active search's pinned status. <br><br>While open, the search can be entered/updated in the search box and the pinned status can be toggled by clicking the pushpin icon. If no changes are made to an active search, submitting it a second time will reset the quick search. <tiphead>Notes</tiphead>Options labeled with \"pinned\" will make searches default to being pinned. <br><br>A pinned search will persist across other pages in the same browsing session for that tab until it ends or the search is unpinned. <br><br>When not set to disabled, the quick search can be opened by using the \"F\" hotkey. Additionally, an active search can be reset by using \"Shift + F\". Pressing \"Escape\" while the quick search is open will close it.", {txtOptions:["Disabled:disabled", "Fade:fade", "Fade (Pinned):fade pinned", "Remove:remove", "Remove (Pinned):remove pinned"]}),
345 remove_tag_headers: newOption("checkbox", false, "Remove Tag Headers", "Remove the \"copyrights\", \"characters\", and \"artist\" headers from the sidebar tag list."),
346 resize_link_style: newOption("dropdown", "full", "Resize Link Style", "Set how the resize links in the post sidebar options section will display. <tipdesc>Full:</tipdesc> Show the \"resize to window\", \"resize to window width\", and \"resize to window height\" links on separate lines. <tipdesc>Minimal:</tipdesc> Show the \"resize to window\" (W&H), \"resize to window width\" (W), and \"resize to window height\" (H) links on one line.", {txtOptions:["Full:full", "Minimal:minimal"]}),
347 script_blacklisted_tags: "",
348 search_add: newOption("dropdown", "disabled", "Search Add", "Modify the sidebar tag list by adding, removing, or replacing links in the sidebar tag list that modify the current search's tags. <tipdesc>Remove:</tipdesc> Remove any preexisting \"+\" and \"–\" links. <tipdesc>Link:</tipdesc> Add \"+\" and \"–\" links to modified versions of the current search that include or exclude their respective tags. <tipdesc>Toggle:</tipdesc> Add toggle links that modify the search box with their respective tags. Clicking a toggle link will switch between a tag being included (+), excluded (–), potentially included among other tags (~), and removed (»). Right clicking a toggle link will immediately remove its tag. If a tag already exists in the search box or gets entered/removed through alternative means, the toggle link will automatically update to reflect the tag's current status. <tiphead>Note</tiphead>The remove option is intended for users above the basic user level that want to remove the links. For users that can't normally see the links and do not wish to see them, this setting should be set to disabled.", {txtOptions:["Disabled:disabled", "Remove:remove", "Link:link", "Toggle:toggle"]}),
349 show_banned: newOption("checkbox", false, "Show Banned", "Display all banned posts in the search, pool, popular, favorites, comments, notes, and favorite group listings."),
350 show_deleted: newOption("checkbox", false, "Show Deleted", "Display all deleted posts in the search, pool, popular, favorites, notes, and favorite group listings. <tiphead>Note</tiphead>When using this option, your Danbooru account settings should have \"deleted post filter\" set to no and \"show deleted children\" set to yes in order to function properly and minimize connections to Danbooru."),
351 show_loli: newOption("checkbox", false, "Show Loli", "Display loli posts in the search, pool, popular, favorites, comments, notes, and favorite group listings."),
352 show_resized_notice: newOption("dropdown", "all", "Show Resized Notice", "Set which image type(s) the purple notice bar about image resizing is allowed to display on. <tiphead>Tip</tiphead>When a sample and original image are available for a post, a new option for swapping between the sample and original image becomes available in the sidebar options menu. Even if you disable the resized notice bar, you will always have access to its main function.", {txtOptions:["None (Disabled):none", "Original:original", "Sample:sample", "Original & Sample:all"]}),
353 show_shota: newOption("checkbox", false, "Show Shota", "Display shota posts in the search, pool, popular, favorites, comments, notes, and favorite group listings."),
354 show_toddlercon: newOption("checkbox", false, "Show Toddlercon", "Display toddlercon posts in the search, pool, popular, favorites, comments, notes, and favorite group listings."),
355 single_color_borders: newOption("checkbox", false, "Single Color Borders", "Only use one color for each thumbnail border."),
356 thumb_info: newOption("dropdown", "disabled", "Thumbnail Info", "Display the score (★), favorite count (♥), and rating (S, Q, or E) for a post with its thumbnail. <tipdesc>Below:</tipdesc> Display the extra information below thumbnails. <tipdesc>Hover:</tipdesc> Display the extra information upon hovering over a thumbnail's area. <tiphead>Note</tiphead>Extra information will not be added to the thumbnails in the comments listing since the score and rating are already visible there. Instead, the number of favorites will be added next to the existing score display.", {txtOptions:["Disabled:disabled", "Below:below", "Hover:hover"]}),
357 thumbnail_count: newOption("dropdown", 0, "Thumbnail Count", "Change the number of thumbnails that display in the search, favorites, and notes listings.", {txtOptions:["Disabled:0"], numRange:[1,200]}),
358 track_new: newOption("checkbox", false, "Track New Posts", "Add a menu option titled \"new\" to the posts section submenu (between \"listing\" and \"upload\") that links to a customized search focused on keeping track of new posts.<tiphead>Note</tiphead>While browsing the new posts, the current page of posts is also tracked. If the new post listing is left, clicking the \"new\" link later on will attempt to pull up the posts where browsing was left off at.<tiphead>Tip</tiphead>If you would like to bookmark the new post listing, drag and drop the link to your bookmarks or right click it and bookmark/copy the location from the context menu."),
359 status_borders: borderSet(["deleted", true, "#000000", "solid", "post-status-deleted"], ["flagged", true, "#FF0000", "solid", "post-status-flagged"], ["pending", true, "#0000FF", "solid", "post-status-pending"], ["child", true, "#CCCC00", "solid", "post-status-has-parent"], ["parent", true, "#00FF00", "solid", "post-status-has-children"]),
360 tag_borders: borderSet(["loli", true, "#FFC0CB", "solid"], ["shota", true, "#66CCFF", "solid"], ["toddlercon", true, "#9370DB", "solid"], ["status:banned", true, "#000000", "solid"]),
361 thumb_cache_limit: newOption("dropdown", 5000, "Thumbnail Info Cache Limit", "Limit the number of thumbnail information entries cached in the browser.<tiphead>Note</tiphead>No actual thumbnails are cached. Only filename information used to speed up the display of hidden thumbnails is stored. Every 1000 entries is approximately equal to 0.1 megabytes of space.", {txtOptions:["Disabled:0"], numList:[1000,2000,3000,4000,5000,6000,7000,8000,9000,10000]}),
362 collapse_sidebar_data: {post: {}, thumb: {}},
363 track_new_data: {viewed: 0, viewing: 1}
364 },
365 quick_search: "",
366 search_add: {
367 active_links: {},
368 links: {},
369 old: ""
370 },
371 sections: { // Setting sections and ordering.
372 blacklist_options: newSection("general", ["blacklist_session_toggle", "blacklist_post_display", "blacklist_thumb_mark", "blacklist_highlight_color", "blacklist_thumb_controls", "blacklist_smart_view", "blacklist_add_bars"], "Options"),
373 border_options: newSection("general", ["custom_tag_borders", "custom_status_borders", "single_color_borders", "border_width", "border_spacing"], "Options"),
374 browse: newSection("general", ["show_loli", "show_shota", "show_toddlercon", "show_banned", "show_deleted", "thumbnail_count", "thumb_info", "post_link_new_window"], "Post Browsing"),
375 control: newSection("general", ["load_sample_first", "alternate_image_swap", "image_swap_mode", "post_resize", "post_resize_mode", "post_drag_scroll", "autoscroll_post", "disable_embedded_notes"], "Post Control"),
376 endless: newSection("general", ["endless_default", "endless_session_toggle", "endless_separator", "endless_scroll_limit", "endless_remove_dup", "endless_pause_interval", "endless_fill", "endless_preload"], "Endless Pages"),
377 notices: newSection("general", ["show_resized_notice", "minimize_status_notices", "hide_sign_up_notice", "hide_upgrade_notice", "hide_tos_notice", "hide_comment_notice", "hide_tag_notice", "hide_upload_notice", "hide_pool_notice", "hide_ban_notice"], "Notices"),
378 sidebar: newSection("general", ["remove_tag_headers", "post_tag_scrollbars", "search_tag_scrollbars", "autohide_sidebar", "fixed_sidebar", "collapse_sidebar"], "Tag Sidebar"),
379 misc: newSection("general", ["direct_downloads", "track_new", "clean_links", "arrow_nav", "post_tag_titles", "search_add", "page_counter", "comment_score", "quick_search"], "Misc."),
380 misc_layout: newSection("general", ["fixed_paginator", "move_save_search"], "Misc."),
381 script_settings: newSection("general", ["bypass_api", "manage_cookies", "enable_status_message", "resize_link_style", "override_blacklist", "override_resize", "override_sample", "thumb_cache_limit"], "Script Settings"),
382 status_borders: newSection("border", "status_borders", "Custom Status Borders", "When using custom status borders, the borders can be edited here. For easy color selection, use one of the many free tools on the internet like <a target=\"_blank\" href=\"http://www.quackit.com/css/css_color_codes.cfm\">this one</a>."),
383 tag_borders: newSection("border", "tag_borders", "Custom Tag Borders", "When using custom tag borders, the borders can be edited here. For easy color selection, use one of the many free tools on the internet like <a target=\"_blank\" href=\"http://www.quackit.com/css/css_color_codes.cfm\">this one</a>.")
384 },
385 settings: {
386 changed: {}
387 },
388 timers: {},
389 user: {} // User settings.
390 };
391
392 localStorageCheck();
393
394 loadSettings(); // Load user settings.
395
396 // Provide a session ID in order to detect XML requests carrying over from other pages.
397 window.bbbSession = new Date().getTime();
398
399 // Location variables.
400 var gLoc = danbLoc(); // Current location
401 var gLocRegex = new RegExp("\\b" + gLoc + "\\b");
402
403 // Script variables.
404 // Global
405 var show_loli = bbb.user.show_loli;
406 var show_shota = bbb.user.show_shota;
407 var show_toddlercon = bbb.user.show_toddlercon;
408 var show_banned = bbb.user.show_banned;
409 var deleted_shown = (gLoc === "search" && /^(?:any|deleted)$/i.test(getTagVar("status"))); // Check whether deleted posts are shown by default.
410 var show_deleted = deleted_shown || bbb.user.show_deleted;
411 var direct_downloads = bbb.user.direct_downloads;
412 var post_link_new_window = bbb.user.post_link_new_window;
413
414 var blacklist_session_toggle = bbb.user.blacklist_session_toggle;
415 var blacklist_post_display = bbb.user.blacklist_post_display;
416 var blacklist_thumb_mark = bbb.user.blacklist_thumb_mark;
417 var blacklist_highlight_color = bbb.user.blacklist_highlight_color;
418 var blacklist_add_bars = bbb.user.blacklist_add_bars;
419 var blacklist_thumb_controls = bbb.user.blacklist_thumb_controls;
420 var blacklist_smart_view = bbb.user.blacklist_smart_view;
421
422 var custom_tag_borders = bbb.user.custom_tag_borders;
423 var custom_status_borders = bbb.user.custom_status_borders;
424 var single_color_borders = bbb.user.single_color_borders;
425 var border_spacing = bbb.user.border_spacing;
426 var border_width = bbb.user.border_width;
427 var clean_links = bbb.user.clean_links;
428 var comment_score = bbb.user.comment_score;
429 var thumb_info = bbb.user.thumb_info;
430 var autohide_sidebar = gLocRegex.test(bbb.user.autohide_sidebar);
431 var fixed_sidebar = gLocRegex.test(bbb.user.fixed_sidebar);
432 var fixed_paginator = bbb.user.fixed_paginator;
433 var collapse_sidebar = bbb.user.collapse_sidebar;
434 var move_save_search = bbb.user.move_save_search;
435 var page_counter = bbb.user.page_counter;
436 var quick_search = bbb.user.quick_search;
437
438 var bypass_api = bbb.user.bypass_api;
439 var manage_cookies = bbb.user.manage_cookies;
440 var enable_status_message = bbb.user.enable_status_message;
441 var resize_link_style = bbb.user.resize_link_style;
442 var override_blacklist = bbb.user.override_blacklist;
443 var override_resize = bbb.user.override_resize;
444 var override_sample = bbb.user.override_sample;
445 var track_new = bbb.user.track_new;
446
447 var show_resized_notice = bbb.user.show_resized_notice;
448 var hide_sign_up_notice = bbb.user.hide_sign_up_notice;
449 var hide_upgrade_notice = bbb.user.hide_upgrade_notice;
450 var minimize_status_notices = bbb.user.minimize_status_notices;
451 var hide_tos_notice = bbb.user.hide_tos_notice;
452 var hide_comment_notice = bbb.user.hide_comment_notice;
453 var hide_tag_notice = bbb.user.hide_tag_notice;
454 var hide_upload_notice = bbb.user.hide_upload_notice;
455 var hide_pool_notice = bbb.user.hide_pool_notice;
456 var hide_ban_notice = bbb.user.hide_ban_notice;
457
458 // Search
459 var arrow_nav = bbb.user.arrow_nav;
460 var search_add = bbb.user.search_add;
461 var search_tag_scrollbars = bbb.user.search_tag_scrollbars;
462 var thumbnail_count = bbb.user.thumbnail_count;
463 var thumbnail_count_default = 20; // Number of thumbnails BBB should expect Danbooru to return by default.
464 var thumb_cache_limit = bbb.user.thumb_cache_limit;
465
466 // Post
467 var alternate_image_swap = bbb.user.alternate_image_swap;
468 var post_resize = accountSettingCheck("post_resize");
469 var post_resize_mode = bbb.user.post_resize_mode;
470 var post_drag_scroll = bbb.user.post_drag_scroll;
471 var load_sample_first = accountSettingCheck("load_sample_first");
472 var remove_tag_headers = bbb.user.remove_tag_headers;
473 var post_tag_scrollbars = bbb.user.post_tag_scrollbars;
474 var post_tag_titles = bbb.user.post_tag_titles;
475 var autoscroll_post = bbb.user.autoscroll_post;
476 var image_swap_mode = bbb.user.image_swap_mode;
477 var disable_embedded_notes = bbb.user.disable_embedded_notes;
478
479 // Endless
480 var endless_default = bbb.user.endless_default;
481 var endless_fill = bbb.user.endless_fill;
482 var endless_pause_interval = bbb.user.endless_pause_interval;
483 var endless_preload = bbb.user.endless_preload;
484 var endless_remove_dup = bbb.user.endless_remove_dup;
485 var endless_scroll_limit = bbb.user.endless_scroll_limit;
486 var endless_separator = bbb.user.endless_separator;
487 var endless_session_toggle = bbb.user.endless_session_toggle;
488
489 // Stored data
490 var status_borders = bbb.user.status_borders;
491 var tag_borders = bbb.user.tag_borders;
492 var collapse_sidebar_data = bbb.user.collapse_sidebar_data;
493 var track_new_data = bbb.user.track_new_data;
494 var script_blacklisted_tags = bbb.user.script_blacklisted_tags;
495
496 // Other data
497 var bbbHiddenImg = "data:image/png;base64,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";
498 var bbbBlacklistImg = "data:image/png;base64,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";
499 var bbbBlacklistIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAAkFBMVEUAAAD////////////////////////////////////////////////////////////////p6en////////////////////////////////////////////////////////+/v7///////////////////////////////////////////////////////////97JICZAAAAL3RSTlMACAQBoUSi5QcnMfmnsMW9VQno+vjsxrwstPyzMhOpSxS65vX06czqQf7NvqbzQKZY7GsAAADASURBVHhefZDnDoJAEITn8Kh3FAEFQbH3Mu//doaNLSFxfn7J7hT80cgJlAqc0S8bu55P+p47/rJQ8yUdflhCHq/WpjmZvKjSPPOyBFbZlNRKPFySzTaOANQJ6fZujsf9YUcjNMvpOQACn+kyLmjaObBI6QcAFGkRxYblLAIsqd4Q0ayUDzeBcr4A5q2h6U5Vfy5GeQbIh8l6I0YSKal72k3uhUSS8OQ0WwGPddFQq2/NPLW22rxrDgcZTvd35KGevk8VfmeGhUQAAAAASUVORK5CYII=";
500
501 /* "INIT" */
502 modifyDanbScript();
503
504 customCSS(); // Contains the portions related to notices.
505
506 thumbInfo();
507
508 removeTagHeaders();
509
510 searchAdd();
511
512 minimizeStatusNotices();
513
514 postTagTitles();
515
516 trackNew();
517
518 injectSettings();
519
520 modifyPage();
521
522 autohideSidebar();
523
524 moveSaveSearch();
525
526 pageCounter();
527
528 quickSearch();
529
530 postLinkNewWindow();
531
532 commentScoreInit();
533
534 cleanLinks();
535
536 postDDL();
537
538 arrowNav();
539
540 fixLimit();
541
542 bbbHotkeys();
543
544 endlessInit();
545
546 delayMe(formatThumbnails); // Delayed to allow Danbooru to run first.
547
548 delayMe(blacklistInit); // Delayed to allow Danbooru to run first.
549
550 delayMe(fixedSidebar); // Delayed to allow Danbooru layout to finalize.
551
552 delayMe(collapseSidebar); // Delayed to allow Danbooru layout to finalize.
553
554 delayMe(fixedPaginator); // Delayed to allow Danbooru layout to finalize.
555
556 /* Functions */
557
558 /* Functions for XML API info */
559 function searchJSON(mode, optArg) {
560 // Figure out the desired URL for a JSON API request, trigger any necessary xml flag, and update the status message.
561 var url = location.href.split("#", 1)[0];
562 var idCache, idList, idSearch, page; // If/else variables.
563
564 if (mode === "search" || mode === "notes" || mode === "favorites") {
565 url = (allowUserLimit() ? updateURLQuery(url, {limit: thumbnail_count}) : url);
566 bbb.flags.thumbs_xml = true;
567
568 if (mode === "search")
569 fetchJSON(url.replace(/\/?(?:posts)?\/?(?:\?|$)/, "/posts.json?"), "search");
570 else if (mode === "notes")
571 fetchJSON(url.replace(/\/notes\/?(?:\?|$)/, "/notes.json?"), "notes");
572 else if (mode === "favorites")
573 fetchJSON(url.replace(/\/favorites\/?(?:\?|$)/, "/favorites.json?"), "favorites");
574
575 bbbStatus("posts", "new");
576 }
577 else if (mode === "popular" || mode === "popular_view") {
578 bbb.flags.thumbs_xml = true;
579
580 fetchJSON(url.replace(/\/(popular_view|popular)\/?/, "/$1.json"), mode);
581 bbbStatus("posts", "new");
582 }
583 else if (mode === "pool" || mode === "favorite_group") {
584 idCache = getIdCache();
585 bbb.flags.thumbs_xml = true;
586
587 if (idCache)
588 searchJSON(mode + "_search", {post_ids: idCache});
589 else // Get a new cache.
590 fetchJSON(url.replace(/\/(pools|favorite_groups)\/(\d+)/, "/$1/$2.json"), mode + "_cache", mode + "_search");
591
592 bbbStatus("posts", "new");
593 }
594 else if (mode === "pool_search" || mode === "favorite_group_search") {
595 page = Number(getVar("page")) || 1;
596 idList = optArg.post_ids.split(" ");
597 idSearch = idList.slice((page - 1) * thumbnail_count_default, page * thumbnail_count_default);
598
599 fetchJSON("/posts.json?tags=status:any+id:" + idSearch.join(","), mode, idSearch);
600 }
601 else if (mode === "endless") {
602 bbb.flags.endless_xml = true;
603
604 if (gLoc === "pool" || gLoc === "favorite_group") {
605 idCache = getIdCache();
606
607 if (idCache)
608 searchJSON("endless_" + gLoc + "_search", {post_ids: idCache});
609 else // Get a new cache.
610 fetchJSON(url.replace(/\/(pools|favorite_groups)\/(\d+)/, "/$1/$2.json"), gLoc + "_cache", "endless_" + gLoc + "_search");
611 }
612 else {
613 url = endlessNexURL();
614
615 fetchJSON(url.replace(/(\?)|$/, ".json$1"), "endless");
616 }
617
618 bbbStatus("posts", "new");
619 }
620 else if (mode === "endless_pool_search" || mode === "endless_favorite_group_search") {
621 idList = optArg.post_ids.split(" ");
622 page = Number(getVar("page", endlessNexURL())); // If a pool gets over 1000 pages, I have no idea what happens for regular users. Biggest pool is currently around 400 pages so we won't worry about that for the time being.
623 idSearch = idList.slice((page - 1) * thumbnail_count_default, page * thumbnail_count_default);
624
625 fetchJSON("/posts.json?tags=status:any+id:" + idSearch.join(","), "endless", idSearch);
626 }
627 else if (mode === "comments") {
628 fetchJSON(url.replace(/\/comments\/?/, "/comments.json"), "comments");
629 bbbStatus("posts", "new");
630 }
631 else if (mode === "parent" || mode === "child") {
632 var parentUrl = "/posts.json?limit=200&tags=status:any+parent:" + optArg;
633
634 fetchJSON(parentUrl, mode, optArg);
635 bbbStatus("posts", "new");
636 }
637 else if (mode === "ugoira") {
638 fetchJSON(url.replace(/\/posts\/(\d+)/, "/posts/$1.json"), "ugoira");
639 bbbStatus("posts", "new");
640 }
641 }
642
643 function fetchJSON(url, mode, optArg, session, retries) {
644 // Retrieve JSON.
645 var xmlhttp = new XMLHttpRequest();
646 var xmlRetries = retries || 0;
647 var xmlSession = session || window.bbbSession;
648
649 if (xmlhttp !== null) {
650 xmlhttp.onreadystatechange = function() {
651 if (xmlSession !== window.bbbSession) // If we end up receiving an xml response from a different page, reject it.
652 xmlhttp.abort();
653 else if (xmlhttp.readyState === 4) { // 4 = "loaded"
654 if (xmlhttp.status === 200) { // 200 = "OK"
655 var xml = JSON.parse(xmlhttp.responseText);
656
657 // Update status message.
658 if (mode === "search" || mode === "popular" || mode === "popular_view" || mode === "notes" || mode === "favorites" || mode === "pool_search" || mode === "favorite_group_search") {
659 bbb.flags.thumbs_xml = false;
660
661 parseListing(xml, optArg);
662 }
663 else if (mode === "post")
664 parsePost(xml);
665 else if (mode === "pool_cache" || mode === "favorite_group_cache") {
666 var collId = /\/(?:pools|favorite_groups)\/(\d+)/.exec(location.href)[1];
667
668 sessionStorage.bbbSetItem("bbb_" + mode + "_" + collId, new Date().getTime() + " " + xml.post_ids);
669 searchJSON(optArg, xml);
670 }
671 else if (mode === "endless") {
672 bbb.flags.endless_xml = false;
673
674 endlessXMLJSONHandler(xml, optArg);
675 }
676 else if (mode === "comments")
677 parseComments(xml);
678 else if (mode === "parent" || mode === "child")
679 parseRelations(xml, mode, optArg);
680 else if (mode === "ugoira")
681 fixHiddenUgoira(xml);
682
683 if (mode !== "pool_cache" && mode !== "favorite_group_cache")
684 bbbStatus("posts", "done");
685 }
686 else {
687 if (xmlhttp.status === 403 || xmlhttp.status === 401) {
688 bbbNotice('Error retrieving post information. Access denied. You must be logged in to a Danbooru account to access the API for hidden image information and direct downloads. <br><span style="font-size: smaller;">(<span><a href="#" id="bbb-bypass-api-link">Do not warn me again and automatically bypass API features in the future.</a></span>)</span>', -1);
689 document.getElementById("bbb-bypass-api-link").addEventListener("click", function(event) {
690 if (event.button !== 0)
691 return;
692
693 updateSettings("bypass_api", true);
694 this.parentNode.innerHTML = "Settings updated. You may change this setting under the preferences tab in the settings panel.";
695 event.preventDefault();
696 }, false);
697 bbbStatus("posts", "error");
698 }
699 else if (xmlhttp.status === 421) {
700 bbbNotice("Error retrieving post information. Your Danbooru API access is currently throttled. Please try again later.", -1);
701 bbbStatus("posts", "error");
702 }
703 else if (xmlhttp.status !== 0) {
704 if (xmlRetries < 1) {
705 xmlRetries++;
706 fetchJSON(url, mode, optArg, xmlSession, xmlRetries);
707 }
708 else {
709 var linkId = uniqueIdNum(); // Create a unique ID.
710 var noticeMsg = bbbNotice('Error retrieving post information (JSON Code: ' + xmlhttp.status + ' ' + xmlhttp.statusText + '). (<a id="' + linkId + '" href="#">Retry</a>)', -1);
711
712 bbbStatus("posts", "error");
713
714 document.getElementById(linkId).addEventListener("click", function(event) {
715 if (event.button !== 0)
716 return;
717
718 closeBbbNoticeMsg(noticeMsg);
719 searchJSON(mode, optArg);
720 event.preventDefault();
721 }, false);
722 }
723 }
724 }
725 }
726 };
727 xmlhttp.open("GET", url, true);
728 xmlhttp.send(null);
729 }
730 }
731
732 function parseListing(xml, optArg) {
733 // Use JSON results for thumbnail listings.
734 var posts = xml;
735 var orderedIds = (gLoc === "pool" || gLoc === "favorite_group" ? optArg : undefined);
736
737 if (!posts[0])
738 return;
739
740 // Thumb preparation.
741 var newThumbs = createThumbListing(posts, orderedIds);
742
743 // Update the existing thumbnails with new ones.
744 updateThumbListing(newThumbs);
745
746 // Fix the paginator. The paginator isn't always in the new container, so run this on the whole page after the new container is inserted.
747 fixPaginator();
748
749 // Fix hidden thumbnails.
750 fixHiddenThumbs();
751
752 // Update the URL with the limit value.
753 fixURLLimit();
754
755 // Cache thumbnails to the history for random searches.
756 saveStateCache();
757 }
758
759 function parsePost(postInfo) {
760 // Take a post's info and alter its page.
761 var post = bbb.post.info = formatInfo(postInfo || scrapePost());
762 var imgContainer = document.getElementById("image-container");
763
764 if (!imgContainer) {
765 bbbNotice("Post content could not be located.", -1);
766 return;
767 }
768
769 if (!post || !post.file_url) {
770 bbbNotice("Due to a lack of provided information, this post cannot be viewed.", -1);
771 return;
772 }
773
774 // Stop if we're on Safebooru and the image isn't safe.
775 if (safebPostTest(post))
776 return;
777
778 // Enable the "Resize to window", "Toggle Notes", "Random Post", and "Find similar" options for logged out users.
779 createOptionsSection();
780
781 // Fix the direct post links in the information and options sections for hidden posts.
782 fixPostDownloadLinks();
783
784 // Replace the "resize to window" link with new resize links.
785 modifyResizeLink();
786
787 // Keep any original video from continuing to play/download after being removed.
788 var origVideo = imgContainer.getElementsByTagName("video")[0];
789
790 if (origVideo) {
791 origVideo.pause();
792 origVideo.src = "about:blank";
793 origVideo.load();
794 }
795
796 // Create content.
797 if (post.file_ext === "swf") // Create flash object.
798 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <object height="' + post.image_height + '" width="' + post.image_width + '"> <params name="movie" value="' + post.file_url + '"> <embed allowscriptaccess="never" src="' + post.file_url + '" height="' + post.image_height + '" width="' + post.image_width + '"> </params> </object> <p><a href="' + post.file_url + '">Save this flash (right click and save)</a></p>';
799 else if (post.file_ext === "webm" || post.file_ext === "mp4") { // Create video
800 var playerLoop = (post.has_sound ? '' : ' loop="loop"'); // No looping for videos with sound.
801
802 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <video id="image" autoplay="autoplay"' + playerLoop + ' controls="controls" src="' + post.file_url + '" height="' + post.image_height + '" width="' + post.image_width + '"></video> <p><a href="' + post.file_url + '">Save this video (right click and save)</a></p>';
803 }
804 else if (post.file_ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(post.tag_string)) { // Create ugoira
805 var useUgoiraOrig = getVar("original");
806
807 // Get rid of all the old events handlers.
808 if (Danbooru.Ugoira && Danbooru.Ugoira.player)
809 $(Danbooru.Ugoira.player).unbind();
810
811 if ((load_sample_first && useUgoiraOrig !== "1") || useUgoiraOrig === "0") { // Load sample webm version.
812 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <video id="image" autoplay="autoplay" loop="loop" controls="controls" src="' + post.large_file_url + '" height="' + post.image_height + '" width="' + post.image_width + '" data-fav-count="' + post.fav_count + '" data-flags="' + post.flags + '" data-has-active-children="' + post.has_active_children + '" data-has-children="' + post.has_children + '" data-large-height="' + post.sample_height + '" data-large-width="' + post.sample_width + '" data-original-height="' + post.image_height + '" data-original-width="' + post.image_width + '" data-rating="' + post.rating + '" data-score="' + post.score + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '"></video> <p><a href="' + post.large_file_url + '">Save this video (right click and save)</a> | <a href="' + updateURLQuery(location.href, {original: "1"}) + '">View original</a> | <a href="#" id="bbb-note-toggle">Toggle notes</a></p>';
813
814 // Prep the "toggle notes" link.
815 noteToggleLinkInit();
816 }
817 else { // Load original ugoira version.
818 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <canvas data-ugoira-content-type="' + post.pixiv_ugoira_frame_data.content_type.replace(/"/g, """) + '" data-ugoira-frames="' + JSON.stringify(post.pixiv_ugoira_frame_data.data).replace(/"/g, """) + '" data-fav-count="' + post.fav_count + '" data-flags="' + post.flags + '" data-has-active-children="' + post.has_active_children + '" data-has-children="' + post.has_children + '" data-large-height="' + post.image_height + '" data-large-width="' + post.image_width + '" data-original-height="' + post.image_height + '" data-original-width="' + post.image_width + '" data-rating="' + post.rating + '" data-score="' + post.score + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '" height="' + post.image_height + '" width="' + post.image_width + '" id="image"></canvas> <div id="ugoira-controls"> <div id="ugoira-control-panel" style="width: ' + post.image_width + 'px; min-width: 350px;"> <button id="ugoira-play" name="button" style="display: none;" type="submit">Play</button> <button id="ugoira-pause" name="button" type="submit">Pause</button> <div id="seek-slider" style="width: ' + (post.image_width - 81) + 'px; min-width: 269px;"></div> </div> <p id="save-video-link"><a href="' + post.large_file_url + '">Save as video (right click and save)</a> | <a href="' + updateURLQuery(location.href, {original: "0"}) + '">View sample</a> | <a href="#" id="bbb-note-toggle">Toggle notes</a></p> </div>';
819
820 // Make notes toggle when clicking the ugoira animation.
821 noteToggleInit();
822
823 // Prep the "toggle notes" link. The "toggle notes" link is added here just for consistency's sake.
824 noteToggleLinkInit();
825
826 if (post.pixiv_ugoira_frame_data.data) // Set up the post.
827 ugoiraInit();
828 else // Fix hidden posts.
829 searchJSON("ugoira");
830 }
831 }
832 else if (!post.image_height) // Create manual download.
833 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div><p><a href="' + post.file_url + '">Save this file (right click and save)</a></p>';
834 else { // Create image
835 var newWidth, newHeight, newUrl; // If/else variables.
836 var imgDesc = (getMeta("og:title") || "").replace(" - Danbooru", "");
837
838 if (load_sample_first && post.has_large) {
839 newWidth = post.sample_width;
840 newHeight = post.sample_height;
841 newUrl = post.large_file_url;
842 }
843 else {
844 newWidth = post.image_width;
845 newHeight = post.image_height;
846 newUrl = post.file_url;
847 }
848
849 imgContainer.innerHTML = '<div id="note-container"></div> <div id="note-preview"></div> <img alt="' + post.tag_string + '" data-fav-count="' + post.fav_count + '" data-flags="' + post.flags + '" data-has-active-children="' + post.has_active_children + '" data-has-children="' + post.has_children + '" data-large-height="' + post.sample_height + '" data-large-width="' + post.sample_width + '" data-original-height="' + post.image_height + '" data-original-width="' + post.image_width + '" data-rating="' + post.rating + '" data-score="' + post.score + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '" height="' + newHeight + '" width="' + newWidth + '" id="image" src="' + newUrl + '" /> <img src="about:blank" height="1" width="1" id="bbb-loader" style="position: absolute; right: 0px; top: 0px; display: none;"/> <p class="desc">' + imgDesc + '</p>';
850
851 bbb.el.bbbLoader = document.getElementById("bbb-loader");
852
853 // Create/replace the elements related to image swapping and set them up.
854 swapImageInit();
855
856 if (alternate_image_swap) // Make sample/original images swap when clicking the image.
857 alternateImageSwap();
858 else // Make notes toggle when clicking the image.
859 noteToggleInit();
860 }
861
862 // Enable drag scrolling.
863 dragScrollInit();
864
865 // Resize the content if desired.
866 if (post_resize)
867 resizePost(post_resize_mode);
868
869 // Enable translation mode.
870 translationModeInit();
871
872 // Disable embedded notes.
873 disableEmbeddedNotes();
874
875 // Load/reload notes.
876 Danbooru.Note.load_all("bbb");
877
878 // Auto position the content if desired.
879 autoscrollPost();
880
881 // Blacklist.
882 blacklistUpdate();
883
884 // Fix the parent/child notice(s).
885 checkRelations();
886 }
887
888 function parseComments(xml) {
889 // Fix missing comments by inserting them into their appropriate position.
890 var posts = xml;
891 var numPosts = posts.length;
892 var expectedPosts = numPosts;
893 var existingPosts = getPosts();
894 var eci = 0;
895
896 for (var i = 0; i < numPosts; i++) {
897 var post = formatInfo(posts[i]);
898 var existingPost = existingPosts[eci];
899
900 if (!existingPost || String(post.id) !== existingPost.getAttribute("data-id")) {
901 if (!/(?:^|\s)(?:loli|shota|toddlercon)(?:$|\s)/.test(post.tag_string) && !post.is_banned) // API post isn't hidden and doesn't exist on the page. Skip it and try to find where the page's info matches up.
902 continue;
903 else if ((!show_loli && /(?:^|\s)loli(?:$|\s)/.test(post.tag_string)) || (!show_shota && /(?:^|\s)shota(?:$|\s)/.test(post.tag_string)) || (!show_toddlercon && /(?:^|\s)toddlercon(?:$|\s)/.test(post.tag_string)) || (!show_banned && post.is_banned) || safebPostTest(post)) { // Skip hidden posts if the user has selected to do so.
904 expectedPosts--;
905 continue;
906 }
907
908 // Prepare the post information.
909 var tagLinks = post.tag_string.bbbSpacePad();
910 var generalTags = post.tag_string_general.split(" ");
911 var artistTags = post.tag_string_artist.split(" ");
912 var copyrightTags = post.tag_string_copyright.split(" ");
913 var characterTags = post.tag_string_character.split(" ");
914 var limit = (thumbnail_count ? "&limit=" + thumbnail_count : "");
915 var j, jl, tag; // Loop variables.
916
917 for (j = 0, jl = generalTags.length; j < jl; j++) {
918 tag = generalTags[j];
919 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-0"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
920 }
921
922 for (j = 0, jl = artistTags.length; j < jl; j++) {
923 tag = artistTags[j];
924 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-1"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
925 }
926
927 for (j = 0, jl = copyrightTags.length; j < jl; j++) {
928 tag = copyrightTags[j];
929 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-3"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
930 }
931
932 for (j = 0, jl = characterTags.length; j < jl; j++) {
933 tag = characterTags[j];
934 tagLinks = tagLinks.replace(tag.bbbSpacePad(), ' <span class="category-4"> <a href="/posts?tags=' + encodeURIComponent(tag) + limit + '">' + tag.replace(/_/g, " ") + '</a> </span> ');
935 }
936
937 // Create the new post.
938 var childSpan = document.createElement("span");
939
940 childSpan.innerHTML = '<div id="post_' + post.id + '" class="post post-preview' + post.thumb_class + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '" data-rating="' + post.rating + '" data-flags="' + post.flags + '" data-score="' + post.score + '" data-parent-id="' + post.parent_id + '" data-has-children="' + post.has_children + '" data-id="' + post.id + '" data-has-sound="' + post.has_sound + '" data-width="' + post.image_width + '" data-height="' + post.image_height + '" data-approver-id="' + post.approver_id + '" data-fav-count="' + post.fav_count + '" data-pixiv-id="' + post.pixiv_id + '" data-md5="' + post.md5 + '" data-file-ext="' + post.file_ext + '" data-file-url="' + post.file_url + '" data-large-file-url="' + post.large_file_url + '" data-preview-file-url="' + post.preview_file_url + '"> <div class="preview"> <a href="/posts/' + post.id + '"> <img alt="' + post.md5 + '" src="' + post.preview_file_url + '" /> </a> </div> <div class="comments-for-post" data-post-id="' + post.id + '"> <div class="header"> <div class="row"> <span class="info"> <strong>Date</strong> <time datetime="' + post.created_at + '" title="' + post.created_at.replace(/(.+)T(.+)-(.+)/, "$1 $2 -$3") + '">' + post.created_at.replace(/(.+)T(.+):\d+-.+/, "$1 $2") + '</time> </span> <span class="info"> <strong>User</strong> <a href="/users/' + post.uploader_id + '">' + post.uploader_name + '</a> </span> <span class="info"> <strong>Rating</strong> ' + post.rating + ' </span> <span class="info"> <strong>Score</strong> <span> <span id="score-for-post-' + post.id + '">' + post.score + '</span> </span> </span> </div> <div class="row list-of-tags"> <strong>Tags</strong>' + tagLinks + '</div> </div> </div> <div class="clearfix"></div> </div>';
941
942 // Prepare thumbnails.
943 prepThumbnails(childSpan);
944
945 if (!existingPost) // There isn't a next post so append the new post to the end before the paginator.
946 document.getElementById("a-index").insertBefore(childSpan.firstElementChild, getPaginator());
947 else // Insert new post before the post that should follow it.
948 existingPost.parentNode.insertBefore(childSpan.firstElementChild, existingPost);
949
950 // Get the comments and image info.
951 searchPages("post_comments", post.id);
952 }
953
954 eci++;
955 }
956
957 // If we don't have the expected number of posts, the API info and page are too out of sync. (Message disabled to work around deleted comments until an accurate method is worked out.)
958 // if (existingPosts.length !== expectedPosts)
959 // bbbNotice("Loading of hidden post(s) failed. Please refresh.", -1);
960 }
961
962 function parseRelations(xml, mode, parentId) {
963 // Create a new parent/child notice.
964 var posts = xml;
965 var activePost = bbb.post.info;
966 var numPosts = posts.length;
967 var relationCookie = getCookie()["show-relationship-previews"];
968 var showPreview = (relationCookie === undefined || relationCookie === "1" ? true : false);
969 var childSpan = document.createElement("span");
970 var query = "?tags=parent:" + parentId + (show_deleted ? "+status:any" : "") + (thumbnail_count ? "&limit=" + thumbnail_count : "");
971 var thumbs = "";
972 var forceShowDeleted = activePost.is_deleted; // If the parent is deleted or the active post is deleted, all deleted posts are shown.
973 var parentDeleted = false;
974 var isSafebooru = (location.host.indexOf("safebooru") > -1 ? true : false);
975 var target, previewLinkId, previewLinkTxt, previewId, classes, msg, displayStyle; // If/else variables.
976 var i, post; // Loop variables.
977
978 // Figure out if the parent is deleted.
979 for (i = 0; i < numPosts; i++) {
980 post = posts[i];
981
982 if (post.id === parentId) {
983 parentDeleted = post.is_deleted;
984 forceShowDeleted = forceShowDeleted || parentDeleted;
985 }
986 }
987
988 // Set up the notice variables.
989 if (showPreview) {
990 previewLinkTxt = "« hide";
991 displayStyle = "block";
992 }
993 else {
994 previewLinkTxt = "show »";
995 displayStyle = "none";
996 }
997
998 if (mode === "child") {
999 target = document.getElementsByClassName("notice-child")[0];
1000 previewLinkId = "has-parent-relationship-preview-link";
1001 previewId = "has-parent-relationship-preview";
1002 classes = "notice-child";
1003
1004 if (!isSafebooru) {
1005 if (numPosts)
1006 msg = 'This post belongs to a <a href="/posts' + query + '">parent</a>' + (parentDeleted ? " (deleted)" : "");
1007
1008 if (numPosts === 3)
1009 msg += ' and has <a href="/posts' + query + '">a sibling</a>';
1010 else if (numPosts > 3)
1011 msg += ' and has <a href="/posts' + query + '">' + (numPosts - 2) + ' siblings</a>';
1012 }
1013 else {
1014 var parentNotSafe = true;
1015
1016 for (i = 0; i < numPosts; i++) {
1017 if (posts[i].id === parentId)
1018 parentNotSafe = false;
1019 }
1020
1021 var siblingLimit = (parentNotSafe ? 2 : 3);
1022
1023 if (numPosts)
1024 msg = 'This post belongs to a <a href="/posts' + query + '">parent</a>' + (parentNotSafe ? " (not safe)" : (parentDeleted ? " (deleted)" : ""));
1025
1026 if (numPosts === siblingLimit)
1027 msg += ' and has <a href="/posts' + query + '">a sibling</a>';
1028 else if (numPosts > siblingLimit)
1029 msg += ' and has <a href="/posts' + query + '">' + (numPosts - 2) + ' siblings</a>';
1030 }
1031 }
1032 else if (mode === "parent") {
1033 target = document.getElementsByClassName("notice-parent")[0];
1034 previewLinkId = "has-children-relationship-preview-link";
1035 previewId = "has-children-relationship-preview";
1036 classes = "notice-parent";
1037
1038 if (!isSafebooru) {
1039 if (numPosts === 2)
1040 msg = 'This post has <a href="/posts' + query + '">a child</a>';
1041 else if (numPosts > 2)
1042 msg = 'This post has <a href="/posts' + query + '">' + (numPosts - 1) + ' children</a>';
1043 }
1044 else {
1045 if (numPosts === 1)
1046 msg = 'This post has no safe <a href="/posts' + query + '">children</a>';
1047 else if (numPosts === 2)
1048 msg = 'This post has <a href="/posts' + query + '">a child</a>';
1049 else if (numPosts > 2)
1050 msg = 'This post has <a href="/posts' + query + '">' + (numPosts - 1) + ' children</a>';
1051 }
1052 }
1053
1054 // Create the main notice element.
1055 childSpan.innerHTML = '<div class="ui-corner-all ui-state-highlight notice ' + classes + '"> ' + msg + ' (<a href="/wiki_pages?title=help%3Apost_relationships">learn more</a>) <a href="#" id="' + previewLinkId + '">' + previewLinkTxt + '</a> <div id="' + previewId + '" style="display: ' + displayStyle + ';"> </div> </div>';
1056
1057 var newNotice = childSpan.firstElementChild;
1058 var thumbDiv = getId(previewId, newNotice);
1059 var previewLink = getId(previewLinkId, newNotice);
1060
1061 // Create the thumbnails.
1062 for (i = numPosts - 1; i >= 0; i--) {
1063 post = formatInfo(posts[i]);
1064
1065 if ((!show_loli && /(?:^|\s)loli(?:$|\s)/.test(post.tag_string)) || (!show_shota && /(?:^|\s)shota(?:$|\s)/.test(post.tag_string)) || (!show_toddlercon && /(?:^|\s)toddlercon(?:$|\s)/.test(post.tag_string)) || (!show_deleted && post.is_deleted && !forceShowDeleted) || (!show_banned && post.is_banned) || safebPostTest(post))
1066 continue;
1067
1068 checkHiddenThumbs(post);
1069
1070 var thumb = createThumbHTML(post, (clean_links ? "" : query)) + " ";
1071
1072 if (post.id === parentId)
1073 thumbs = thumb + thumbs;
1074 else
1075 thumbs += thumb;
1076 }
1077
1078 thumbDiv.innerHTML = thumbs;
1079
1080 // Highlight the post we're on.
1081 var activeThumb = getId("post_" + activePost.id, thumbDiv);
1082
1083 if (activeThumb)
1084 activeThumb.bbbAddClass("current-post");
1085
1086 // Make the show/hide links work.
1087 previewLink.bbbOverrideClick(function(event) {
1088 if (thumbDiv.style.display === "block") {
1089 thumbDiv.style.display = "none";
1090 previewLink.innerHTML = "show »";
1091 createCookie("show-relationship-previews", 0, 365);
1092 }
1093 else {
1094 thumbDiv.style.display = "block";
1095 previewLink.innerHTML = "« hide";
1096 createCookie("show-relationship-previews", 1, 365);
1097 }
1098
1099 event.preventDefault();
1100 });
1101
1102 // Prepare thumbnails.
1103 prepThumbnails(newNotice);
1104
1105 // Replace/add the notice.
1106 if (target)
1107 target.parentNode.replaceChild(newNotice, target);
1108 else if (mode === "child") {
1109 target = document.getElementsByClassName("notice-parent")[0] || bbb.el.resizeNotice || document.getElementById("image-container");
1110 target.parentNode.insertBefore(newNotice, target);
1111 }
1112 else if (mode === "parent") {
1113 target = bbb.el.resizeNotice || document.getElementById("image-container");
1114 target.parentNode.insertBefore(newNotice, target);
1115 }
1116
1117 // Fix hidden thumbnails.
1118 fixHiddenThumbs();
1119 }
1120
1121 function fixHiddenUgoira(xml) {
1122 // Use xml info to fix the missing info for hidden ugoira posts.
1123 var post = bbb.post.info;
1124 post.pixiv_ugoira_frame_data = xml.pixiv_ugoira_frame_data;
1125
1126 var imgContainer = document.getElementById("image-container");
1127 var ugoira = (imgContainer ? imgContainer.getElementsByTagName("canvas")[0] : undefined);
1128
1129 if (ugoira) {
1130 // Fix the missing data attributes.
1131 ugoira.setAttribute("data-ugoira-content-type", post.pixiv_ugoira_frame_data.content_type);
1132 ugoira.setAttribute("data-ugoira-frames", JSON.stringify(post.pixiv_ugoira_frame_data.data));
1133
1134 // Append the necessary script.
1135 var mainScript = document.createElement("script");
1136 mainScript.addEventListener("load", ugoiraInit, true); // Wait for this script to load before running the JavaScript that requires it.
1137 mainScript.src = "/assets/ugoira_player.js";
1138 document.head.appendChild(mainScript);
1139 }
1140 }
1141
1142 function endlessXMLJSONHandler(xml, optArg) {
1143 // Create a thumbnail listing from JSON results and pass it to the queue.
1144 var orderedIds = optArg;
1145 var posts = createThumbListing(xml, orderedIds);
1146 var newPage = document.createElement("div");
1147 newPage.className = "bbb-endless-page";
1148
1149 newPage.appendChild(posts);
1150 endlessQueuePage(newPage);
1151 }
1152
1153 /* Functions for XML page info */
1154 function searchPages(mode, optArg) {
1155 // Let other functions that don't require the API run (alternative to searchJSON) and retrieve various pages for info.
1156 var url; // If/else variable.
1157
1158 if (mode === "search" || mode === "notes" || mode === "favorites" || mode === "thumbnails") {
1159 url = updateURLQuery(location.href, {limit: thumbnail_count});
1160 bbb.flags.thumbs_xml = true;
1161
1162 fetchPages(url, "thumbnails");
1163 bbbStatus("posts", "new");
1164 }
1165 else if (mode === "endless") {
1166 url = endlessNexURL();
1167 bbb.flags.endless_xml = true;
1168
1169 fetchPages(url, "endless");
1170 bbbStatus("posts", "new");
1171 }
1172 else if (mode === "paginator") {
1173 url = (allowUserLimit() ? updateURLQuery(location.href, {limit: thumbnail_count}) : location.href);
1174 bbb.flags.paginator_xml = true;
1175
1176 fetchPages(url, "paginator");
1177 }
1178 else if (mode === "post_comments") {
1179 url = "/posts/" + optArg;
1180
1181 fetchPages(url, "post_comments", optArg);
1182 bbbStatus("post_comments", "new");
1183 }
1184 else if (mode === "hidden") {
1185 url = "/posts/" + optArg;
1186 bbb.flags.hidden_xml = true;
1187
1188 fetchPages(url, "hidden", optArg);
1189 bbbStatus("hidden", "new");
1190 }
1191 }
1192
1193 function fetchPages(url, mode, optArg, session, retries) {
1194 // Retrieve an actual page for certain pieces of information.
1195 var xmlhttp = new XMLHttpRequest();
1196 var xmlRetries = retries || 0;
1197 var xmlSession = session || window.bbbSession;
1198
1199 if (xmlhttp !== null) {
1200 xmlhttp.onreadystatechange = function() {
1201 if (xmlSession !== window.bbbSession) // If we end up receiving an xml response form a different page, reject it.
1202 xmlhttp.abort();
1203 else if (xmlhttp.readyState === 4) { // 4 = "loaded"
1204 if (xmlhttp.status === 200) { // 200 = "OK"
1205 var docEl = document.createElement("html");
1206
1207 docEl.innerHTML = xmlhttp.responseText;
1208
1209 if (mode === "paginator") {
1210 bbb.flags.paginator_xml = false;
1211
1212 replacePaginator(docEl);
1213 }
1214 else if (mode === "post_comments") {
1215 replaceComments(docEl, optArg);
1216 bbbStatus("post_comments", "done");
1217 }
1218 else if (mode === "thumbnails") {
1219 bbb.flags.thumbs_xml = false;
1220
1221 replaceThumbnails(docEl);
1222 bbbStatus("posts", "done");
1223 }
1224 else if (mode === "hidden") {
1225 bbb.flags.hidden_xml = false;
1226
1227 replaceHidden(docEl);
1228 bbbStatus("hidden", "done");
1229 }
1230 else if (mode === "endless") {
1231 bbb.flags.endless_xml = false;
1232
1233 endlessXMLPageHandler(docEl);
1234 bbbStatus("posts", "done");
1235 }
1236 }
1237 else if (xmlhttp.status !== 0) {
1238 if (xmlRetries < 1) {
1239 xmlRetries++;
1240 fetchPages(url, mode, optArg, xmlSession, xmlRetries);
1241 }
1242 else {
1243 var linkId = uniqueIdNum(); // Create a unique ID.
1244 var msg; // If/else variable.
1245
1246 if (mode === "hidden") {
1247 msg = "Error retrieving hidden thumbnails";
1248 bbbStatus("hidden", "error");
1249 }
1250 else if (mode === "thumbnails" || mode === "endless") {
1251 msg = "Error retrieving post information";
1252 bbbStatus("posts", "error");
1253 }
1254 else if (mode === "post_comments") {
1255 msg = "Error retrieving comment information";
1256 bbbStatus("post_comments", "error");
1257 }
1258 else if (mode === "paginator")
1259 msg = "Error updating paginator";
1260
1261 var noticeMsg = bbbNotice(msg + ' (HTML Code: ' + xmlhttp.status + ' ' + xmlhttp.statusText + '). (<a id="' + linkId + '" href="#">Retry</a>)', -1);
1262
1263 document.getElementById(linkId).addEventListener("click", function(event) {
1264 if (event.button !== 0)
1265 return;
1266
1267 closeBbbNoticeMsg(noticeMsg);
1268 searchPages(mode, optArg);
1269 event.preventDefault();
1270 }, false);
1271 }
1272 }
1273 }
1274 };
1275 xmlhttp.open("GET", url, true);
1276 xmlhttp.send(null);
1277 }
1278 }
1279
1280 function replacePaginator(el) {
1281 // Replace the contents inside the paginator div so as to preserve the original div and any event listeners attached to it.
1282 var oldPag = getPaginator();
1283 var newPag = getPaginator(el);
1284
1285 if (oldPag && newPag)
1286 oldPag.innerHTML = newPag.innerHTML;
1287 }
1288
1289 function replaceComments(docEl, postId) {
1290 // Fix hidden comments with information from a post.
1291 var divId = "post_" + postId;
1292 var commentDiv = document.getElementById(divId);
1293 var commentSection = docEl.getElementsByClassName("comments-for-post")[0];
1294 var comments = commentSection.getElementsByClassName("comment");
1295 var numComments = comments.length;
1296 var toShow = 6; // Number of comments to display.
1297 var post = scrapePost(docEl);
1298 var previewImg = commentDiv.getElementsByTagName("img")[0];
1299 var target = commentDiv.getElementsByClassName("comments-for-post")[0];
1300 var newContent = document.createDocumentFragment();
1301
1302 // Fix the image.
1303 if (post.preview_file_url) {
1304 if (post.file_url) {
1305 commentDiv.setAttribute("data-md5", post.md5);
1306 commentDiv.setAttribute("data-file-ext", post.file_ext);
1307 commentDiv.setAttribute("data-file-url", post.file_url);
1308 commentDiv.setAttribute("data-large-file-url", post.large_file_url);
1309 }
1310
1311 previewImg.src = post.preview_file_url;
1312 previewImg.alt = /([^\/]+)\.\w+$/.exec(post.preview_file_url)[1];
1313 commentDiv.setAttribute("data-preview-file-url", post.preview_file_url);
1314 }
1315
1316 // Fix the comments.
1317 if (numComments > toShow) {
1318 for (var i = 0, toHide = numComments - toShow; i < toHide; i++)
1319 comments[i].style.display = "none";
1320
1321 commentSection.getElementsByClassName("row notices")[0].innerHTML = '<span class="info" id="threshold-comments-notice-for-' + postId + '"> <a href="/comments?include_below_threshold=true&post_id=' + postId + '" data-remote="true">Show all comments</a> </span>';
1322 }
1323
1324 // Add it all in and get it ready.
1325 while (commentSection.firstElementChild)
1326 newContent.appendChild(commentSection.firstElementChild);
1327
1328 target.appendChild(newContent);
1329
1330 Danbooru.Comment.initialize_all();
1331 $("#" + divId + " .simple_form .dtext-preview").hide();
1332 $("#" + divId + " .simple_form input[value=Preview]").click(Danbooru.Dtext.click_button);
1333 }
1334
1335 function replaceThumbnails(docEl) {
1336 // Replace the thumbnails and paginator with new ones.
1337 // Thumb preparation.
1338 var newThumbs = document.createDocumentFragment();
1339 var newPosts = getPosts(docEl);
1340
1341 for (var i = 0, il = newPosts.length; i < il; i++)
1342 newThumbs.appendChild(newPosts[i]);
1343
1344 // Update the existing thumbnails with new ones.
1345 updateThumbListing(newThumbs);
1346
1347 // Replace paginator with new paginator.
1348 replacePaginator(docEl);
1349
1350 // Update the URL with the limit value.
1351 fixURLLimit();
1352
1353 // Cache thumbnails to the history for random searches.
1354 saveStateCache();
1355 }
1356
1357 function replaceHidden(docEl) {
1358 // Fix the hidden image placeholders with information from a post.
1359 var hiddenImgs = document.getElementsByClassName("bbb-hidden-thumb");
1360 var article = hiddenImgs[0];
1361
1362 // Hidden thumbnails no longer exist in the page so stop.
1363 if (!article)
1364 return;
1365
1366 var previewImg = article.getElementsByTagName("img")[0];
1367 var hiddenId = article.getAttribute("data-id");
1368 var bcc = bbb.cache.current;
1369 var post = scrapePost(docEl);
1370
1371 if (String(post.id) !== hiddenId) // Out of sync. Reset.
1372 searchPages("hidden", hiddenId);
1373 else if (post.preview_file_url) { // Update the thumbnail with the correct information.
1374 if (post.file_url) {
1375 article.setAttribute("data-md5", post.md5);
1376 article.setAttribute("data-file-ext", post.file_ext);
1377 article.setAttribute("data-file-url", post.file_url);
1378 article.setAttribute("data-large-file-url", post.large_file_url);
1379
1380 // Fix ddl.
1381 postDDL(article);
1382 }
1383
1384 previewImg.src = post.preview_file_url;
1385 article.setAttribute("data-preview-file-url", post.preview_file_url);
1386
1387 bcc.history.push(hiddenId);
1388 bcc.names[hiddenId] = /[^\/]+$/.exec(post.file_url || post.preview_file_url)[0];
1389
1390 article.bbbRemoveClass("bbb-hidden-thumb");
1391
1392 // Continue to the next image or finish by updating the cache.
1393 if (hiddenImgs[0]) {
1394 hiddenId = hiddenImgs[0].getAttribute("data-id");
1395 searchPages("hidden", hiddenId);
1396 }
1397 else
1398 updateThumbCache();
1399 }
1400 else { // The image information couldn't be found.
1401 bbb.flags.hidden_xml = true; // Flag the XML as active to signal a problem and disable further attempts.
1402
1403 updateThumbCache();
1404 bbbNotice("Error retrieving thumbnail information.", -1);
1405 bbbStatus("hidden", "error");
1406 }
1407 }
1408
1409 function endlessXMLPageHandler(docEl) {
1410 // Take thumbnails from a page and pass them to the queue or retrieve hidden posts as necessary.
1411 bbb.endless.new_paginator = getPaginator(docEl);
1412
1413 if (useAPI() && potentialHiddenPosts(gLoc, docEl))
1414 searchJSON("endless");
1415 else {
1416 var posts = getPosts(docEl);
1417 var newPage = document.createElement("div");
1418 newPage.className = "bbb-endless-page";
1419
1420 for (var i = 0, il = posts.length; i < il; i++)
1421 newPage.appendChild(posts[i]);
1422
1423 endlessQueuePage(newPage);
1424 }
1425 }
1426
1427 function isThere(url) {
1428 // Checks if file exists. Thanks to some random forum!
1429 var req = new XMLHttpRequest(); // XMLHttpRequest object.
1430 try {
1431 req.open("HEAD", url, false);
1432 req.send(null);
1433 return (req.status === 200 ? true : false);
1434 } catch(er) {
1435 return false;
1436 }
1437 }
1438
1439 /* Functions for retrieving page info */
1440 function scrapePost(pageEl) {
1441 // Retrieve info from the current document or a supplied element containing the HTML with it.
1442 var target = pageEl || document;
1443 var postContent = getPostContent(target);
1444 var imgContainer = postContent.container;
1445
1446 if (!imgContainer)
1447 return {};
1448
1449 var postEl = postContent.el;
1450 var postTag = (postEl ? postEl.tagName : undefined);
1451 var dataInfo = [imgContainer.getAttribute("data-file-url"), imgContainer.getAttribute("data-md5"), imgContainer.getAttribute("data-file-ext")];
1452 var directLink = getId("image-resize-link", target) || target.querySelector("#post-information ul li a[href^='/data/']");
1453 var twitterInfo = getMeta("twitter:image", target);
1454 var previewInfo = getMeta("og:image", target);
1455 var imgHeight = Number(imgContainer.getAttribute("data-height"));
1456 var imgWidth = Number(imgContainer.getAttribute("data-width"));
1457 var imgInfo = {
1458 md5: "",
1459 file_ext: "",
1460 file_url: "",
1461 large_file_url: "",
1462 preview_file_url: "",
1463 has_large: undefined,
1464 has_sound: (imgContainer.getAttribute("data-has-sound") === "true" ? true : false),
1465 id: Number(imgContainer.getAttribute("data-id")),
1466 fav_count: Number(imgContainer.getAttribute("data-fav-count")),
1467 has_children: (imgContainer.getAttribute("data-has-children") === "true" ? true : false),
1468 has_active_children: (postTag === "IMG" || postTag === "CANVAS" ? postEl.getAttribute("data-has-active-children") === "true" : !!target.getElementsByClassName("notice-parent")[0]),
1469 parent_id: (imgContainer.getAttribute("data-parent-id") ? Number(imgContainer.getAttribute("data-parent-id")) : null),
1470 rating: imgContainer.getAttribute("data-rating"),
1471 score: Number(imgContainer.getAttribute("data-score")),
1472 tag_string: imgContainer.getAttribute("data-tags"),
1473 pool_string: imgContainer.getAttribute("data-pools"),
1474 uploader_name: imgContainer.getAttribute("data-uploader"),
1475 is_deleted: (getMeta("post-is-deleted", target) === "false" ? false : true),
1476 is_flagged: (getMeta("post-is-flagged", target) === "false" ? false : true),
1477 is_pending: (getId("pending-approval-notice", target) ? true : false),
1478 is_banned: (imgContainer.getAttribute("data-flags").indexOf("banned") < 0 ? false : true),
1479 image_height: imgHeight || null,
1480 image_width: imgWidth || null,
1481 is_hidden: !postEl
1482 };
1483 var infoValues; // If/else variable.
1484
1485 // Try to extract the file's name and extension.
1486 if (dataInfo[1])
1487 infoValues = dataInfo;
1488 else if (directLink)
1489 infoValues = /data\/(\w+)\.(\w+)/.exec(directLink.href);
1490 else if (twitterInfo)
1491 infoValues = (twitterInfo.indexOf("sample") > -1 ? /data\/sample\/sample-(\w+)\.\w/.exec(twitterInfo) : /data\/(\w+)\.(\w+)/.exec(twitterInfo));
1492 else if (previewInfo)
1493 infoValues = /data\/preview\/(\w+?)\.\w/.exec(previewInfo);
1494
1495 if (infoValues) {
1496 var md5 = infoValues[1];
1497 var ext = infoValues[2];
1498
1499 // Test for the original image file extension if it is unknown.
1500 if (!ext && imgWidth) {
1501 var testExt = ["jpg", "png", "gif", "jpeg", "webm", "mp4"];
1502
1503 for (var i = 0, il = testExt.length; i < il; i++) {
1504 if (isThere("/data/" + md5 + "." + testExt[i])) {
1505 ext = testExt[i];
1506 break;
1507 }
1508 }
1509 }
1510
1511 var isUgoira = (postTag === "CANVAS" || (ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(imgInfo.tag_string)));
1512 var isAnimatedImg = /(?:^|\s)animated_(?:gif|png)(?:$|\s)/.test(imgInfo.tag_string);
1513
1514 if (isUgoira) {
1515 if (postTag === "CANVAS") {
1516 imgInfo.pixiv_ugoira_frame_data = {
1517 id: undefined, // Don't have this value.
1518 post_id: imgInfo.id,
1519 data: JSON.parse(postEl.getAttribute("data-ugoira-frames")),
1520 content_type: postEl.getAttribute("data-ugoira-content-type").replace(/"/gi, "")
1521 };
1522 }
1523 else {
1524 imgInfo.pixiv_ugoira_frame_data = {
1525 id: "", // Don't have this value.
1526 post_id: imgInfo.id,
1527 data: "",
1528 content_type: ""
1529 };
1530 }
1531 }
1532
1533 imgInfo.has_large = (!isAnimatedImg && ((imgWidth > 850 && ext !== "swf" && ext !== "webm" && ext !== "mp4") || isUgoira) ? true : false);
1534 imgInfo.md5 = md5;
1535 imgInfo.file_ext = ext;
1536 imgInfo.file_url = "/data/" + md5 + "." + ext;
1537 imgInfo.preview_file_url = (!imgHeight || ext === "swf" ? "/images/download-preview.png" : "/data/preview/" + md5 + ".jpg");
1538
1539 if (isUgoira)
1540 imgInfo.large_file_url = "/data/sample/sample-" + md5 + ".webm";
1541 else if (imgInfo.has_large)
1542 imgInfo.large_file_url = "/data/sample/sample-" + md5 + ".jpg";
1543 else
1544 imgInfo.large_file_url = "/data/" + md5 + "." + ext;
1545 }
1546 else if (previewInfo === "/images/download-preview.png")
1547 imgInfo.preview_file_url = "/images/download-preview.png";
1548
1549 return imgInfo;
1550 }
1551
1552 function scrapeThumb(article) {
1553 // Retrieve info from a thumbnail. Mainly for remaking thumbnails.
1554 var imgInfo = {
1555 md5: article.getAttribute("data-md5") || "",
1556 file_ext: article.getAttribute("data-file-ext") || "",
1557 file_url: article.getAttribute("data-file-url") || "",
1558 large_file_url: article.getAttribute("data-large-file-url") || "",
1559 preview_file_url: article.getAttribute("data-preview-file-url") || "",
1560 has_sound: (article.getAttribute("data-has-sound") === "true" ? true : false),
1561 id: Number(article.getAttribute("data-id")),
1562 pixiv_id: Number(article.getAttribute("data-pixiv-id")) || null,
1563 fav_count: Number(article.getAttribute("data-fav-count")),
1564 has_children: (article.getAttribute("data-has-children") === "true" ? true : false),
1565 has_active_children: article.bbbHasClass("post-status-has-children"), // Assumption. Basically a flag for the children class.
1566 parent_id: (article.getAttribute("data-parent-id") ? Number(article.getAttribute("data-parent-id")) : null),
1567 rating: article.getAttribute("data-rating"),
1568 score: Number(article.getAttribute("data-score")),
1569 tag_string: article.getAttribute("data-tags"),
1570 pool_string: article.getAttribute("data-pools"),
1571 uploader_name: article.getAttribute("data-uploader"),
1572 approver_id: article.getAttribute("data-approver-id") || null,
1573 is_deleted: (article.getAttribute("data-flags").indexOf("deleted") < 0 ? false : true),
1574 is_flagged: (article.getAttribute("data-flags").indexOf("flagged") < 0 ? false : true),
1575 is_pending: (article.getAttribute("data-flags").indexOf("pending") < 0 ? false : true),
1576 is_banned: (article.getAttribute("data-flags").indexOf("banned") < 0 ? false : true),
1577 image_height: Number(article.getAttribute("data-height")) || null,
1578 image_width: Number(article.getAttribute("data-width")) || null
1579 };
1580
1581 return imgInfo;
1582 }
1583
1584 function getId(elId, target) {
1585 // Retrieve an element by ID from either the current document or an element containing it.
1586 if (!target || target === document)
1587 return document.getElementById(elId);
1588 else if (target.id === elId)
1589 return target;
1590 else
1591 return target.querySelector("#" + elId);
1592
1593 return null;
1594 }
1595
1596 function getPostContent(pageEl) {
1597 // Retrieve the post content related elements.
1598 var target = pageEl || document;
1599 var imgContainer = getId("image-container", target);
1600
1601 if (!imgContainer)
1602 return {};
1603
1604 var img = getId("image", target);
1605 var swfObj = imgContainer.getElementsByTagName("object")[0];
1606 var swfEmb = (swfObj ? swfObj.getElementsByTagName("embed")[0] : undefined);
1607 var video = imgContainer.getElementsByTagName("video")[0];
1608 var ugoira = imgContainer.getElementsByTagName("canvas")[0];
1609 var other = imgContainer.querySelector("a[href^='/data/']");
1610 var el = swfEmb || video || ugoira || img || other;
1611 var secondaryEl = swfObj; // Other elements related to the main element. Only applies to flash for now.
1612
1613 return {container: imgContainer, el: el, secEl: secondaryEl};
1614 }
1615
1616 function getPosts(target) {
1617 // Return a list of posts depending from the document or a specific element.
1618 if (!target || target === document) // All posts in the document.
1619 return document.querySelectorAll(".post-preview");
1620 else if (target instanceof DocumentFragment || !target.bbbHasClass("post-preview")) // All posts in a specific element.
1621 return target.querySelectorAll(".post-preview");
1622 else // Single specific post.
1623 return [target];
1624 }
1625
1626 function getPaginator(target) {
1627 // Return the paginator of the document or a specific element.
1628 if (!target || target === document) // Paginator in the document.
1629 return document.getElementsByClassName("paginator")[0];
1630 else if (!target.bbbHasClass("paginator")) // Paginator in a specific element.
1631 return target.getElementsByClassName("paginator")[0];
1632 else // Single specific paginator.
1633 return target;
1634 }
1635
1636 function getThumbContainer(mode, pageEl) {
1637 // Retrieve the element that contains the thumbnails.
1638 var target = pageEl || document;
1639 var container; // If/else variable.
1640
1641 if (mode === "search") {
1642 container = getId("posts", target);
1643 container = (container ? container.getElementsByTagName("div")[0] : undefined);
1644 }
1645 else if (mode === "popular" || mode === "notes" || mode === "popular_view")
1646 container = getId("a-index", target);
1647 else if (mode === "pool" || mode === "favorite_group") {
1648 container = getId("a-show", target);
1649 container = (container ? container.getElementsByTagName("section")[0] : undefined);
1650 }
1651 else if (mode === "favorites")
1652 container = getId("posts", target);
1653
1654 // Can't always depend on the first post so it's used as a fallback.
1655 if (!container) {
1656 var firstPost = getPosts(target)[0];
1657
1658 if (firstPost)
1659 container = firstPost.parentNode;
1660 }
1661
1662 return container;
1663 }
1664
1665 function getThumbSibling(mode, pageEl) {
1666 // If it exists, retrieve the element that thumbnails should be added before.
1667 var target = pageEl || document;
1668 var sibling; // If/else variable.
1669
1670 var posts = getPosts(target);
1671 var numPosts = posts.length;
1672 var lastPost = (numPosts ? posts[numPosts - 1] : undefined);
1673 var lastPostParent = (lastPost ? lastPost.parentNode : undefined);
1674 var thumbContainer = getThumbContainer(mode, target);
1675 var lastPostEl = (lastPostParent && lastPostParent !== thumbContainer && lastPostParent.parentNode === thumbContainer ? lastPostParent : lastPost);
1676
1677 if (lastPostEl) {
1678 var contChildren = thumbContainer.children;
1679
1680 for (var i = contChildren.length - 1; i >= 0; i--) {
1681 if (contChildren[i] === lastPostEl) {
1682 sibling = contChildren[i + 1];
1683 break;
1684 }
1685 }
1686 }
1687 else if (mode === "pool" || mode === "notes" || mode === "favorites" || mode === "favorite_group") {
1688 var paginator = getPaginator(target);
1689 var endlessDiv = getId("bbb-endless-button-div", target);
1690
1691 sibling = endlessDiv || paginator;
1692 }
1693
1694 return sibling;
1695 }
1696
1697 function getPaginatorNextURL(target) {
1698 // Retrieve the next page's URL from the paginator.
1699 var paginator = getPaginator(target);
1700
1701 if (paginator) {
1702 var paginatorLinks = paginator.getElementsByTagName("a");
1703
1704 for (var i = paginatorLinks.length - 1; i >= 0; i--) {
1705 var paginatorLink = paginatorLinks[i];
1706
1707 if (paginatorLink.rel.toLowerCase() === "next" && paginatorLink.href) {
1708 return paginatorLink.href;
1709 }
1710 }
1711 }
1712
1713 return undefined;
1714 }
1715
1716 function getMeta(meta, pageEl) {
1717 // Get a value from an HTML meta tag.
1718 var target = pageEl || document;
1719 var metaTags = target.getElementsByTagName("meta");
1720
1721 for (var i = 0, il = metaTags.length; i < il; i++) {
1722 var tag = metaTags[i];
1723
1724 if (tag.name === meta || tag.getAttribute("property") === meta) {
1725 if (tag.hasAttribute("content"))
1726 return tag.content;
1727 else
1728 return undefined;
1729 }
1730 }
1731
1732 return undefined;
1733 }
1734
1735 function getVar(urlVar, targetUrl) {
1736 // Retrieve a value from a specified/current URL's query string.
1737 // Undefined refers to a param that isn't even declared. Null refers to a declared param that hasn't been defined with a value (&test&). An empty string ("") refers to a param that has been defined with nothing (&test=&).
1738 var url = targetUrl;
1739
1740 if (!url)
1741 url = location.search;
1742
1743 var result = url.split(new RegExp("[&\?]" + urlVar))[1];
1744
1745 if (result === undefined)
1746 return undefined;
1747
1748 result = result.split(/[#&]/, 1)[0].split("=", 2)[1];
1749
1750 if (result === undefined)
1751 return null;
1752 else
1753 return result;
1754 }
1755
1756 function getTagVar(urlVar, url) {
1757 // Retrieve a metatag's value from the tag portion of a specified/current URL's query string.
1758 if (!url)
1759 url = location.search;
1760
1761 var tags = getVar("tags", url);
1762
1763 // If the tags parameter isn't provided or has no value, the metatag is undefined.
1764 if (tags === null || tags === undefined)
1765 return undefined;
1766
1767 tags = tags.split(/\+|%20/g);
1768
1769 for (var i = 0, il = tags.length; i < il; i++) {
1770 var tag = decodeURIComponent(tags[i]);
1771
1772 if (tag.indexOf(urlVar + ":") === 0)
1773 return encodeURIComponent(tag.split(":")[1]); // Let the calling function decide whether it wants the decoded tag or not.
1774 }
1775
1776 return undefined;
1777 }
1778
1779 function getThumbQuery() {
1780 // Return the thumbnail URL query value.
1781 var query = "";
1782
1783 if (gLoc === "search" || gLoc === "favorites") {
1784 query = getCurTags();
1785 query = (query ? "?tags=" + query : "");
1786 }
1787 else if (gLoc === "pool")
1788 query = "?pool_id=" + /\/pools\/(\d+)/.exec(location.pathname)[1];
1789 else if (gLoc === "favorite_group")
1790 query = "?favgroup_id=" + /\/favorite_groups\/(\d+)/.exec(location.pathname)[1];
1791
1792 return query;
1793 }
1794
1795 function getCurTags() {
1796 // Retrieve the current search tags for URL use.
1797 var tags; // If/else variable.
1798
1799 if (gLoc === "search")
1800 tags = getVar("tags") || "";
1801 else if (gLoc === "favorites") {
1802 tags = document.getElementById("tags");
1803 tags = (tags ? tags.getAttribute("value").replace("fav:", "ordfav:").bbbSpaceClean() : ""); // Use getAttribute to avoid potential user changes to the input.
1804 }
1805
1806 return tags;
1807 }
1808
1809 function getLimit(url) {
1810 // Retrieve the current specified limit value. The query limit overrides the search limit.
1811 var loc = danbLoc(url);
1812 var limit; // If/else variable.
1813
1814 if (loc === "pool" || loc === "popular" || loc === "favorite_group")
1815 limit = thumbnail_count_default;
1816 else if (loc === "comments")
1817 limit = 5;
1818 else if (loc === "popular_view")
1819 limit = 101;
1820 else {
1821 var queryLimit = getQueryLimit(url);
1822 var searchLimit = getSearchLimit(url);
1823
1824 limit = (queryLimit !== undefined ? queryLimit : searchLimit);
1825 }
1826
1827 return limit;
1828 }
1829
1830 function getQueryLimit(url) {
1831 // Retrieve the limit from a URL's query portion. Always use the default for certain areas where the limit is not allowed.
1832 var queryLimit = getVar("limit", url);
1833
1834 if (queryLimit !== null && queryLimit !== undefined) { // Treat the limit as undefined when the limit parameter is declared with no value.
1835 queryLimit = decodeURIComponent(queryLimit);
1836
1837 if (queryLimit === "" || !/^\s*\d+/.test(queryLimit)) // No thumbnails show up when the limit is declared with a blank value or has no number directly after any potential white space.
1838 return 0;
1839 else // The query limit finds its value in a manner similar to parseInt. Dump leading spaces and grab numbers until a non-numerical character is hit.
1840 return parseInt(queryLimit, 10);
1841 }
1842
1843 return undefined;
1844 }
1845
1846 function getSearchLimit(url) {
1847 // Retrieve the limit from the search/limit tag used in a search.
1848 var searchLimit = getTagVar("limit", url);
1849
1850 if (searchLimit !== undefined) {
1851 searchLimit = decodeURIComponent(searchLimit);
1852
1853 if (searchLimit === "") // No thumbnails show up when the limit is declared but left blank.
1854 return 0;
1855 else if (!bbbIsNum(searchLimit.replace(/\s/g, "")) || searchLimit.indexOf(".") > -1 || Number(searchLimit) < 0) // Non-numerical, negative, and decimal values are ignored. Treat the limit as undefined.
1856 return undefined;
1857 else
1858 return Number(searchLimit);
1859 }
1860
1861 return undefined;
1862 }
1863
1864 /* Functions for the settings panel */
1865 function injectSettings() {
1866 var menu = document.getElementById("top");
1867 menu = (menu ? menu.getElementsByTagName("menu")[0] : undefined);
1868
1869 if (!menu)
1870 return;
1871
1872 var menuItems = menu.getElementsByTagName("li");
1873 var numMenuItems = menu.getElementsByTagName("li").length;
1874 var moreItem = menuItems[numMenuItems - 1];
1875
1876 for (var i = numMenuItems - 1; i >= 0; i--) {
1877 var menuLink = menuItems[i];
1878
1879 if (menuLink.textContent.indexOf("More") > -1) {
1880 moreItem = menuLink;
1881 break;
1882 }
1883 }
1884
1885 var link = document.createElement("a");
1886 link.href = "#";
1887 link.innerHTML = "BBB Settings";
1888 link.addEventListener("click", function(event) {
1889 if (event.button !== 0)
1890 return;
1891
1892 openMenu();
1893 event.preventDefault();
1894 }, false);
1895
1896 var item = document.createElement("li");
1897 item.appendChild(link);
1898
1899 if (moreItem)
1900 menu.insertBefore(item, moreItem);
1901 else
1902 menu.appendChild(item);
1903
1904 window.addEventListener("resize", adjustMenuTimer, false);
1905 }
1906
1907 function openMenu() {
1908 if (bbb.el.menu.window)
1909 return;
1910
1911 loadSettings();
1912 createMenu();
1913 }
1914
1915 function reloadMenu() {
1916 removeMenu();
1917 createMenu();
1918 }
1919
1920 function createMenu() {
1921 var menu = bbb.el.menu.window = document.createElement("div");
1922 menu.id = "bbb-menu";
1923 menu.style.visibility = "hidden";
1924
1925 var tip = bbb.el.menu.tip = document.createElement("div");
1926 tip.id = "bbb-expl";
1927 menu.appendChild(tip);
1928
1929 var header = document.createElement("h1");
1930 header.innerHTML = "Better Better Booru Settings";
1931 header.style.textAlign = "center";
1932 menu.appendChild(header);
1933
1934 var tabBar = document.createElement("div");
1935 tabBar.style.padding = "0px 15px";
1936 tabBar.addEventListener("click", function(event) {
1937 if (event.button !== 0)
1938 return;
1939
1940 var target = event.target;
1941
1942 if (target.href)
1943 changeTab(target);
1944
1945 event.preventDefault();
1946 }, false);
1947 menu.appendChild(tabBar);
1948
1949 var generalTab = bbb.el.menu.generalTab = document.createElement("a");
1950 generalTab.name = "general";
1951 generalTab.href = "#";
1952 generalTab.innerHTML = "General";
1953 generalTab.className = "bbb-tab bbb-active-tab";
1954 tabBar.appendChild(generalTab);
1955
1956 var blacklistTab = bbb.el.menu.blacklistTab = document.createElement("a");
1957 blacklistTab.name = "blacklist";
1958 blacklistTab.href = "#";
1959 blacklistTab.innerHTML = "Blacklist";
1960 blacklistTab.className = "bbb-tab";
1961 tabBar.appendChild(blacklistTab);
1962
1963 var borderTab = bbb.el.menu.borderTab = document.createElement("a");
1964 borderTab.name = "borders";
1965 borderTab.href = "#";
1966 borderTab.innerHTML = "Borders";
1967 borderTab.className = "bbb-tab";
1968 tabBar.appendChild(borderTab);
1969
1970 var layoutTab = bbb.el.menu.layoutTab = document.createElement("a");
1971 layoutTab.name = "layout";
1972 layoutTab.href = "#";
1973 layoutTab.innerHTML = "Layout";
1974 layoutTab.className = "bbb-tab";
1975 tabBar.appendChild(layoutTab);
1976
1977 var prefTab = bbb.el.menu.prefTab = document.createElement("a");
1978 prefTab.name = "pref";
1979 prefTab.href = "#";
1980 prefTab.innerHTML = "Preferences";
1981 prefTab.className = "bbb-tab";
1982 tabBar.appendChild(prefTab);
1983
1984 var helpTab = bbb.el.menu.helpTab = document.createElement("a");
1985 helpTab.name = "help";
1986 helpTab.href = "#";
1987 helpTab.innerHTML = "Help";
1988 helpTab.className = "bbb-tab";
1989 tabBar.appendChild(helpTab);
1990
1991 var scrollDiv = bbb.el.menu.scrollDiv = document.createElement("div");
1992 scrollDiv.className = "bbb-scroll-div";
1993 menu.appendChild(scrollDiv);
1994 scrollDiv.scrollTop = 0;
1995
1996 var generalPage = bbb.el.menu.generalPage = document.createElement("div");
1997 generalPage.className = "bbb-page";
1998 generalPage.style.display = "block";
1999 scrollDiv.appendChild(generalPage);
2000
2001 generalPage.bbbSection(bbb.sections.browse);
2002 generalPage.bbbSection(bbb.sections.control);
2003 generalPage.bbbSection(bbb.sections.endless);
2004 generalPage.bbbSection(bbb.sections.misc);
2005
2006 var blacklistPage = bbb.el.menu.blacklistPage = document.createElement("div");
2007 blacklistPage.className = "bbb-page";
2008 scrollDiv.appendChild(blacklistPage);
2009
2010 blacklistPage.bbbSection(bbb.sections.blacklist_options);
2011 blacklistPage.bbbBlacklistSection();
2012
2013 var layoutPage = bbb.el.menu.layoutPage = document.createElement("div");
2014 layoutPage.className = "bbb-page";
2015 scrollDiv.appendChild(layoutPage);
2016
2017 layoutPage.bbbSection(bbb.sections.sidebar);
2018 layoutPage.bbbSection(bbb.sections.notices);
2019 layoutPage.bbbSection(bbb.sections.misc_layout);
2020
2021 var bordersPage = bbb.el.menu.bordersPage = document.createElement("div");
2022 bordersPage.className = "bbb-page";
2023 scrollDiv.appendChild(bordersPage);
2024
2025 bordersPage.bbbSection(bbb.sections.border_options);
2026 bordersPage.bbbSection(bbb.sections.status_borders);
2027 bordersPage.bbbSection(bbb.sections.tag_borders);
2028
2029 var prefPage = bbb.el.menu.prefPage = document.createElement("div");
2030 prefPage.className = "bbb-page";
2031 scrollDiv.appendChild(prefPage);
2032
2033 prefPage.bbbSection(bbb.sections.script_settings);
2034 prefPage.bbbBackupSection();
2035
2036 var helpPage = bbb.el.menu.helpPage = document.createElement("div");
2037 helpPage.className = "bbb-page";
2038 scrollDiv.appendChild(helpPage);
2039
2040 helpPage.bbbTextSection('Thumbnail Matching Rules', 'For creating thumbnail matching rules, please consult the following examples:<ul><li><b>tag1</b> - Match posts with tag1.</li><li><b>tag1 tag2</b> - Match posts with tag1 AND tag2.</li><li><b>-tag1</b> - Match posts without tag1.</li><li><b>tag1 -tag2</b> - Match posts with tag1 AND without tag2.</li><li><b>~tag1 ~tag2</b> - Match posts with tag1 OR tag2.</li><li><b>~tag1 ~-tag2</b> - Match posts with tag1 OR without tag2.</li><li><b>tag1 ~tag2 ~tag3</b> - Match posts with tag1 AND either tag2 OR tag3.</li></ul><br>Wildcards can be used with any of the above methods:<ul><li><b>~tag1* ~-*tag2</b> - Match posts with tags starting with tag1 or posts without tags ending with tag2.</li></ul><br>Multiple match rules can be specified by using commas or separate lines when possible:<ul><li><b>tag1 tag2, tag3 tag4</b> - Match posts with tag1 AND tag2 or posts with tag3 AND tag4.</li><li><b>tag1 ~tag2 ~tag3, tag4</b> - Match posts with tag1 AND either tag2 OR tag3 or posts with tag4.</li></ul><br>Tags can be nested/grouped together by using parentheses coupled with percent signs:<ul><li><b>(% ~tag1 ~tag2 %) (% ~tag3 ~tag3 %)</b> - Match posts with either tag1 OR tag2 AND either tag3 OR tag4.</li><li><b>tag1 (% tag2, tag3 tag4 %)</b> - Match posts with tag1 AND tag2 or posts with tag1 AND tag3 AND tag4.</li><li><b>tag1 -(% tag2 tag3 %)</b> - Match posts with tag1 AND without tag2 AND tag3.</li><li><b>tag1 ~tag2 ~(% tag3 tag4 %)</b> - Match posts with tag1 and either tag2 OR tag3 AND tag4.</li></ul><br>The following metatags are supported:<ul><li><b>rating:safe</b> - Match posts rated safe. Accepted values include safe, explicit, and questionable.</li><li><b>status:pending</b> - Match pending posts. Accepted values include active, pending, flagged, banned, and deleted. Note that flagged posts also count as active posts.</li><li><b>user:albert</b> - Match posts made by the user Albert.</li><li><b>pool:1</b> - Match posts that are in the pool with an ID number of 1. Accepted values include pool ID numbers, "series" for posts in series category pools, "collection" for posts in collection category pools, "any" for posts in any pool, "none" for posts not in a pool, "active" for posts in an active (not deleted) pool, and "inactive" for posts only in an inactive (deleted) pool.</li><li><b>parent:1</b> - Match posts that have the post with an ID number of 1 as a parent. Accepted values include post ID numbers, "any" for any posts with a parent, and "none" for posts without a parent.</li><li><b>child:any</b> - Match any posts that have children. Accepted values include "any" for any posts with children and "none" for posts without children.</li><li><b>id:1</b> - Match posts with an ID number of 1.</li><li><b>score:1</b> - Match posts with a score of 1.</li><li><b>favcount:1</b> - Match posts with a favorite count of 1.</li><li><b>height:1</b> - Match posts with a height of 1.</li><li><b>width:1</b> - Match posts with a width of 1.</li></ul><br>The id, score, favcount, width, and height metatags can also use number ranges for matching:<ul><li><b>score:<5</b> - Match posts with a score less than 5.</li><li><b>score:>5</b> - Match posts with a score greater than 5.</li><li><b>score:<=5</b> or <b>score:..5</b> - Match posts with a score equal to OR less than 5.</li><li><b>score:>=5</b> or <b>score:5..</b> - Match posts with a score equal to OR greater than 5.</li><li><b>score:1..5</b> - Match posts with a score equal to OR greater than 1 AND equal to OR less than 5.</li></ul>');
2041 helpPage.bbbTextSection('Hotkeys', '<b>Posts</b><ul><li><b>B</b> - Open BBB menu.</li><li><b>1</b> - Resize to window.</li><li><b>2</b> - Resize to window width.</li><li><b>3</b> - Resize to window height.</li><li><b>4</b> - Reset/remove resizing.</li></ul><div style="font-size: smaller;">Note: Numbers refer to the main typing keypad and not the numeric keypad.</div><br><b>General</b><ul><li><b>B</b> - Open BBB menu.</li><li><b>E</b> - Toggle endless pages.</li><li><b>F</b> - Open quick search.</li><li><b>Shift + F</b> - Reset quick search.</li></ul>');
2042 helpPage.bbbTextSection('Questions, Suggestions, or Bugs?', 'If you have any questions, please use the Greasy Fork feedback forums located <a target="_blank" href="https://greasyfork.org/scripts/3575-better-better-booru/feedback">here</a>. If you\'d like to report a bug or make a suggestion, please create an issue on GitHub <a target="_blank" href="https://github.com/pseudonymous/better-better-booru/issues">here</a>.');
2043 helpPage.bbbTocSection();
2044
2045 var close = document.createElement("a");
2046 close.innerHTML = "Save & Close";
2047 close.href = "#";
2048 close.className = "bbb-button";
2049 close.style.marginRight = "15px";
2050 close.addEventListener("click", function(event) {
2051 if (event.button !== 0)
2052 return;
2053
2054 removeMenu();
2055 saveSettings();
2056 event.preventDefault();
2057 }, false);
2058
2059 var cancel = document.createElement("a");
2060 cancel.innerHTML = "Cancel";
2061 cancel.href = "#";
2062 cancel.className = "bbb-button";
2063 cancel.addEventListener("click", function(event) {
2064 if (event.button !== 0)
2065 return;
2066
2067 removeMenu();
2068 loadSettings();
2069 event.preventDefault();
2070 }, false);
2071
2072 var reset = document.createElement("a");
2073 reset.innerHTML = "Reset to Defaults";
2074 reset.href = "#";
2075 reset.className = "bbb-button";
2076 reset.style.cssFloat = "right";
2077 reset.style.color = "#ff1100";
2078 reset.addEventListener("click", function(event) {
2079 if (event.button !== 0)
2080 return;
2081
2082 loadDefaults();
2083 reloadMenu();
2084 event.preventDefault();
2085 }, false);
2086
2087 menu.appendChild(close);
2088 menu.appendChild(cancel);
2089 menu.appendChild(reset);
2090
2091 // Add menu to the DOM and manipulate the dimensions.
2092 document.body.appendChild(menu);
2093
2094 var viewHeight = document.documentElement.clientHeight;
2095 var barWidth = scrollbarWidth();
2096 var scrollDivDiff = menu.offsetHeight - scrollDiv.clientHeight;
2097
2098 scrollDiv.style.maxHeight = viewHeight - scrollDiv.bbbGetPadding().height - scrollDivDiff - 50 + "px"; // Subtract 50 for margins (25 each).
2099 scrollDiv.style.minWidth = 901 + barWidth + 3 + "px"; // Should keep the potential scrollbar from intruding on the original drawn layout if I'm thinking about this correctly. Seems to work in practice anyway.
2100 scrollDiv.style.paddingLeft = barWidth + 3 + "px";
2101
2102 var menuWidth = menu.offsetWidth;
2103
2104 menu.style.marginLeft = -menuWidth / 2 + "px";
2105 menu.style.visibility = "visible";
2106 }
2107
2108 function createSection(section) {
2109 var sectionFrag = document.createDocumentFragment();
2110 var i, il; // Loop variables.
2111
2112 if (section.header) {
2113 var sectionHeader = document.createElement("h2");
2114 sectionHeader.innerHTML = section.header;
2115 sectionHeader.className = "bbb-header";
2116 sectionFrag.appendChild(sectionHeader);
2117 }
2118
2119 if (section.text) {
2120 var sectionText = document.createElement("div");
2121 sectionText.innerHTML = section.text;
2122 sectionText.className = "bbb-section-text";
2123 sectionFrag.appendChild(sectionText);
2124 }
2125
2126 var sectionDiv = document.createElement("div");
2127 sectionDiv.className = "bbb-section-options";
2128 sectionFrag.appendChild(sectionDiv);
2129
2130 if (section.type === "general") {
2131 var settingList = section.settings;
2132 var sll = settingList.length;
2133 var halfway = (sll > 1 ? Math.ceil(sll / 2) : 0);
2134
2135 var leftSide = document.createElement("div");
2136 leftSide.className = "bbb-section-options-left";
2137 sectionDiv.appendChild(leftSide);
2138
2139 var rightSide = document.createElement("div");
2140 rightSide.className = "bbb-section-options-right";
2141 sectionDiv.appendChild(rightSide);
2142
2143 var optionTarget = leftSide;
2144
2145 for (i = 0; i < sll; i++) {
2146 var settingName = settingList[i];
2147
2148 if (halfway && i >= halfway)
2149 optionTarget = rightSide;
2150
2151 var newOption = createOption(settingName);
2152 optionTarget.appendChild(newOption);
2153 }
2154 }
2155 else if (section.type === "border") {
2156 var borderSettings = bbb.user[section.settings];
2157
2158 for (i = 0, il = borderSettings.length; i < il; i++) {
2159 var newBorderOption = createBorderOption(borderSettings, i);
2160 sectionDiv.appendChild(newBorderOption);
2161 }
2162
2163 var indexWrapper = document.createElement("div");
2164 indexWrapper.setAttribute("data-bbb-index", i);
2165 sectionDiv.appendChild(indexWrapper);
2166
2167 var borderDivider = document.createElement("div");
2168 borderDivider.className = "bbb-border-divider";
2169 indexWrapper.appendChild(borderDivider);
2170 }
2171
2172 return sectionFrag;
2173 }
2174
2175 Element.prototype.bbbSection = function(section) {
2176 this.appendChild(createSection(section));
2177 };
2178
2179 function createOption(settingName) {
2180 var optionObject = bbb.options[settingName];
2181 var userSetting = bbb.user[settingName];
2182 var i, il; // Loop variables.
2183
2184 var label = document.createElement("label");
2185 label.className = "bbb-general-label";
2186
2187 var textSpan = document.createElement("span");
2188 textSpan.className = "bbb-general-text";
2189 textSpan.innerHTML = optionObject.label;
2190 label.appendChild(textSpan);
2191
2192 var inputSpan = document.createElement("span");
2193 inputSpan.className = "bbb-general-input";
2194 label.appendChild(inputSpan);
2195
2196 var item; // Switch variable.
2197 var itemFrag = document.createDocumentFragment();
2198
2199 switch (optionObject.type) {
2200 case "dropdown":
2201 var txtOptions = optionObject.txtOptions;
2202 var numRange = optionObject.numRange;
2203 var numList = optionObject.numList;
2204 var selectOption; // If/else variable.
2205
2206 item = document.createElement("select");
2207 item.name = settingName;
2208
2209 if (txtOptions) {
2210 for (i = 0, il = txtOptions.length; i < il; i++) {
2211 var txtOption = txtOptions[i].split(":");
2212
2213 selectOption = document.createElement("option");
2214 selectOption.innerHTML = txtOption[0];
2215 selectOption.value = txtOption[1];
2216
2217 if (selectOption.value === String(userSetting))
2218 selectOption.selected = true;
2219
2220 item.appendChild(selectOption);
2221 }
2222 }
2223
2224 if (numList) {
2225 for (i = 0, il = numList.length; i < il; i++) {
2226 selectOption = document.createElement("option");
2227 selectOption.innerHTML = numList[i];
2228 selectOption.value = numList[i];
2229
2230 if (selectOption.value === String(userSetting))
2231 selectOption.selected = true;
2232
2233 item.appendChild(selectOption);
2234 }
2235 }
2236
2237 if (numRange) {
2238 var end = numRange[1];
2239
2240 for (i = numRange[0]; i <= end; i++) {
2241 selectOption = document.createElement("option");
2242 selectOption.innerHTML = i;
2243 selectOption.value = i;
2244
2245 if (selectOption.value === String(userSetting))
2246 selectOption.selected = true;
2247
2248 item.appendChild(selectOption);
2249 }
2250 }
2251
2252 item.addEventListener("change", function() {
2253 var selected = this.value;
2254 bbb.user[settingName] = (bbbIsNum(selected) ? Number(selected) : selected);
2255 bbb.settings.changed[settingName] = true;
2256 }, false);
2257 itemFrag.appendChild(item);
2258 break;
2259 case "checkbox":
2260 item = document.createElement("input");
2261 item.name = settingName;
2262 item.type = "checkbox";
2263 item.checked = userSetting;
2264 item.addEventListener("click", function(event) {
2265 if (event.button !== 0)
2266 return;
2267
2268 bbb.user[settingName] = this.checked;
2269 bbb.settings.changed[settingName] = true;
2270 }, false);
2271 itemFrag.appendChild(item);
2272 break;
2273 case "text":
2274 item = document.createElement("input");
2275 item.name = settingName;
2276 item.type = "text";
2277 item.value = userSetting;
2278 item.addEventListener("change", function() {
2279 bbb.user[settingName] = (optionObject.isTagInput ? this.value.bbbTagClean() : this.value.bbbSpaceClean());
2280 bbb.settings.changed[settingName] = true;
2281 }, false);
2282 itemFrag.appendChild(item);
2283
2284 if (optionObject.isTagInput) {
2285 var tagExpand = document.createElement("a");
2286 tagExpand.href = "#";
2287 tagExpand.className = "bbb-edit-link";
2288 tagExpand.innerHTML = "»";
2289 tagExpand.addEventListener("click", function(event) {
2290 if (event.button !== 0)
2291 return;
2292
2293 tagEditWindow(item, bbb.user, settingName);
2294 event.preventDefault();
2295 }, false);
2296 itemFrag.appendChild(tagExpand);
2297 }
2298 break;
2299 case "number":
2300 item = document.createElement("input");
2301 item.name = settingName;
2302 item.type = "text";
2303 item.value = userSetting;
2304 item.addEventListener("change", function() {
2305 bbb.user[settingName] = Number(this.value);
2306 bbb.settings.changed[settingName] = true;
2307 }, false);
2308 itemFrag.appendChild(item);
2309 break;
2310 default:
2311 bbbNotice('Unexpected menu object type for "' + optionObject.label + '". (Type: ' + optionObject.type + ')', -1);
2312 return label;
2313 }
2314 inputSpan.appendChild(itemFrag);
2315
2316 var explLink = document.createElement("a");
2317 explLink.innerHTML = "?";
2318 explLink.href = "#";
2319 explLink.className = "bbb-expl-link";
2320 explLink.bbbSetTip(bbb.options[settingName].expl);
2321 inputSpan.appendChild(explLink);
2322
2323 return label;
2324 }
2325
2326 function createBorderOption(borderSettings, index) {
2327 var borderItem = borderSettings[index];
2328 var isStatus = (borderItem.class_name ? true : false);
2329
2330 var borderSpacer = document.createElement("span");
2331 borderSpacer.className = "bbb-border-spacer";
2332
2333 var indexWrapper = document.createElement("div");
2334 indexWrapper.setAttribute("data-bbb-index", index);
2335
2336 var borderDivider = document.createElement("div");
2337 borderDivider.className = "bbb-border-divider";
2338 indexWrapper.appendChild(borderDivider);
2339
2340 var borderDiv = document.createElement("div");
2341 borderDiv.className = "bbb-border-div";
2342 indexWrapper.appendChild(borderDiv);
2343
2344 var borderBarDiv = document.createElement("div");
2345 borderBarDiv.className = "bbb-border-bar";
2346 borderDiv.appendChild(borderBarDiv);
2347
2348 var enableLabel = document.createElement("label");
2349 enableLabel.innerHTML = "Enabled:";
2350 borderBarDiv.appendChild(enableLabel);
2351
2352 var enableBox = document.createElement("input");
2353 enableBox.type = "checkbox";
2354 enableBox.checked = borderItem.is_enabled;
2355 enableBox.addEventListener("click", function(event) {
2356 if (event.button === 0)
2357 borderItem.is_enabled = this.checked;
2358 }, false);
2359 enableLabel.appendChild(enableBox);
2360
2361 var editSpan = document.createElement("span");
2362 editSpan.style.cssFloat = "right";
2363 borderBarDiv.appendChild(editSpan);
2364
2365 var moveButton = document.createElement("a");
2366 moveButton.href = "#";
2367 moveButton.innerHTML = "Move";
2368 moveButton.className = "bbb-border-button";
2369 moveButton.addEventListener("click", function(event) {
2370 if (event.button !== 0)
2371 return;
2372
2373 moveBorder(borderSettings, indexWrapper);
2374 event.preventDefault();
2375 }, false);
2376 moveButton.bbbSetTip("Click the blue highlighted area that indicates where you would like to move this border.");
2377 editSpan.appendChild(moveButton);
2378
2379 var previewButton = document.createElement("a");
2380 previewButton.href = "#";
2381 previewButton.innerHTML = "Preview";
2382 previewButton.className = "bbb-border-button";
2383 previewButton.bbbBorderPreview(borderItem);
2384 editSpan.appendChild(previewButton);
2385
2386 if (!isStatus) {
2387 var deleteButton = document.createElement("a");
2388 deleteButton.href = "#";
2389 deleteButton.innerHTML = "Delete";
2390 deleteButton.className = "bbb-border-button";
2391 deleteButton.addEventListener("click", function(event) {
2392 if (event.button !== 0)
2393 return;
2394
2395 deleteBorder(borderSettings, indexWrapper);
2396 event.preventDefault();
2397 }, false);
2398 editSpan.appendChild(deleteButton);
2399
2400 var newButton = document.createElement("a");
2401 newButton.href = "#";
2402 newButton.innerHTML = "New";
2403 newButton.className = "bbb-border-button";
2404 newButton.addEventListener("click", function(event) {
2405 if (event.button !== 0)
2406 return;
2407
2408 createBorder(borderSettings, indexWrapper);
2409 event.preventDefault();
2410 }, false);
2411 newButton.bbbSetTip("Click the blue highlighted area that indicates where you would like to create a border.");
2412 editSpan.appendChild(newButton);
2413 }
2414
2415 editSpan.appendChild(borderSpacer.cloneNode(false));
2416
2417 var helpButton = document.createElement("a");
2418 helpButton.href = "#";
2419 helpButton.innerHTML = "Help";
2420 helpButton.className = "bbb-border-button";
2421 helpButton.bbbSetTip("<b>Enabled:</b> When checked, the border will be applied. When unchecked, it won't be applied.<tipdesc>Status/Tags:</tipdesc> Describes the posts that the border should be applied to. For custom tag borders, you may specify the rules the post must match for the border to be applied. Please read the \"thumbnail matching rules\" section under the help tab for information about creating rules.<tipdesc>Color:</tipdesc> Set the color of the border. Hex RGB color codes (#000000, #FFFFFF, etc.) are the recommended values.<tipdesc>Style:</tipdesc> Set how the border looks. Please note that double only works with a border width of 3 or higher.<tipdesc>Move:</tipdesc> Move the border to a new position. Higher borders have higher priority. In the event of a post matching more than 4 borders, the first 4 borders get applied and the rest are ignored. If single color borders are enabled, only the first matching border is applied.<tipdesc>Preview:</tipdesc> Display a preview of the border's current settings.<tipdesc>Delete:</tipdesc> Remove the border and its settings.<tipdesc>New:</tipdesc> Create a new border.");
2422 editSpan.appendChild(helpButton);
2423
2424 var borderSettingsDiv = document.createElement("div");
2425 borderSettingsDiv.className = "bbb-border-settings";
2426 borderDiv.appendChild(borderSettingsDiv);
2427
2428 var nameLabel = document.createElement("label");
2429 nameLabel.className = "bbb-border-name";
2430 borderSettingsDiv.appendChild(nameLabel);
2431
2432 if (isStatus)
2433 nameLabel.innerHTML = "Status:" + borderItem.tags;
2434 else {
2435 nameLabel.innerHTML = "Tags:";
2436
2437 var nameInput = document.createElement("input");
2438 nameInput.type = "text";
2439 nameInput.value = borderItem.tags;
2440 nameInput.addEventListener("change", function() { borderItem.tags = this.value.bbbTagClean(); }, false);
2441 nameLabel.appendChild(nameInput);
2442
2443 var nameExpand = document.createElement("a");
2444 nameExpand.href = "#";
2445 nameExpand.className = "bbb-edit-link";
2446 nameExpand.innerHTML = "»";
2447 nameExpand.addEventListener("click", function(event) {
2448 if (event.button !== 0)
2449 return;
2450
2451 tagEditWindow(nameInput, borderItem, "tags");
2452 event.preventDefault();
2453 }, false);
2454 nameLabel.appendChild(nameExpand);
2455 }
2456
2457 var colorLabel = document.createElement("label");
2458 colorLabel.innerHTML = "Color:";
2459 colorLabel.className = "bbb-border-color";
2460 borderSettingsDiv.appendChild(colorLabel);
2461
2462 var colorInput = document.createElement("input");
2463 colorInput.type = "text";
2464 colorInput.value = borderItem.border_color;
2465 colorInput.addEventListener("change", function() { borderItem.border_color = this.value.bbbSpaceClean(); }, false);
2466 colorLabel.appendChild(colorInput);
2467
2468 var styleLabel = document.createElement("label");
2469 styleLabel.innerHTML = "Style:";
2470 styleLabel.className = "bbb-border-style";
2471 borderSettingsDiv.appendChild(styleLabel);
2472
2473 var styleDrop = document.createElement("select");
2474 styleDrop.addEventListener("change", function() { borderItem.border_style = this.value; }, false);
2475 styleLabel.appendChild(styleDrop);
2476
2477 var solidOption = document.createElement("option");
2478 solidOption.innerHTML = "solid";
2479 solidOption.value = "solid";
2480 styleDrop.appendChild(solidOption);
2481
2482 var dashedOption = document.createElement("option");
2483 dashedOption.innerHTML = "dashed";
2484 dashedOption.value = "dashed";
2485 styleDrop.appendChild(dashedOption);
2486
2487 var dottedOption = document.createElement("option");
2488 dottedOption.innerHTML = "dotted";
2489 dottedOption.value = "dotted";
2490 styleDrop.appendChild(dottedOption);
2491
2492 var doubleOption = document.createElement("option");
2493 doubleOption.innerHTML = "double";
2494 doubleOption.value = "double";
2495 styleDrop.appendChild(doubleOption);
2496
2497 var styleOptions = styleDrop.getElementsByTagName("option");
2498
2499 for (var i = 0; i < 4; i++) {
2500 if (styleOptions[i].value === borderItem.border_style) {
2501 styleOptions[i].selected = true;
2502 break;
2503 }
2504 }
2505
2506 return indexWrapper;
2507 }
2508
2509 function createTextSection(header, text) {
2510 var sectionFrag = document.createDocumentFragment();
2511
2512 if (header) {
2513 var sectionHeader = document.createElement("h2");
2514 sectionHeader.innerHTML = header;
2515 sectionHeader.className = "bbb-header";
2516 sectionFrag.appendChild(sectionHeader);
2517 }
2518
2519 if (text) {
2520 var desc = document.createElement("div");
2521 desc.innerHTML = text;
2522 desc.className = "bbb-section-text";
2523 sectionFrag.appendChild(desc);
2524 }
2525
2526 return sectionFrag;
2527 }
2528
2529 Element.prototype.bbbTextSection = function(header, text) {
2530 this.appendChild(createTextSection(header, text));
2531 };
2532
2533 function createBackupSection() {
2534 var sectionFrag = document.createDocumentFragment();
2535
2536 var sectionHeader = document.createElement("h2");
2537 sectionHeader.innerHTML = "Backup/Restore Settings";
2538 sectionHeader.className = "bbb-header";
2539 sectionFrag.appendChild(sectionHeader);
2540
2541 var sectionDiv = document.createElement("div");
2542 sectionDiv.className = "bbb-section-options";
2543 sectionFrag.appendChild(sectionDiv);
2544
2545 var backupTextarea = bbb.el.menu.backupTextarea = document.createElement("textarea");
2546 backupTextarea.className = "bbb-backup-area";
2547 sectionDiv.appendChild(backupTextarea);
2548
2549 var buttonDiv = document.createElement("div");
2550 buttonDiv.className = "bbb-section-options";
2551 sectionFrag.appendChild(buttonDiv);
2552
2553 var textBackup = document.createElement("a");
2554 textBackup.innerHTML = "Create Backup Text";
2555 textBackup.href = "#";
2556 textBackup.className = "bbb-button";
2557 textBackup.style.marginRight = "15px";
2558 textBackup.addEventListener("click", function(event) {
2559 if (event.button !== 0)
2560 return;
2561
2562 createBackupText();
2563 event.preventDefault();
2564 }, false);
2565 buttonDiv.appendChild(textBackup);
2566
2567 var pageBackup = document.createElement("a");
2568 pageBackup.innerHTML = "Create Backup Page";
2569 pageBackup.href = "#";
2570 pageBackup.className = "bbb-button";
2571 pageBackup.style.marginRight = "15px";
2572 pageBackup.addEventListener("click", function(event) {
2573 if (event.button !== 0)
2574 return;
2575
2576 createBackupPage();
2577 event.preventDefault();
2578 }, false);
2579 buttonDiv.appendChild(pageBackup);
2580
2581 var rightButtons = document.createElement("span");
2582 rightButtons.style.cssFloat = "right";
2583 buttonDiv.appendChild(rightButtons);
2584
2585 var restoreBackup = document.createElement("a");
2586 restoreBackup.innerHTML = "Restore Backup";
2587 restoreBackup.style.marginRight = "15px";
2588 restoreBackup.href = "#";
2589 restoreBackup.className = "bbb-button";
2590 restoreBackup.addEventListener("click", function(event) {
2591 if (event.button !== 0)
2592 return;
2593
2594 restoreBackupText();
2595 event.preventDefault();
2596 }, false);
2597 rightButtons.appendChild(restoreBackup);
2598
2599 var helpButton = document.createElement("a");
2600 helpButton.innerHTML = "Help";
2601 helpButton.href = "#";
2602 helpButton.className = "bbb-button";
2603 helpButton.bbbSetTip("Create copies of your settings that can be used for recovering lost/corrupted settings or transferring settings.<tiphead>Directions</tiphead>There are two options for creating a backup. Creating a text backup will provide a plain text format backup in the area provided that can be copied and saved where desired. Creating a backup page will open a new page that can be saved with the browser's \"save page\" or bookmark options. <br><br>To restore a backup, copy and paste the desired backup into the provided area and click \"restore backup\".");
2604 rightButtons.appendChild(helpButton);
2605
2606 return sectionFrag;
2607 }
2608
2609 Element.prototype.bbbBackupSection = function() {
2610 this.appendChild(createBackupSection());
2611 };
2612
2613 function createBlacklistSection() {
2614 var sectionFrag = document.createDocumentFragment();
2615
2616 var sectionHeader = document.createElement("h2");
2617 sectionHeader.innerHTML = "Blacklist";
2618 sectionHeader.className = "bbb-header";
2619 sectionFrag.appendChild(sectionHeader);
2620
2621 var sectionDiv = document.createElement("div");
2622 sectionDiv.className = "bbb-section-options";
2623 sectionFrag.appendChild(sectionDiv);
2624
2625 var blacklistTextarea = bbb.el.menu.blacklistTextarea = document.createElement("textarea");
2626 blacklistTextarea.className = "bbb-blacklist-area";
2627 blacklistTextarea.value = searchSingleToMulti(bbb.user.script_blacklisted_tags);
2628 blacklistTextarea.addEventListener("change", function() { bbb.user.script_blacklisted_tags = searchMultiToSingle(blacklistTextarea.value); }, false);
2629 sectionDiv.appendChild(blacklistTextarea);
2630
2631 var buttonDiv = document.createElement("div");
2632 buttonDiv.className = "bbb-section-options";
2633 sectionFrag.appendChild(buttonDiv);
2634
2635 var formatButton = document.createElement("a");
2636 formatButton.innerHTML = "Format";
2637 formatButton.href = "#";
2638 formatButton.className = "bbb-button";
2639 formatButton.addEventListener("click", function(event) {
2640 if (event.button !== 0)
2641 return;
2642
2643 var textareaString = searchMultiToSingle(blacklistTextarea.value);
2644
2645 blacklistTextarea.value = searchSingleToMulti(textareaString);
2646 event.preventDefault();
2647 }, false);
2648 buttonDiv.appendChild(formatButton);
2649
2650 var helpButton = document.createElement("a");
2651 helpButton.innerHTML = "Help";
2652 helpButton.href = "#";
2653 helpButton.className = "bbb-button";
2654 helpButton.style.cssFloat = "right";
2655 helpButton.bbbSetTip("Hide posts that match the specified tag(s).<tiphead>Directions</tiphead>Please read the \"thumbnail matching rules\" section under the help tab for information about creating matching rules for posts you wish to blacklist. Blank lines will be ignored and are only used for improved readability.<br><br> All commas outside of tag groups will be converted to new lines and all extra spaces and extra blank lines will be removed the next time the settings are opened. By using the \"format\" button, you can manually perform this action on the blacklist rules. <tiphead>Note</tiphead>When logged in, the account's \"blacklisted tags\" list will override this option. This behavior can be changed with the \"override blacklist\" option under the preferences tab.");
2656 buttonDiv.appendChild(helpButton);
2657
2658 return sectionFrag;
2659 }
2660
2661 Element.prototype.bbbBlacklistSection = function() {
2662 this.appendChild(createBlacklistSection());
2663 };
2664
2665 function createTocSection(page) {
2666 // Generate a Table of Contents based on the page's current section headers.
2667 var sectionFrag = document.createDocumentFragment();
2668 var pageSections = page.getElementsByTagName("h2");
2669
2670 var sectionHeader = document.createElement("h2");
2671 sectionHeader.innerHTML = "Table of Contents";
2672 sectionHeader.className = "bbb-header";
2673 sectionFrag.appendChild(sectionHeader);
2674
2675 var sectionText = document.createElement("div");
2676 sectionText.className = "bbb-section-text";
2677 sectionFrag.appendChild(sectionText);
2678
2679 var tocList = document.createElement("ol");
2680 tocList.className = "bbb-toc";
2681 sectionText.appendChild(tocList);
2682
2683 for (var i = 0, il = pageSections.length; i < il;) {
2684 var listItem = document.createElement("li");
2685 tocList.appendChild(listItem);
2686
2687 var linkItem = document.createElement("a");
2688 linkItem.textContent = pageSections[i].textContent;
2689 linkItem.href = "#" + (++i);
2690 listItem.appendChild(linkItem);
2691 }
2692
2693 tocList.addEventListener("click", function (event) {
2694 var targetValue = event.target.href;
2695
2696 if (event.button !== 0 || !targetValue)
2697 return;
2698
2699 var sectionTop = pageSections[targetValue.split("#")[1]].offsetTop;
2700
2701 bbb.el.menu.scrollDiv.scrollTop = sectionTop;
2702 event.preventDefault();
2703 }, false);
2704
2705 return sectionFrag;
2706 }
2707
2708 Element.prototype.bbbTocSection = function() {
2709 var page = this;
2710 page.insertBefore(createTocSection(page), page.firstElementChild);
2711 };
2712
2713 function newOption(type, def, lbl, expl, optPropObject) {
2714 /*
2715 * Option type notes
2716 * =================
2717 * By specifying a unique type, you can create a specialized menu option.
2718 *
2719 * Checkbox, text, and number do not require any extra properties.
2720 *
2721 * Dropdown requires either txtOptions, numRange, or numList.
2722 * txtOptions = Array containing a list of options and their values separated by a colon. (ex: ["option1:value1", "option2:value2"])
2723 * numRange = Array containing the starting and ending numbers of the number range.
2724 * numList = Array containing a list of the desired numbers.
2725 * If more than one of these is provided, they are added to the list in this order: txtOptions, numList, numRange
2726 */
2727
2728 var option = {
2729 type: type,
2730 def: def, // Default.
2731 label: lbl,
2732 expl: expl // Explanation.
2733 };
2734
2735 if (optPropObject) { // Additional properties provided in the form of an object.
2736 for (var i in optPropObject) {
2737 if (optPropObject.hasOwnProperty(i))
2738 option[i] = optPropObject[i];
2739 }
2740 }
2741
2742 return option;
2743 }
2744
2745 function newSection(type, settingList, header, text) {
2746 /*
2747 * Section type notes
2748 * ==================
2749 * Current section types are general and border.
2750 *
2751 * The setting list for general sections are provided in the form of an array containing the setting names as strings.
2752 * The setting list for border sections is the setting name containing the borders as a string.
2753 */
2754 return {
2755 type: type,
2756 settings: settingList,
2757 header: header,
2758 text: text
2759 };
2760 }
2761
2762 function newBorder(tags, isEnabled, color, style, className) {
2763 return {
2764 tags: tags,
2765 is_enabled: isEnabled,
2766 border_color: color,
2767 border_style: style,
2768 class_name: className
2769 };
2770 }
2771
2772 function borderSet() {
2773 var formatted = [];
2774
2775 for (var i = 0, il = arguments.length; i < il; i++) {
2776 var border = arguments[i];
2777
2778 formatted.push(newBorder(border[0], border[1], border[2], border[3], border[4]));
2779 }
2780
2781 return formatted;
2782 }
2783
2784 function resetBorderElements(section) {
2785 // Reset the list of border items after moving or creating a new border.
2786 var borderElements = section.children;
2787
2788 for (var i = 0, il = borderElements.length; i < il; i++) {
2789 var borderElement = borderElements[i];
2790
2791 borderElement.bbbRemoveClass("bbb-no-highlight");
2792 borderElement.setAttribute("data-bbb-index", i);
2793 }
2794 }
2795
2796 function deleteBorder(borderSettings, borderElement) {
2797 // Remove a border and if it's the last border, create a blank disabled one.
2798 var section = borderElement.parentNode;
2799 var index = Number(borderElement.getAttribute("data-bbb-index"));
2800
2801 section.removeChild(borderElement);
2802 borderSettings.splice(index,1);
2803
2804 if (!borderSettings[0]) {
2805 // If no borders are left, add a new blank border.
2806 var newBorderItem = newBorder("", false, "#000000", "solid");
2807 borderSettings.push(newBorderItem);
2808
2809 var newBorderElement = createBorderOption(borderSettings, 0);
2810 section.insertBefore(newBorderElement, section.firstElementChild);
2811 }
2812
2813 resetBorderElements(section);
2814 }
2815
2816 function moveBorder(borderSettings, borderElement) {
2817 // Prepare to move a border and wait for the user to click where it'll go.
2818 var section = borderElement.parentNode;
2819 var index = Number(borderElement.getAttribute("data-bbb-index"));
2820
2821 borderElement.bbbAddClass("bbb-no-highlight");
2822 borderElement.nextSibling.bbbAddClass("bbb-no-highlight");
2823 bbb.borderEdit = {mode: "move", settings: borderSettings, section: section, index: index, el: borderElement};
2824 section.bbbAddClass("bbb-insert-highlight");
2825 bbb.el.menu.window.addEventListener("click", insertBorder, true);
2826 }
2827
2828 function createBorder(borderSettings, borderElement) {
2829 // Prepare to create a border and wait for the user to click where it'll go.
2830 var section = borderElement.parentNode;
2831
2832 bbb.borderEdit = {mode: "new", settings: borderSettings, section: section};
2833 section.bbbAddClass("bbb-insert-highlight");
2834 bbb.el.menu.window.addEventListener("click", insertBorder, true);
2835 }
2836
2837 function insertBorder(event) {
2838 // Place either a new or moved border where indicated.
2839 var target = event.target;
2840 var section = bbb.borderEdit.section;
2841
2842 if (target.className === "bbb-border-divider" && event.button === 0) {
2843 var newIndex = Number(target.parentNode.getAttribute("data-bbb-index"));
2844 var borderSettings = bbb.borderEdit.settings;
2845
2846 if (bbb.borderEdit.mode === "new") { // Make a new border.
2847 var newBorderItem = newBorder("", false, "#000000", "solid");
2848 borderSettings.splice(newIndex, 0, newBorderItem);
2849
2850 var newBorderElement = createBorderOption(borderSettings, newIndex);
2851
2852 section.insertBefore(newBorderElement, section.children[newIndex]);
2853
2854 }
2855 else if (bbb.borderEdit.mode === "move") { // Move the border.
2856 var oldIndex = bbb.borderEdit.index;
2857
2858 if (newIndex !== oldIndex) {
2859 var borderItem = borderSettings.splice(oldIndex, 1)[0];
2860 var borderElement = bbb.borderEdit.el;
2861
2862 if (newIndex < oldIndex)
2863 borderSettings.splice(newIndex, 0, borderItem);
2864 else if (newIndex > oldIndex)
2865 borderSettings.splice(newIndex - 1, 0, borderItem);
2866
2867 section.insertBefore(borderElement, section.children[newIndex]);
2868 }
2869 }
2870 }
2871
2872 resetBorderElements(section);
2873 section.bbbRemoveClass("bbb-insert-highlight");
2874 bbb.el.menu.window.removeEventListener("click", insertBorder, true);
2875 }
2876
2877 function showTip(event, content, styleString) {
2878 var x = event.clientX;
2879 var y = event.clientY;
2880 var tip = bbb.el.menu.tip;
2881
2882 if (styleString)
2883 tip.setAttribute("style", styleString);
2884
2885 formatTip(event, tip, content, x, y);
2886 }
2887
2888 function hideTip() {
2889 bbb.el.menu.tip.removeAttribute("style");
2890 }
2891
2892 Element.prototype.bbbBorderPreview = function(borderItem) {
2893 this.addEventListener("click", function(event) {
2894 if (event.button !== 0)
2895 return;
2896
2897 showTip(event, "<img src=\"http://danbooru.donmai.us/data/preview/d34e4cf0a437a5d65f8e82b7bcd02606.jpg\" alt=\"IMAGE\" style=\"width: 105px; height: 150px; border-color: " + borderItem.border_color + "; border-style: " + borderItem.border_style + "; border-width: " + bbb.user.border_width + "px; padding:" + bbb.user.border_spacing + "px; line-height: 150px; text-align: center; vertical-align: middle;\">", "background-color: #FFFFFF;");
2898 event.preventDefault();
2899 }, false);
2900 this.addEventListener("mouseout", hideTip, false);
2901 };
2902
2903 Element.prototype.bbbSetTip = function(text) {
2904 var tip = bbb.el.menu.tip;
2905
2906 this.addEventListener("click", function(event) {
2907 if (event.button !== 0)
2908 return;
2909
2910 showTip(event, text, false);
2911 event.preventDefault();
2912 }, false);
2913 this.addEventListener("mouseout", function() { bbb.timers.hideTip = window.setTimeout(hideTip, 100); }, false);
2914 tip.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.hideTip); }, false);
2915 tip.addEventListener("mouseleave", hideTip, false);
2916 };
2917
2918 function changeTab(tab) {
2919 var activeTab = document.getElementsByClassName("bbb-active-tab")[0];
2920
2921 if (tab === activeTab)
2922 return;
2923
2924 activeTab.bbbRemoveClass("bbb-active-tab");
2925 bbb.el.menu[activeTab.name + "Page"].style.display = "none";
2926 bbb.el.menu.scrollDiv.scrollTop = 0;
2927 tab.bbbAddClass("bbb-active-tab");
2928 bbb.el.menu[tab.name + "Page"].style.display = "block";
2929 }
2930
2931 function tagEditWindow(input, object, prop) {
2932 var tagEditBlocker = document.createDocumentFragment();
2933
2934 var tagEditHeader = document.createElement("h2");
2935 tagEditHeader.innerHTML = "Tag Editor";
2936 tagEditHeader.className = "bbb-header";
2937 tagEditBlocker.appendChild(tagEditHeader);
2938
2939 var tagEditArea = bbb.el.menu.tagEditArea = document.createElement("textarea");
2940 tagEditArea.value = searchSingleToMulti(input.value);
2941 tagEditArea.className = "bbb-edit-area";
2942 tagEditBlocker.appendChild(tagEditArea);
2943
2944 var tagEditOk = function() {
2945 var tags = searchMultiToSingle(tagEditArea.value);
2946
2947 input.value = tags;
2948 object[prop] = tags;
2949 };
2950
2951 bbbDialog(tagEditBlocker, {ok: tagEditOk, cancel: true});
2952 }
2953
2954 function adjustMenuHeight() {
2955 var menu = bbb.el.menu.window;
2956 var scrollDiv = bbb.el.menu.scrollDiv;
2957 var viewHeight = document.documentElement.clientHeight;
2958 var scrollDivDiff = menu.offsetHeight - scrollDiv.clientHeight;
2959
2960 scrollDiv.style.maxHeight = viewHeight - scrollDiv.bbbGetPadding().height - scrollDivDiff - 50 + "px"; // Subtract 50 for margins (25 each).
2961 bbb.timers.adjustMenu = 0;
2962 }
2963
2964 function adjustMenuTimer() {
2965 if (!bbb.timers.adjustMenu && bbb.el.menu.window)
2966 bbb.timers.adjustMenu = window.setTimeout(adjustMenuHeight, 50);
2967 }
2968
2969 function removeMenu() {
2970 // Destroy the menu so that it gets rebuilt.
2971 var menu = bbb.el.menu.window;
2972
2973 if (!menu)
2974 return;
2975
2976 menu.parentNode.removeChild(menu);
2977 bbb.el.menu = {};
2978 }
2979
2980 function loadSettings() {
2981 // Load stored settings.
2982 var settings = localStorage.getItem("bbb_settings");
2983
2984 if (settings === null) {
2985 if (!getCookie().bbb_no_settings && !bbb.flags.local_storage_full) {
2986 // Alert the user when there are no settings so that new users know what to do and other users are aware their usual settings aren't in effect.
2987 var noSettingsNotice = function() {
2988 if (!getCookie().bbb_no_settings) {
2989 // Trigger the notice if it hasn't been displayed in another tab/window.
2990 var domain = location.protocol + "//" + location.hostname;
2991
2992 bbbNotice("No settings could be detected for " + domain + ". Please take a moment to set/restore your options by using the \"BBB Settings\" link in the Danbooru navigation bar.", 15);
2993 createCookie("bbb_no_settings", 1);
2994 }
2995
2996 document.removeEventListener("mousemove", noSettingsNotice, false);
2997 };
2998
2999 document.addEventListener("mousemove", noSettingsNotice, false);
3000 }
3001
3002 loadDefaults();
3003 }
3004 else {
3005 bbb.user = JSON.parse(settings);
3006 checkUser(bbb.user, bbb.options);
3007
3008 if (bbb.user.bbb_version !== bbb.options.bbb_version) {
3009 convertSettings("load");
3010 saveSettings();
3011 }
3012 }
3013 }
3014
3015 function loadDefaults() {
3016 // Load the default settings.
3017 bbb.user = {};
3018
3019 for (var i in bbb.options) {
3020 if (bbb.options.hasOwnProperty(i)) {
3021 if (typeof(bbb.options[i].def) !== "undefined")
3022 bbb.user[i] = bbb.options[i].def;
3023 else
3024 bbb.user[i] = bbb.options[i];
3025 }
3026 }
3027 }
3028
3029 function checkUser(user, options) {
3030 // Verify the user has all the base settings and add them with their default values if they don't.
3031 for (var i in options) {
3032 if (options.hasOwnProperty(i)) {
3033 if (typeof(user[i]) === "undefined") {
3034 if (typeof(options[i].def) !== "undefined")
3035 user[i] = options[i].def;
3036 else
3037 user[i] = options[i];
3038 }
3039 else if (typeof(user[i]) === "object" && !(user[i] instanceof Array))
3040 checkUser(user[i], options[i]);
3041 }
3042 }
3043 }
3044
3045 function saveSettings() {
3046 // Save the user settings to localStorage after making any necessary checks/adjustments.
3047 if (bbb.settings.changed.track_new && !bbb.user.track_new && bbb.user.track_new_data.viewed) // Reset new post tracking if it has been disabled.
3048 bbb.user.track_new_data = bbb.options.track_new_data.def;
3049
3050 if (bbb.settings.changed.thumb_cache_limit && thumb_cache_limit !== bbb.user.thumb_cache_limit) // Trim down the thumb cache as necessary if the limit has changed.
3051 adjustThumbCache();
3052
3053 if (bbb.settings.changed.thumbnail_count) // Update the link limit values if the user has changed the value.
3054 fixLimit(bbb.user.thumbnail_count);
3055
3056 if (bbb.settings.changed.blacklist_highlight_color && bbb.user.blacklist_highlight_color === "") // Use the default highlight color if the field is left blank.
3057 bbb.user.blacklist_highlight_color = "#CCCCCC";
3058
3059 bbb.settings.changed = {};
3060 localStorage.bbbSetItem("bbb_settings", JSON.stringify(bbb.user));
3061 }
3062
3063 function updateSettings() {
3064 // Change & save the settings without the panel. Accepts a comma delimited list of alternating settings and values: setting1, value1, setting2, value2
3065 loadSettings();
3066
3067 for (var i = 0, il = arguments.length; i < il; i += 2) {
3068 var setting = arguments[i].split(".");
3069 var value = arguments[i + 1];
3070 var settingPath = bbb.user;
3071
3072 for (var j = 0, jl = setting.length - 1; j < jl; j++)
3073 settingPath = settingPath[setting[j]];
3074
3075 settingPath[setting[j]] = value;
3076 bbb.settings.changed[setting[j]] = true;
3077 }
3078
3079 saveSettings();
3080 }
3081
3082 function convertSettings(reason) {
3083 // If the user settings are from an old version, attempt to convert some settings and update the version number. Settings will start conversion at the appropriate case and be allowed to run through every case after it until the end.
3084 var userVer = bbb.user.bbb_version;
3085 var scriptVer = bbb.options.bbb_version;
3086
3087 if (isOldVersion(userVer)) {
3088 switch (userVer) {
3089 case "6.0.2":
3090 // Temporary special tests for users that used the test version.
3091 if (/500$/.test(bbb.user.thumb_cache_limit))
3092 bbb.user.thumb_cache_limit = bbb.options.thumb_cache_limit.def;
3093
3094 if (!/\.(jpg|gif|png)/.test(localStorage.getItem("bbb_thumb_cache"))) {
3095 localStorage.removeItem("bbb_thumb_cache");
3096 loadThumbCache();
3097 }
3098
3099 if (bbb.user.tag_scrollbars === "false")
3100 bbb.user.tag_scrollbars = 0;
3101
3102 case "6.1":
3103 case "6.2":
3104 case "6.2.1":
3105 case "6.2.2":
3106 // Reset the thumb cache to deal with "download-preview" and incorrect extension entries.
3107 if (localStorage.getItem("bbb_thumb_cache")) {
3108 localStorage.removeItem("bbb_thumb_cache");
3109 loadThumbCache();
3110 }
3111
3112 // Convert the old hide_original_notice setting to the new show_resized_notice setting that replaces it.
3113 if (bbb.user.hide_original_notice)
3114 bbb.user.show_resized_notice = "sample";
3115
3116 // Set the new show_banned setting to true if show_deleted is true.
3117 if (bbb.user.show_deleted)
3118 bbb.user.show_banned = true;
3119
3120 // Add a custom border for banned posts to match the other hidden post borders.
3121 if (!/(?:^|\s)status:banned(?:$|\s)/i.test(JSON.stringify(bbb.user.tag_borders)))
3122 bbb.user.tag_borders.push(newBorder("status:banned", false, "#000000", "solid"));
3123
3124 // Warn about uninstalling old version from Userscripts.org
3125 if (reason !== "backup")
3126 bbbNotice("You have just been updated from a version of this script that was hosted on Userscripts.org. Before continuing any further, please open your userscript manager and remove any versions of this script older than version 6.3 that may be there.", 0);
3127
3128 case "6.3":
3129 case "6.3.1":
3130 case "6.3.2":
3131 case "6.4":
3132 case "6.5":
3133 case "6.5.1":
3134 case "6.5.2":
3135 case "6.5.3":
3136 case "6.5.4":
3137 // Copy over settings to their new names.
3138 if (bbb.user.image_drag_scroll)
3139 bbb.user.post_drag_scroll = bbb.user.image_drag_scroll;
3140
3141 if (bbb.user.image_resize)
3142 bbb.user.post_resize = bbb.user.image_resize;
3143
3144 if (bbb.user.image_resize_mode)
3145 bbb.user.post_resize_mode = bbb.user.image_resize_mode;
3146
3147 if (bbb.user.tag_scrollbars)
3148 bbb.user.post_tag_scrollbars = bbb.user.tag_scrollbars;
3149
3150 // Convert old settings.
3151 if (bbb.user.autoscroll_image)
3152 bbb.user.autoscroll_post = "post";
3153
3154 if (bbb.user.search_add)
3155 bbb.user.search_add = "link";
3156
3157 if (bbb.user.override_account) {
3158 bbb.user.override_blacklist = "always";
3159 bbb.user.override_resize = true;
3160 bbb.user.override_sample = true;
3161 }
3162
3163 case "7.0":
3164 // Reduce the maximum thumb cache limit.
3165 if (bbb.user.thumb_cache_limit > 10000)
3166 bbb.user.thumb_cache_limit = 10000;
3167
3168 case "7.1":
3169 case "7.2":
3170 case "7.2.1":
3171 case "7.2.2":
3172 case "7.2.3":
3173 case "7.2.4":
3174 break;
3175 }
3176
3177 cleanUser();
3178 bbb.user.bbb_version = scriptVer;
3179 }
3180 else if (userVer !== scriptVer) // Revert the version number for downgrades so that conversion can properly work on the settings again for a future upgrade.
3181 bbb.user.bbb_version = scriptVer;
3182 }
3183
3184 function cleanUser() {
3185 // Verify the user doesn't have any settings that aren't in the base settings and delete them if they do.
3186 var user = bbb.user;
3187
3188 for (var i in user) {
3189 if (user.hasOwnProperty(i)) {
3190 if (typeof(bbb.options[i]) === "undefined")
3191 delete user[i];
3192 }
3193 }
3194 }
3195
3196 function createBackupText() {
3197 // Create a plain text version of the settings.
3198 var textarea = bbb.el.menu.backupTextarea;
3199 textarea.value = "Better Better Booru v" + bbb.user.bbb_version + " Backup (" + timestamp() + "):\r\n\r\n" + JSON.stringify(bbb.user) + "\r\n";
3200 textarea.focus();
3201 textarea.setSelectionRange(0,0);
3202 }
3203
3204 function createBackupPage() {
3205 // Open a new tab/window and place the setting text in it.
3206 window.open(('data:text/html,<!doctype html><html style="background-color: #FFFFFF;"><head><meta charset="UTF-8" /><title>Better Better Booru v' + bbb.user.bbb_version + ' Backup (' + timestamp() + ')</title></head><body style="background-color: #FFFFFF; color: #000000; padding: 20px; word-wrap: break-word;">' + JSON.stringify(bbb.user) + '</body></html>').replace(/#/g, encodeURIComponent("#")));
3207 }
3208
3209 function restoreBackupText() {
3210 // Load the backup text provided into the script.
3211 var textarea = bbb.el.menu.backupTextarea;
3212 var backupString = textarea.value.replace(/\r?\n/g, "").match(/\{.+\}/);
3213
3214 if (backupString) {
3215 try {
3216 bbb.user = JSON.parse(backupString); // This is where we expect an error.
3217 checkUser(bbb.user, bbb.options);
3218 convertSettings("backup");
3219 reloadMenu();
3220 bbbDialog("Backup settings loaded successfully. After reviewing the settings to ensure they are correct, please click \"save & close\" to finalize the restore.");
3221 }
3222 catch (error) {
3223 if (error instanceof SyntaxError)
3224 bbbDialog("The backup does not appear to be formatted correctly. Please make sure everything was pasted correctly/completely and that only one backup is provided.");
3225 else
3226 bbbDialog("Unexpected error: " + error.message);
3227 }
3228 }
3229 else
3230 bbbDialog("A backup could not be detected in the text provided. Please make sure everything was pasted correctly/completely.");
3231 }
3232
3233 /* Post functions */
3234 function swapImageInit() {
3235 // Create the custom elements for swapping between the sample and original images and set them up.
3236 createSwapElements();
3237
3238 if (image_swap_mode === "load")
3239 swapImageLoad();
3240 else if (image_swap_mode === "view")
3241 swapImageView();
3242 }
3243
3244 function createSwapElements() {
3245 // Create the elements for swapping between the original and sample image.
3246 var post = bbb.post.info;
3247
3248 if (!post.has_large)
3249 return;
3250
3251 // Remove the original notice (it's not always there) and replace it with our own.
3252 var img = document.getElementById("image");
3253 var imgContainer = document.getElementById("image-container");
3254 var resizeNotice = document.getElementById("image-resize-notice");
3255
3256 if (resizeNotice)
3257 resizeNotice.parentNode.removeChild(resizeNotice);
3258
3259 var bbbResizeNotice = bbb.el.resizeNotice = document.createElement("div");
3260 bbbResizeNotice.id = "image-resize-notice";
3261 bbbResizeNotice.className = "ui-corner-all ui-state-highlight notice notice-resized";
3262 bbbResizeNotice.style.position = "relative";
3263 bbbResizeNotice.style.display = "none";
3264 bbbResizeNotice.innerHTML = '<span id="bbb-resize-status"></span> (<a href="" id="bbb-resize-link"></a>)<span style="display: block;" class="close-button ui-icon ui-icon-closethick" id="close-resize-notice"></span>';
3265
3266 var resizeStatus = bbb.el.resizeStatus = getId("bbb-resize-status", bbbResizeNotice);
3267 var resizeLink = bbb.el.resizeLink = getId("bbb-resize-link", bbbResizeNotice);
3268 var closeResizeNotice = bbb.el.closeResizeNotice = getId("close-resize-notice", bbbResizeNotice);
3269
3270 closeResizeNotice.addEventListener("click", function(event) {
3271 if (event.button !== 0)
3272 return;
3273
3274 var showResNot = bbb.user.show_resized_notice;
3275
3276 bbbResizeNotice.style.display = "none";
3277
3278 if (img.src.indexOf("/sample/") < 0) { // Original image.
3279 if (showResNot === "original")
3280 showResNot = "none";
3281 else if (showResNot === "all")
3282 showResNot = "sample";
3283
3284 bbbNotice("Settings updated. The resized notice will now be hidden when viewing original images. You may change this setting under \"notices\" in the settings panel.", 10);
3285 }
3286 else { // Sample image.
3287 if (showResNot === "sample")
3288 showResNot = "none";
3289 else if (showResNot === "all")
3290 showResNot = "original";
3291
3292 bbbNotice("Settings updated. The resized notice will now be hidden when viewing sample images. You may change this setting under \"notices\" in the settings panel.", 10);
3293 }
3294
3295 updateSettings("show_resized_notice", showResNot);
3296 }, false);
3297
3298 // Create a swap image link in the sidebar options section.
3299 var optionsSectionList = document.getElementById("post-options");
3300 optionsSectionList = (optionsSectionList ? optionsSectionList.getElementsByTagName("ul")[0] : undefined);
3301
3302 var firstOption = (optionsSectionList ? optionsSectionList.getElementsByTagName("li")[0] : undefined);
3303
3304 var swapListItem = document.createElement("li");
3305
3306 var swapLink = bbb.el.swapLink = document.createElement("a");
3307 swapListItem.appendChild(swapLink);
3308
3309 swapLink.addEventListener("click", function(event) {
3310 if (event.button !== 0)
3311 return;
3312
3313 swapPost();
3314 event.preventDefault();
3315 }, false);
3316
3317 // Prepare the element text, etc.
3318 swapImageUpdate((load_sample_first ? "sample" : "original"));
3319
3320 // Add the elements to the document.
3321 imgContainer.parentNode.insertBefore(bbbResizeNotice, imgContainer);
3322
3323 if (optionsSectionList && firstOption)
3324 optionsSectionList.insertBefore(swapListItem, firstOption);
3325 }
3326
3327 function swapImageLoad() {
3328 // Set up the post to load the content before displaying it.
3329 var post = bbb.post.info;
3330
3331 if (!post.has_large)
3332 return;
3333
3334 var img = document.getElementById("image");
3335 var bbbLoader = bbb.el.bbbLoader;
3336 var resizeStatus = bbb.el.resizeStatus;
3337 var resizeLink = bbb.el.resizeLink;
3338 var swapLink = bbb.el.swapLink;
3339
3340 resizeLink.addEventListener("click", function(event) {
3341 if (event.button !== 0)
3342 return;
3343
3344 swapPost();
3345 event.preventDefault();
3346 }, false);
3347 bbbLoader.addEventListener("load", function() { // Change the image to the successfully loaded sample/original image.
3348 if (!bbb.post.swapped)
3349 bbb.post.swapped = true;
3350
3351 if (bbbLoader.src !== "about:blank") {
3352 img.src = bbbLoader.src;
3353 bbbLoader.src = "about:blank";
3354 }
3355 }, false);
3356 bbbLoader.addEventListener("error", function(event) { // State the image has failed loading and provide a retry link.
3357 if (bbbLoader.src !== "about:blank") {
3358 var currentImg = (bbbLoader.src.indexOf("/sample/") < 0 ? "Original" : "Sample");
3359
3360 resizeStatus.innerHTML = currentImg + " image loading failed!";
3361 resizeLink.innerHTML = "retry";
3362 swapLink.innerHTML = "View " + currentImg.toLowerCase();
3363 bbbLoader.src = "about:blank";
3364 }
3365
3366 event.preventDefault();
3367 }, false);
3368 img.addEventListener("load", function() { // Update the swap image elements.
3369 if (bbbLoader.src === "about:blank") {
3370 if (img.src.indexOf("/sample/") < 0) // Original image loaded.
3371 swapImageUpdate("original");
3372 else // Sample image loaded.
3373 swapImageUpdate("sample");
3374 }
3375
3376 if (bbb.post.swapped)
3377 resizePost("swap");
3378 }, false);
3379 }
3380
3381 function swapImageView() {
3382 // Set up the post to display the content as it loads.
3383 var post = bbb.post.info;
3384
3385 if (!post.has_large)
3386 return;
3387
3388 var img = document.getElementById("image");
3389 var resizeStatus = bbb.el.resizeStatus;
3390 var resizeLink = bbb.el.resizeLink;
3391 var swapLink = bbb.el.swapLink;
3392
3393 resizeLink.addEventListener("click", function(event) {
3394 if (event.button !== 0)
3395 return;
3396
3397 swapPost();
3398 event.preventDefault();
3399 }, false);
3400 img.addEventListener("error", function(event) { // State the image has failed loading and provide a link to the other image.
3401 if (img.src !== "about:blank") {
3402 var currentImg = (img.src.indexOf("/sample/") < 0 ? "Original" : "Sample");
3403 var otherImg = (currentImg === "Original" ? "sample" : "original");
3404
3405 resizeStatus.innerHTML = currentImg + " image loading failed!";
3406 resizeLink.innerHTML = "view " + otherImg;
3407 swapLink.innerHTML = "View " + otherImg;
3408 }
3409
3410 event.preventDefault();
3411 }, false);
3412 }
3413
3414 function noteToggleInit() {
3415 // Override Danbooru's image click handler for toggling notes with a custom one.
3416 var image = document.getElementById("image");
3417
3418 if (!image)
3419 return;
3420
3421 image.bbbOverrideClick(function(event) {
3422 if (!Danbooru.Note.TranslationMode.active && !bbb.drag_scroll.moved)
3423 Danbooru.Note.Box.toggle_all();
3424 });
3425 }
3426
3427 function noteToggleLinkInit() {
3428 // Make a "toggle notes" link in the sidebar options or prepare an existing link.
3429 var toggleLink = document.getElementById("bbb-note-toggle");
3430
3431 if (!toggleLink) {
3432 var before = document.getElementById((isLoggedIn() ? "add-notes-list" : "random-post"));
3433
3434 if (before) {
3435 var listNoteToggle = document.createElement("li");
3436 listNoteToggle.innerHTML = '<a href="#" id="bbb-note-toggle">Toggle notes</a>';
3437 before.parentNode.insertBefore(listNoteToggle, before);
3438 toggleLink = document.getElementById("bbb-note-toggle");
3439 }
3440 }
3441
3442 if (toggleLink) {
3443 document.getElementById("bbb-note-toggle").addEventListener("click", function(event) {
3444 if (event.button !== 0)
3445 return;
3446
3447 Danbooru.Note.Box.toggle_all();
3448 event.preventDefault();
3449 }, false);
3450 }
3451 }
3452
3453 function translationModeInit() {
3454 // Set up translation mode.
3455 var post = bbb.post.info;
3456 var postContent = getPostContent();
3457 var postEl = postContent.el;
3458 var postTag = (postEl ? postEl.tagName : undefined);
3459 var translateLink = document.getElementById("translate");
3460 var toggleFunction; // If/else variable.
3461
3462 if (post.file_ext !== "webm" && post.file_ext !== "mp4" && post.file_ext !== "swf") { // Don't allow translation functions on videos or flash.
3463 if (postTag !== "VIDEO") { // Make translation mode work on non-video content.
3464 // Set up/override the translate link and hotkey if notes aren't locked.
3465 if (!document.getElementById("note-locked-notice") && translateLink) {
3466 translateLink.bbbOverrideClick(Danbooru.Note.TranslationMode.toggle);
3467 createHotkey("78", Danbooru.Note.TranslationMode.toggle);
3468 }
3469 }
3470 else { // Allow note viewing on ugoira webm video samples, but don't allow editing.
3471 toggleFunction = function(event) {
3472 bbbNotice('Note editing is not allowed while using the ugoira video sample. Please use the <a href="' + updateURLQuery(location.href, {original: "1"}) + '">original</a> ugoira version for note editing.', -1);
3473 event.preventDefault();
3474 };
3475
3476 Danbooru.Note.TranslationMode.start = toggleFunction;
3477 Danbooru.Note.Edit.show = toggleFunction;
3478
3479 if (translateLink)
3480 translateLink.bbbOverrideClick(toggleFunction);
3481
3482 createHotkey("78", toggleFunction); // Override the hotkey for "N".
3483 }
3484 }
3485 else { // Provide a warning for unsupported content.
3486 toggleFunction = function(event) {
3487 bbbNotice('Note editing is not allowed on flash/video content.', -1);
3488 event.preventDefault();
3489 };
3490
3491 Danbooru.Note.TranslationMode.start = toggleFunction;
3492 Danbooru.Note.Edit.show = toggleFunction;
3493
3494 if (translateLink)
3495 translateLink.bbbOverrideClick(toggleFunction);
3496
3497 createHotkey("78", toggleFunction); // Override the hotkey for "N".
3498 }
3499 }
3500
3501 function disableEmbeddedNotes() {
3502 // Disable embedded notes for viewing and re-enable them for note editing.
3503 var useEmbedded = (getMeta("post-has-embedded-notes") === "true");
3504 var noteContainer = document.getElementById("note-container");
3505 var notesSection = document.getElementById("notes");
3506 var notes = (notesSection ? notesSection.getElementsByTagName("article") : undefined);
3507
3508 if (!disable_embedded_notes || !useEmbedded || !notes[0])
3509 return;
3510
3511 Danbooru.Note.embed = false;
3512
3513 var post = bbb.post.info;
3514 var postContent = getPostContent();
3515 var postEl = postContent.el;
3516 var postTag = (postEl ? postEl.tagName : undefined);
3517
3518 // Stop here for content that doesn't allow note editing.
3519 if (post.file_ext === "webm" || post.file_ext === "mp4" || post.file_ext === "swf" || postTag === "VIDEO")
3520 return;
3521
3522 // Save the original note functions.
3523 var origEditFunction = Danbooru.Note.Edit.show;
3524
3525 // Create override functions.
3526 var toggleFunction = function(event) {
3527 var translateLink = document.getElementById("translate");
3528
3529 if (event.type === "click" && (event.target !== translateLink || event.button !== 0))
3530 return;
3531
3532 resetFunction();
3533 Danbooru.Note.TranslationMode.toggle(event);
3534
3535 event.preventDefault();
3536 event.stopPropagation();
3537 };
3538
3539 var editFunction = function(editTarget) { // This function is actually assigned under an anonymous function in Danbooru. The first argument is an element from the div.
3540 resetFunction();
3541 origEditFunction(editTarget);
3542 };
3543
3544 var resetFunction = function() {
3545 // Remove all overrides/overwrites.
3546 Danbooru.Note.Edit.show = origEditFunction;
3547 document.removeEventListener("click", toggleFunction, true);
3548
3549 if (!document.getElementById("note-locked-notice"))
3550 createHotkey("78", Danbooru.Note.TranslationMode.toggle);
3551
3552 // Reset notes with embedded notes enabled.
3553 Danbooru.Note.embed = true;
3554 noteContainer.innerHTML = "";
3555 Danbooru.Note.load_all("bbb");
3556 };
3557
3558 document.addEventListener("click", toggleFunction, true); // Override all other click events for the translate link.
3559 createHotkey("78", toggleFunction); // Override the hotkey for "N".
3560 Danbooru.Note.Edit.show = editFunction; // Overwrite the note edit function.
3561 }
3562
3563 function alternateImageSwap() {
3564 // Override Danbooru's image click handler for toggling notes with a custom one that swaps the image.
3565 var post = bbb.post.info;
3566 var image = document.getElementById("image");
3567
3568 if (post.has_large && image) {
3569 image.bbbOverrideClick(function(event) {
3570 if (!Danbooru.Note.TranslationMode.active && !bbb.drag_scroll.moved)
3571 swapPost();
3572 });
3573 }
3574
3575 // Set up the "toggle notes" link since the image won't be used for toggling.
3576 noteToggleLinkInit();
3577 }
3578
3579 function createOptionsSection() {
3580 // Create the sidebar options section for logged out users.
3581 if (isLoggedIn())
3582 return;
3583
3584 var post = bbb.post.info;
3585 var infoSection = document.getElementById("post-information");
3586 var options = document.createElement("section");
3587 options.id = "post-options";
3588 options.innerHTML = '<h1>Options</h1><ul><li><a href="#" id="image-resize-to-window-link">Resize to window</a></li><li>Download</li><li><a id="random-post" href="http://danbooru.donmai.us/posts/random">Random post</a></li><li><a href="http://danbooru.iqdb.org/db-search.php?url=http://danbooru.donmai.us' + post.preview_file_url + '">Find similar</a></li></ul>';
3589 infoSection.parentNode.insertBefore(options, infoSection.nextElementSibling);
3590 }
3591
3592 function fixPostDownloadLinks() {
3593 // Fix the "size" and "download" links in the sidebar.
3594 var post = bbb.post.info;
3595 var i, il; // Loop variables.
3596
3597 if (isLoggedIn() && !post.is_hidden)
3598 return;
3599
3600 // Fix the "size" link.
3601 var infoSection = document.getElementById("post-information");
3602
3603 if (infoSection) {
3604 var infoItems = infoSection.getElementsByTagName("li");
3605 var sizeRegex = /^(\s*Size:\s+)([\d\.]+\s+\S+)/i;
3606
3607 for (i = 0, il = infoItems.length; i < il; i++) {
3608 var infoItem = infoItems[i];
3609
3610 if (sizeRegex.test(infoItem.innerHTML)) {
3611 infoItem.innerHTML = infoItem.innerHTML.replace(sizeRegex, '$1<a href="' + post.file_url + '">$2</a>');
3612 break;
3613 }
3614 }
3615 }
3616
3617 // Fix the "download" link.
3618 var optionsSection = document.getElementById("post-options");
3619
3620 if (optionsSection) {
3621 var optionItems = optionsSection.getElementsByTagName("li");
3622 var downloadRegex = /^\s*Download\s*$/i;
3623 var downloadName = (getMeta("og:title") || "").replace(" - Danbooru", " - ");
3624
3625 for (i = 0, il = optionItems.length; i < il; i++) {
3626 var optionItem = optionItems[i];
3627
3628 if (downloadRegex.test(optionItem.innerHTML)) {
3629 optionItem.innerHTML = '<a download="' + downloadName + post.md5 + '.' + post.file_ext + '" href="' + post.file_url + '">Download</a>';
3630 break;
3631 }
3632 }
3633 }
3634 }
3635
3636 function modifyResizeLink() {
3637 // Replace the single resize link with three custom resize links.
3638 var resizeListLink = document.getElementById("image-resize-to-window-link");
3639
3640 if (!resizeListLink)
3641 return;
3642
3643 var resizeListItem = resizeListLink.parentNode;
3644 var resizeListParent = resizeListItem.parentNode;
3645 var optionsFrag = document.createDocumentFragment();
3646
3647 var resizeLinkAll = bbb.el.resizeLinkAll = document.createElement("a");
3648 resizeLinkAll.href = "#";
3649 resizeLinkAll.addEventListener("click", function(event) {
3650 if (event.button !== 0)
3651 return;
3652
3653 resizePost("all");
3654 event.preventDefault();
3655 }, false);
3656
3657 var resizeLinkWidth = bbb.el.resizeLinkWidth = document.createElement("a");
3658 resizeLinkWidth.href = "#";
3659 resizeLinkWidth.addEventListener("click", function(event) {
3660 if (event.button !== 0)
3661 return;
3662
3663 resizePost("width");
3664 event.preventDefault();
3665 }, false);
3666
3667 var resizeLinkHeight = bbb.el.resizeLinkHeight = document.createElement("a");
3668 resizeLinkHeight.href = "#";
3669 resizeLinkHeight.addEventListener("click", function(event) {
3670 if (event.button !== 0)
3671 return;
3672
3673 resizePost("height");
3674 event.preventDefault();
3675 }, false);
3676
3677 if (resize_link_style === "full") {
3678 var resizeListAll = document.createElement("li");
3679 optionsFrag.appendChild(resizeListAll);
3680
3681 resizeLinkAll.innerHTML = "Resize to window";
3682 resizeListAll.appendChild(resizeLinkAll);
3683
3684 var resizeListWidth = document.createElement("li");
3685 optionsFrag.appendChild(resizeListWidth);
3686
3687 resizeLinkWidth.innerHTML = "Resize to window width";
3688 resizeListWidth.appendChild(resizeLinkWidth);
3689
3690 var resizeListHeight = document.createElement("li");
3691 optionsFrag.appendChild(resizeListHeight);
3692
3693 resizeLinkHeight.innerHTML = "Resize to window height";
3694 resizeListHeight.appendChild(resizeLinkHeight);
3695
3696 resizeListParent.replaceChild(optionsFrag, resizeListItem);
3697 }
3698 else if (resize_link_style === "minimal") {
3699 var resizeList = document.createElement("li");
3700 optionsFrag.appendChild(resizeList);
3701
3702 var resizeLabelLink = document.createElement("a");
3703 resizeLabelLink.href = "#";
3704 resizeLabelLink.innerHTML = "Resize:";
3705 resizeLabelLink.style.marginRight = "2px";
3706 resizeLabelLink.addEventListener("click", function(event) {
3707 if (event.button !== 0)
3708 return;
3709
3710 if (bbb.post.resize.mode === "none")
3711 resizePost("all");
3712 else
3713 resizePost("none");
3714
3715 event.preventDefault();
3716 }, false);
3717 resizeList.appendChild(resizeLabelLink);
3718
3719 resizeLinkAll.innerHTML = "(W&H)";
3720 resizeLinkAll.className = "bbb-resize-link";
3721 resizeLinkAll.title = "Resize to Window Width & Height";
3722 resizeList.appendChild(resizeLinkAll);
3723
3724 resizeLinkWidth.innerHTML = "(W)";
3725 resizeLinkWidth.className = "bbb-resize-link";
3726 resizeLinkWidth.title = "Resize to Window Width";
3727 resizeList.appendChild(resizeLinkWidth);
3728
3729 resizeLinkHeight.innerHTML = "(H)";
3730 resizeLinkHeight.className = "bbb-resize-link";
3731 resizeLinkHeight.title = "Resize to Window Height";
3732 resizeList.appendChild(resizeLinkHeight);
3733
3734 resizeList.style.height = "0px";
3735 resizeList.style.visibility = "hidden";
3736 resizeList.style.fontWeight = "bold";
3737 resizeList.style.position = "relative";
3738
3739 resizeListParent.insertBefore(resizeList, resizeListItem);
3740
3741 var allWidth = resizeLinkAll.clientWidth;
3742 var widthWidth = resizeLinkWidth.clientWidth;
3743 var heightWidth = resizeLinkHeight.clientWidth;
3744
3745 resizeLinkAll.style.width = allWidth + "px";
3746 resizeLinkWidth.style.width = widthWidth + "px";
3747 resizeLinkHeight.style.width = heightWidth + "px";
3748
3749 resizeList.style.height = "auto";
3750 resizeList.style.visibility = "visible";
3751 resizeList.style.fontWeight = "normal";
3752
3753 resizeListParent.removeChild(resizeListItem);
3754 }
3755 }
3756
3757 function resizePost(mode) {
3758 // Custom resize post script.
3759 var postContent = getPostContent();
3760 var imgContainer = postContent.container;
3761 var contentDiv = document.getElementById("content");
3762 var ugoiraPanel = document.getElementById("ugoira-control-panel");
3763 var ugoiraSlider = document.getElementById("seek-slider");
3764 var target = postContent.el;
3765 var targetTag = (target ? target.tagName : undefined);
3766
3767 if (!target || !imgContainer || !contentDiv || targetTag === "A")
3768 return;
3769
3770 var currentMode = bbb.post.resize.mode;
3771 var currentRatio = bbb.post.resize.ratio;
3772 var resizeLinkAll = bbb.el.resizeLinkAll;
3773 var resizeLinkWidth = bbb.el.resizeLinkWidth;
3774 var resizeLinkHeight = bbb.el.resizeLinkHeight;
3775 var availableWidth = imgContainer.clientWidth || contentDiv.clientWidth - contentDiv.bbbGetPadding().width;
3776 var availableHeight = document.documentElement.clientHeight - 40;
3777 var targetCurrentWidth = target.clientWidth || parseFloat(target.style.width) || target.getAttribute("width");
3778 var targetCurrentHeight = target.clientHeight || parseFloat(target.style.height) || target.getAttribute("height");
3779 var useDataDim = targetTag === "EMBED" || targetTag === "VIDEO";
3780 var targetWidth = (useDataDim ? imgContainer.getAttribute("data-width") : target.getAttribute("width")); // Was NOT expecting target.width to return the current width (css style width) and not the width attribute's value here...
3781 var targetHeight = (useDataDim ? imgContainer.getAttribute("data-height") : target.getAttribute("height"));
3782 var tooWide = targetCurrentWidth > availableWidth;
3783 var tooTall = targetCurrentHeight > availableHeight;
3784 var widthRatio = availableWidth / targetWidth;
3785 var heightRatio = availableHeight / targetHeight;
3786 var imgMode = mode;
3787 var switchMode = false;
3788 var ratio = 1;
3789 var linkWeight = {all: "normal", width: "normal", height: "normal"};
3790
3791 if (mode === "swap") { // The image is being swapped between the original and sample image so everything needs to be reset. Ignore the current mode.
3792 switchMode = true;
3793 imgMode = "none";
3794 }
3795 else if (mode === currentMode || mode === "none" || (mode === "width" && widthRatio >= 1) || (mode === "height" && heightRatio >= 1) || (mode === "all" && widthRatio >= 1 && heightRatio >= 1)) { // Cases where resizing is being toggled off or isn't needed.
3796 if (currentMode !== "none") { // No need to do anything if the content is already at the original dimensions.
3797 switchMode = true;
3798 imgMode = "none";
3799 }
3800 }
3801 else if (mode === "height" && (tooTall || currentMode !== "none")) {
3802 switchMode = true;
3803 ratio = heightRatio;
3804 linkWeight.height = "bold";
3805 }
3806 else if (mode === "width" && (tooWide || currentMode !== "none")) {
3807 switchMode = true;
3808 ratio = widthRatio;
3809 linkWeight.width = "bold";
3810 }
3811 else if (mode === "all" && (tooWide || tooTall || currentMode !== "none")) {
3812 switchMode = true;
3813 ratio = (widthRatio < heightRatio ? widthRatio : heightRatio);
3814 linkWeight.all = "bold";
3815 }
3816
3817 if (switchMode) {
3818 if (currentRatio !== ratio || mode === "swap") {
3819 if (targetTag === "IMG" || targetTag === "CANVAS") {
3820 target.style.width = targetWidth * ratio + "px";
3821 target.style.height = targetHeight * ratio + "px";
3822
3823 if (ugoiraPanel && ugoiraSlider) {
3824 ugoiraPanel.style.width = targetWidth * ratio + "px";
3825 ugoiraSlider.style.width = targetWidth * ratio - 81 + "px";
3826 }
3827
3828 Danbooru.Note.Box.scale_all();
3829 }
3830 else if (targetTag === "EMBED") {
3831 var secondaryTarget = postContent.secEl;
3832
3833 secondaryTarget.height = target.height = targetHeight * ratio;
3834 secondaryTarget.width = target.width = targetWidth * ratio;
3835 }
3836 else if (targetTag === "VIDEO") {
3837 target.height = targetHeight * ratio;
3838 target.width = targetWidth * ratio;
3839 }
3840 }
3841
3842 bbb.post.resize.mode = imgMode;
3843 bbb.post.resize.ratio = ratio;
3844 resizeLinkAll.style.fontWeight = linkWeight.all;
3845 resizeLinkWidth.style.fontWeight = linkWeight.width;
3846 resizeLinkHeight.style.fontWeight = linkWeight.height;
3847 }
3848 }
3849
3850 function swapPost() {
3851 // Initiate the swap between the sample and original post content.
3852 var post = bbb.post.info;
3853 var target = getPostContent().el;
3854 var targetTag = (target ? target.tagName : undefined);
3855 var bbbLoader = bbb.el.bbbLoader;
3856 var resizeStatus = bbb.el.resizeStatus;
3857 var resizeLink = bbb.el.resizeLink;
3858 var swapLink = bbb.el.swapLink;
3859
3860 if (!post.has_large)
3861 return;
3862
3863 if (post.file_ext === "zip" && /(?:^|\s)ugoira(?:$|\s)/.test(post.tag_string)) {
3864 if (targetTag === "CANVAS")
3865 location.href = updateURLQuery(location.href, {original: "0"});
3866 else if (targetTag === "VIDEO")
3867 location.href = updateURLQuery(location.href, {original: "1"});
3868 }
3869 else if (targetTag === "IMG") {
3870 if (image_swap_mode === "load") { // Load image and then view mode.
3871 if (bbbLoader.src !== "about:blank") { // Messages after cancelling.
3872 if (target.src.indexOf("/sample/") < 0)
3873 swapImageUpdate("original");
3874 else
3875 swapImageUpdate("sample");
3876
3877 bbbLoader.src = "about:blank";
3878 }
3879 else { // Messages during loading.
3880 if (target.src.indexOf("/sample/") < 0) {
3881 resizeStatus.innerHTML = "Loading sample image...";
3882 resizeLink.innerHTML = "cancel";
3883 swapLink.innerHTML = "View sample (cancel)";
3884 bbbLoader.src = post.large_file_url;
3885 }
3886 else {
3887 resizeStatus.innerHTML = "Loading original image...";
3888 resizeLink.innerHTML = "cancel";
3889 swapLink.innerHTML = "View original (cancel)";
3890 bbbLoader.src = post.file_url;
3891 }
3892 }
3893 }
3894 else if (image_swap_mode === "view") { // View image while loading mode.
3895 if (target.src.indexOf("/sample/") < 0) { // Load the sample image.
3896 swapImageUpdate("sample");
3897 target.src = "about:blank";
3898 target.removeAttribute("src");
3899 delayMe(function() { target.src = post.large_file_url; });
3900 }
3901 else { // Load the original image.
3902 swapImageUpdate("original");
3903 target.src = "about:blank";
3904 target.removeAttribute("src");
3905 delayMe(function() { target.src = post.file_url; });
3906 }
3907
3908 if (!bbb.post.swapped)
3909 delayMe(function() { resizePost("swap"); });
3910 else
3911 bbb.post.swapped = true;
3912 }
3913 }
3914 }
3915
3916 function swapImageUpdate(mode) {
3917 // Update all the elements related to swapping images when the image URL is changed.
3918 var post = bbb.post.info;
3919 var img = document.getElementById("image");
3920 var bbbResizeNotice = bbb.el.resizeNotice;
3921 var resizeStatus = bbb.el.resizeStatus;
3922 var resizeLink = bbb.el.resizeLink;
3923 var swapLink = bbb.el.swapLink;
3924 var showResNot = bbb.user.show_resized_notice;
3925
3926 if (mode === "original") { // When the image is changed to the original image.
3927 resizeStatus.innerHTML = "Viewing original";
3928 resizeLink.innerHTML = "view sample";
3929 resizeLink.href = post.large_file_url;
3930 swapLink.innerHTML = "View sample";
3931 swapLink.href = post.large_file_url;
3932 img.setAttribute("height", post.image_height);
3933 img.setAttribute("width", post.image_width);
3934 bbbResizeNotice.style.display = (showResNot === "original" || showResNot === "all" ? "block" : "none");
3935 }
3936 else if (mode === "sample") { // When the image is changed to the sample image.
3937 resizeStatus.innerHTML = "Resized to " + Math.floor(post.sample_ratio * 100) + "% of original";
3938 resizeLink.innerHTML = "view original";
3939 resizeLink.href = post.file_url;
3940 swapLink.innerHTML = "View original";
3941 swapLink.href = post.file_url;
3942 img.setAttribute("height", post.sample_height);
3943 img.setAttribute("width", post.sample_width);
3944 bbbResizeNotice.style.display = (showResNot === "sample" || showResNot === "all" ? "block" : "none");
3945 }
3946 }
3947
3948 function checkRelations() {
3949 // Test whether the parent/child notice could have hidden posts.
3950 var post = bbb.post.info;
3951 var loggedIn = isLoggedIn();
3952 var fixParent = false;
3953 var fixChild = false;
3954 var relationCookie = getCookie()["show-relationship-previews"];
3955 var showPreview = (relationCookie === undefined || relationCookie === "1" ? true : false);
3956 var parentLink = document.getElementById("has-children-relationship-preview-link");
3957 var childLink = document.getElementById("has-parent-relationship-preview-link");
3958 var thumbCount, deletedCount; // If/else variable.
3959
3960 if (post.has_children) {
3961 var parentNotice = document.getElementsByClassName("notice-parent")[0];
3962
3963 if (parentNotice) {
3964 var parentText = parentNotice.textContent.match(/has (\d+|a) child/);
3965 var parentCount = (parentText ? Number(parentText[1]) || 1 : 0);
3966 thumbCount = getPosts(parentNotice).length;
3967 deletedCount = parentNotice.getElementsByClassName("post-status-deleted").length;
3968
3969 if ((!loggedIn && show_deleted && !deletedCount) || (parentCount && parentCount + 1 !== thumbCount))
3970 fixParent = true;
3971 }
3972 else if (show_deleted)
3973 fixParent = true;
3974 }
3975
3976 if (fixParent) {
3977 if (showPreview || !parentLink)
3978 searchJSON("parent", post.id);
3979 else
3980 parentLink.addEventListener("click", requestRelations, false);
3981 }
3982
3983 if (post.parent_id) {
3984 var childNotice = document.getElementsByClassName("notice-child")[0];
3985
3986 if (childNotice) {
3987 var childText = childNotice.textContent.match(/has (\d+|a) sibling/);
3988 var childCount = (childText ? Number(childText[1]) || 1 : 0) + 1;
3989 thumbCount = getPosts(childNotice).length;
3990 deletedCount = childNotice.getElementsByClassName("post-status-deleted").length;
3991
3992 if ((!loggedIn && show_deleted && !deletedCount) || (childCount && childCount + 1 !== thumbCount))
3993 fixChild = true;
3994 }
3995 }
3996
3997 if (fixChild) {
3998 if (showPreview || !childLink)
3999 searchJSON("child", post.parent_id);
4000 else
4001 childLink.addEventListener("click", requestRelations, false);
4002 }
4003 }
4004
4005 function requestRelations(event) {
4006 // Start the parent/child notice JSON request when the user chooses to display the thumbs in a notice.
4007 if (event.button !== 0)
4008 return;
4009
4010 var post = bbb.post.info;
4011 var target = event.target;
4012
4013 if (target.id === "has-children-relationship-preview-link")
4014 searchJSON("parent", post.id);
4015 else if (target.id === "has-parent-relationship-preview-link")
4016 searchJSON("child", post.parent_id);
4017
4018 target.removeEventListener("click", requestRelations, false);
4019 event.preventDefault();
4020 }
4021
4022 function removeTagHeaders() {
4023 // Remove the "copyright", "characters", and "artist" headers in the post sidebar.
4024 var tagList = document.getElementById("tag-list");
4025
4026 if (!tagList || !remove_tag_headers || gLoc !== "post")
4027 return;
4028
4029 var tagHolder = document.createDocumentFragment();
4030 var childIndex = 0;
4031 var mainList; // If/else variable.
4032
4033 while (tagList.children[childIndex]) {
4034 var header = tagList.children[childIndex];
4035 var list = tagList.children[childIndex + 1];
4036
4037 if (header.tagName === "H2" && list && list.tagName === "UL") {
4038 tagList.removeChild(header);
4039 tagList.removeChild(list);
4040
4041 while (list.firstElementChild)
4042 tagHolder.appendChild(list.firstElementChild);
4043 }
4044 else if (header.tagName === "H1" && list && list.tagName === "UL") {
4045 mainList = list;
4046 childIndex += 2;
4047 }
4048 else
4049 childIndex++;
4050 }
4051
4052 if (mainList)
4053 mainList.insertBefore(tagHolder, mainList.firstElementChild);
4054 else {
4055 var newHeader = document.createElement("h1");
4056 newHeader.innerHTML = "Tags";
4057 tagList.appendChild(newHeader);
4058
4059 var newList = document.createElement("ul");
4060 newList.appendChild(tagHolder);
4061 tagList.appendChild(newList);
4062 }
4063 }
4064
4065 function postTagTitles() {
4066 // Replace the post title with the full set of tags.
4067 if (post_tag_titles && gLoc === "post")
4068 document.title = getMeta("tags").replace(/\s/g, ", ").replace(/_/g, " ") + " - Danbooru";
4069 }
4070
4071 function minimizeStatusNotices() {
4072 // Show status notices only when their respective status link is clicked in the sidebar.
4073 if (!minimize_status_notices || gLoc !== "post")
4074 return;
4075
4076 var infoSection = document.getElementById("post-information");
4077 var infoListItems = (infoSection ? infoSection.getElementsByTagName("li") : null);
4078 var flaggedNotice = document.getElementsByClassName("notice-flagged")[0];
4079 var appealedNotice = document.getElementsByClassName("notice-appealed")[0];
4080 var pendingNotice = document.getElementsByClassName("notice-pending")[0];
4081 var deletedNotices = document.getElementsByClassName("notice-deleted");
4082 var i, il, statusListItem, newStatusContent, deletedNotice, bannedNotice; // Loop variables.
4083
4084 if (infoListItems) {
4085 // Locate the status portion of the information section.
4086 for (i = infoListItems.length - 1; i >= 0; i--) {
4087 var infoListItem = infoListItems[i];
4088
4089 if (infoListItem.textContent.indexOf("Status:") > -1) {
4090 statusListItem = infoListItem;
4091 newStatusContent = statusListItem.textContent;
4092 break;
4093 }
4094 }
4095
4096 // Hide and alter the notices and create the appropriate status links.
4097 if (statusListItem) {
4098 if (flaggedNotice) {
4099 flaggedNotice.style.display = "none";
4100 flaggedNotice.style.position = "absolute";
4101 flaggedNotice.style.zIndex = "2003";
4102 newStatusContent = newStatusContent.replace("Flagged", '<a href="#" id="bbb-flagged-link">Flagged</a>');
4103 }
4104
4105 if (pendingNotice) {
4106 pendingNotice.style.display = "none";
4107 pendingNotice.style.position = "absolute";
4108 pendingNotice.style.zIndex = "2003";
4109 newStatusContent = newStatusContent.replace("Pending", '<a href="#" id="bbb-pending-link">Pending</a>');
4110 }
4111
4112 for (i = 0, il = deletedNotices.length; i < il; i++) {
4113 deletedNotices[i].style.display = "none";
4114 deletedNotices[i].style.position = "absolute";
4115 deletedNotices[i].style.zIndex = "2003";
4116
4117 if (deletedNotices[i].getElementsByTagName("li")[0]) {
4118 deletedNotice = deletedNotices[i];
4119 newStatusContent = newStatusContent.replace("Deleted", '<a href="#" id="bbb-deleted-link">Deleted</a>');
4120 }
4121 else {
4122 bannedNotice = deletedNotices[i];
4123 newStatusContent = newStatusContent.replace("Banned", '<a href="#" id="bbb-banned-link">Banned</a>');
4124 }
4125 }
4126
4127 if (appealedNotice) {
4128 appealedNotice.style.display = "none";
4129 appealedNotice.style.position = "absolute";
4130 appealedNotice.style.zIndex = "2003";
4131 newStatusContent = newStatusContent + ' <a href="#" id="bbb-appealed-link">Appealed</a>';
4132 }
4133
4134 statusListItem.innerHTML = newStatusContent;
4135 }
4136
4137 // Prepare the links.
4138 var flaggedLink = document.getElementById("bbb-flagged-link");
4139 var appealedLink = document.getElementById("bbb-appealed-link");
4140 var pendingLink = document.getElementById("bbb-pending-link");
4141 var deletedLink = document.getElementById("bbb-deleted-link");
4142 var bannedLink = document.getElementById("bbb-banned-link");
4143
4144 if (flaggedLink)
4145 statusLinkEvents(flaggedLink, flaggedNotice);
4146 if (appealedLink)
4147 statusLinkEvents(appealedLink, appealedNotice);
4148 if (pendingLink)
4149 statusLinkEvents(pendingLink, pendingNotice);
4150 if (deletedLink)
4151 statusLinkEvents(deletedLink, deletedNotice);
4152 if (bannedLink)
4153 statusLinkEvents(bannedLink, bannedNotice);
4154 }
4155 }
4156
4157 function statusLinkEvents(link, notice) {
4158 // Attach events to the status links to enable a tooltip style notice.
4159 link.addEventListener("click", function(event) {
4160 if (event.button === 0)
4161 showStatusNotice(event, notice);
4162 }, false);
4163 link.addEventListener("mouseout", function() {
4164 bbb.timers.minNotice = window.setTimeout(function() {
4165 notice.style.display = "none";
4166 }, 200);
4167 }, false);
4168 notice.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.minNotice); }, false);
4169 notice.addEventListener("mouseleave", function() { notice.style.display = "none"; }, false);
4170 }
4171
4172 function showStatusNotice(event, noticeEl) {
4173 // Display a minimized status notice upon a click event.
4174 var x = event.pageX;
4175 var y = event.pageY;
4176 var notice = noticeEl;
4177 var topOffset = 0;
4178
4179 notice.style.maxWidth = document.documentElement.clientWidth * 0.66 + "px";
4180 notice.style.visibility = "hidden";
4181 notice.style.display = "block";
4182
4183 // Don't allow the notice to go above the top of the window.
4184 if (event.clientY - notice.offsetHeight - 2 < 5)
4185 topOffset = event.clientY - notice.offsetHeight - 7;
4186
4187 notice.style.left = x + 2 + "px";
4188 notice.style.top = y - notice.offsetHeight - 2 - topOffset + "px";
4189 notice.style.visibility = "visible";
4190
4191 event.preventDefault();
4192 }
4193
4194 function dragScrollInit() {
4195 // Start up drag scroll.
4196 if (!post_drag_scroll)
4197 return;
4198
4199 var target = getPostContent().el;
4200 var targetTag = (target ? target.tagName : undefined);
4201
4202 if (targetTag === "IMG" || targetTag === "VIDEO" || targetTag === "CANVAS") {
4203 bbb.drag_scroll.target = target;
4204
4205 if (!Danbooru.Note.TranslationMode.active)
4206 dragScrollEnable();
4207
4208 var startFunction = Danbooru.Note.TranslationMode.start;
4209 var stopFunction = Danbooru.Note.TranslationMode.stop;
4210
4211 Danbooru.Note.TranslationMode.start = function(event) {
4212 startFunction(event);
4213 dragScrollToggle();
4214 };
4215
4216 Danbooru.Note.TranslationMode.stop = function(event) {
4217 stopFunction(event);
4218 dragScrollToggle();
4219 };
4220
4221 // Disable click behavior when dragging the video around.
4222 if (targetTag === "VIDEO") {
4223 target.parentNode.addEventListener("click", function(event) {
4224 if (event.button === 0 && event.target.id === "image" && bbb.drag_scroll.moved)
4225 event.preventDefault();
4226 }, true);
4227 }
4228 }
4229 }
4230
4231 function dragScrollToggle() {
4232 // Enable drag scroll with translation mode is off and disable it when translation mode is on.
4233 if (!post_drag_scroll || !bbb.drag_scroll.target)
4234 return;
4235
4236 if (Danbooru.Note.TranslationMode.active)
4237 dragScrollDisable();
4238 else
4239 dragScrollEnable();
4240 }
4241
4242 function dragScrollEnable() {
4243 // Add the drag scroll event listeners.
4244 var target = bbb.drag_scroll.target;
4245
4246 target.addEventListener("mousedown", dragScrollOn, false);
4247 target.addEventListener("dragstart", disableEvent, false);
4248 target.addEventListener("selectstart", disableEvent, false);
4249 }
4250
4251 function dragScrollDisable() {
4252 // Remove the drag scroll event listeners.
4253 var target = bbb.drag_scroll.target;
4254
4255 target.removeEventListener("mousedown", dragScrollOn, false);
4256 target.removeEventListener("dragstart", disableEvent, false);
4257 target.removeEventListener("selectstart", disableEvent, false);
4258 }
4259
4260 function dragScrollOn(event) {
4261 // Start monitoring mouse movement.
4262 if (event.button === 0) {
4263 bbb.drag_scroll.lastX = event.clientX;
4264 bbb.drag_scroll.lastY = event.clientY;
4265 bbb.drag_scroll.moved = false;
4266
4267 document.addEventListener("mousemove", dragScrollMove, false);
4268 document.addEventListener("mouseup", dragScrollOff, false);
4269 }
4270 }
4271
4272 function dragScrollMove(event) {
4273 // Move the page based on mouse movement.
4274 var newX = event.clientX;
4275 var newY = event.clientY;
4276 var xDistance = bbb.drag_scroll.lastX - newX;
4277 var yDistance = bbb.drag_scroll.lastY - newY;
4278
4279 window.scrollBy(xDistance, yDistance);
4280
4281 bbb.drag_scroll.lastX = newX;
4282 bbb.drag_scroll.lastY = newY;
4283 bbb.drag_scroll.moved = xDistance !== 0 || yDistance !== 0 || bbb.drag_scroll.moved; // Doing this since I'm not sure what Chrome's mousemove event is doing. It apparently fires even when the moved distance is equal to zero.
4284 }
4285
4286 function dragScrollOff() {
4287 // Stop monitoring mouse movement.
4288 document.removeEventListener("mousemove", dragScrollMove, false);
4289 document.removeEventListener("mouseup", dragScrollOff, false);
4290 }
4291
4292 function disableEvent(event) {
4293 // removeEventListener friendly function for stopping an event.
4294 event.preventDefault();
4295 }
4296
4297 function autoscrollPost() {
4298 // Automatically scroll a post to the desired position.
4299 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
4300
4301 if (autoscroll_post === "none" || scrolled !== 0) // Don't scroll if the page is already srolled.
4302 return;
4303
4304 if (autoscroll_post === "post") {
4305 var target = getPostContent().el;
4306
4307 if (target)
4308 target.scrollIntoView();
4309 }
4310 else if (autoscroll_post === "header") {
4311 var page = document.getElementById("page");
4312
4313 if (!page)
4314 return;
4315
4316 var pageTop = page.offsetTop;
4317
4318 window.scroll(0, pageTop);
4319 }
4320 }
4321
4322 function ugoiraInit() {
4323 // Execute a static copy of Danbooru's embedded JavaScript for setting up the post.
4324 var post = bbb.post.info;
4325
4326 try {
4327 Danbooru.Ugoira = {};
4328
4329 Danbooru.Ugoira.create_player = function() {
4330 var meta_data = {
4331 mime_type: post.pixiv_ugoira_frame_data.content_type,
4332 frames: post.pixiv_ugoira_frame_data.data
4333 };
4334 var options = {
4335 canvas: document.getElementById("image"),
4336 source: post.file_url,
4337 metadata: meta_data,
4338 chunkSize: 300000,
4339 loop: true,
4340 autoStart: true,
4341 debug: false
4342 };
4343
4344 this.player = new ZipImagePlayer(options);
4345 };
4346
4347 Danbooru.Ugoira.player = null;
4348
4349 $(function() {
4350 Danbooru.Ugoira.create_player();
4351 $(Danbooru.Ugoira.player).on("loadProgress", function(event, progress) {
4352 $("#seek-slider").progressbar("value", Math.floor(progress * 100));
4353 });
4354
4355 var player_manually_paused = false;
4356
4357 $("#ugoira-play").click(function(event) {
4358 Danbooru.Ugoira.player.play();
4359 $(this).hide();
4360 $("#ugoira-pause").show();
4361 player_manually_paused = false;
4362 event.preventDefault();
4363 });
4364 $("#ugoira-pause").click(function(event) {
4365 Danbooru.Ugoira.player.pause();
4366 $(this).hide();
4367 $("#ugoira-play").show();
4368 player_manually_paused = true;
4369 event.preventDefault();
4370 });
4371
4372 $("#seek-slider").progressbar({
4373 value: 0
4374 });
4375
4376 $("#seek-slider").slider({
4377 min: 0,
4378 max: Danbooru.Ugoira.player._frameCount-1,
4379 start: function() {
4380 // Need to pause while slider is being dragged or playback speed will bug out
4381 Danbooru.Ugoira.player.pause();
4382 },
4383 slide: function(event, ui) {
4384 Danbooru.Ugoira.player._frame = ui.value;
4385 Danbooru.Ugoira.player._displayFrame();
4386 },
4387 stop: function() {
4388 // Resume playback when dragging stops, but only if player was not paused by the user earlier
4389 if (!(player_manually_paused)) {
4390 Danbooru.Ugoira.player.play();
4391 }
4392 }
4393 });
4394 $(Danbooru.Ugoira.player).on("frame", function(frame, frame_number) {
4395 $("#seek-slider").slider("option", "value", frame_number);
4396 });
4397 });
4398 }
4399 catch (error) {
4400 bbbNotice("Unexpected error creating the ugoira post. (Error: " + error.message + ")", -1);
4401 }
4402 }
4403
4404 /* Thumbnail functions */
4405 function formatThumbnails(target) {
4406 // Create thumbnail titles and borders.
4407 var posts = getPosts(target);
4408 var i, il; // Loop variables.
4409
4410 if (!posts[0])
4411 return;
4412
4413 var searches = bbb.custom_tag.searches;
4414
4415 // Create and cache border search objects.
4416 if (custom_tag_borders && !searches[0]) {
4417 for (i = 0, il = tag_borders.length; i < il; i++)
4418 searches.push(createSearch(tag_borders[i].tags));
4419 }
4420
4421 // Cycle through each post and apply titles and borders.
4422 for (i = 0, il = posts.length; i < il; i++) {
4423 var post = posts[i];
4424 var img = post.getElementsByTagName("img")[0];
4425
4426 if (!img)
4427 continue;
4428
4429 var link = img.parentNode;
4430 var tags = post.getAttribute("data-tags");
4431 var tagsStr = (tags ? tags : "");
4432 var user = post.getAttribute("data-uploader");
4433 var userStr = (user ? " user:" + user : "");
4434 var rating = post.getAttribute("data-rating");
4435 var ratingStr = (rating ? " rating:" + rating : "");
4436 var score = post.getAttribute("data-score");
4437 var scoreStr = (score ? " score:" + score : "");
4438 var views = post.getAttribute("data-views");
4439 var viewsStr = (views ? " views:" + views : "");
4440 var title = tagsStr + userStr + ratingStr + scoreStr + viewsStr;
4441 var id = post.getAttribute("data-id");
4442 var hasChildren = (post.getAttribute("data-has-children") === "true" ? true : false);
4443 var secondary = [];
4444 var secondaryLength = 0;
4445 var styleList = bbb.custom_tag.style_list;
4446 var borderStyle; // If/else variable.
4447
4448 // Skip thumbnails that have already been done.
4449 if (link.bbbHasClass("bbb-thumb-link"))
4450 continue;
4451
4452 // Create title.
4453 img.title = title;
4454
4455 // Give the thumbnail link an identifying class.
4456 link.bbbAddClass("bbb-thumb-link");
4457
4458 // Give the post container an ID class for resolving cases where the same post shows up on the page multiple times.
4459 post.bbbAddClass("post_" + id);
4460
4461 // Correct parent status borders on "no active children" posts for logged out users.
4462 if (hasChildren && show_deleted)
4463 post.bbbAddClass("post-status-has-children");
4464
4465 // Secondary custom tag borders.
4466 if (custom_tag_borders) {
4467 if (typeof(styleList[id]) === "undefined") {
4468 for (var j = 0, jl = tag_borders.length; j < jl; j++) {
4469 var tagBorderItem = tag_borders[j];
4470
4471 if (tagBorderItem.is_enabled && thumbSearchMatch(post, searches[j])) {
4472 secondary.push([tagBorderItem.border_color, tagBorderItem.border_style]);
4473
4474 if (secondary.length === 4)
4475 break;
4476 }
4477 }
4478
4479 secondaryLength = secondary.length;
4480
4481 if (secondaryLength) {
4482 link.bbbAddClass("bbb-custom-tag");
4483
4484 if (secondaryLength === 1 || (single_color_borders && secondaryLength > 1))
4485 borderStyle = "border-color: " + secondary[0][0] + " !important; border-style: " + secondary[0][1] + " !important;";
4486 else if (secondaryLength === 2)
4487 borderStyle = "border-color: " + secondary[0][0] + " " + secondary[1][0] + " " + secondary[1][0] + " " + secondary[0][0] + " !important; border-style: " + secondary[0][1] + " " + secondary[1][1] + " " + secondary[1][1] + " " + secondary[0][1] + " !important;";
4488 else if (secondaryLength === 3)
4489 borderStyle = "border-color: " + secondary[0][0] + " " + secondary[1][0] + " " + secondary[2][0] + " " + secondary[0][0] + " !important; border-style: " + secondary[0][1] + " " + secondary[1][1] + " " + secondary[2][1] + " " + secondary[0][1] + " !important;";
4490 else if (secondaryLength === 4)
4491 borderStyle = "border-color: " + secondary[0][0] + " " + secondary[2][0] + " " + secondary[3][0] + " " + secondary[1][0] + " !important; border-style: " + secondary[0][1] + " " + secondary[2][1] + " " + secondary[3][1] + " " + secondary[1][1] + " !important;";
4492
4493 link.setAttribute("style", borderStyle);
4494 styleList[id] = borderStyle;
4495 }
4496 else
4497 styleList[id] = false;
4498 }
4499 else if (styleList[id] !== false && !post.bbbHasClass("bbb-custom-tag")) { // Post is already tested, but needs to be set up again.
4500 link.bbbAddClass("bbb-custom-tag");
4501 link.setAttribute("style", styleList[id]);
4502 }
4503 }
4504 }
4505 }
4506
4507 function prepThumbnails(target) {
4508 // Take new thumbnails and apply the necessary functions for preparing them.
4509 // Thumbnail classes and titles.
4510 formatThumbnails(target);
4511
4512 // Thumbnail info.
4513 thumbInfo(target);
4514
4515 // Clean links.
4516 cleanLinks(target);
4517
4518 // Blacklist.
4519 blacklistUpdate(target);
4520
4521 // Direct downloads.
4522 postDDL(target);
4523
4524 // Quick search.
4525 quickSearchTest(target);
4526
4527 // Fix the mode menu.
4528 danbModeMenu(target);
4529 }
4530
4531 function checkHiddenThumbs(post) {
4532 // Alter a hidden thumbnails with cache info or queue it for the cache.
4533 if (!post.md5) {
4534 if (!bbb.cache.stored.history)
4535 loadThumbCache();
4536
4537 var cacheName = bbb.cache.stored.names[post.id];
4538
4539 if (cacheName) { // Load the thumbnail info from the cache.
4540 if (cacheName === "download-preview.png")
4541 post.preview_file_url = "/images/download-preview.png";
4542 else {
4543 var cacheValues = cacheName.split(".");
4544 var cacheMd5 = cacheValues[0];
4545 var cacheExt = cacheValues[1];
4546
4547 post.md5 = cacheMd5;
4548 post.file_ext = cacheExt;
4549 post.preview_file_url = (!post.image_height || cacheExt === "swf" ? "/images/download-preview.png" : "/data/preview/" + cacheMd5 + ".jpg");
4550 post.large_file_url = (post.has_large ? "/data/sample/sample-" + cacheMd5 + ".jpg" : "/data/" + cacheName);
4551 post.file_url = "/data/" + cacheName;
4552 }
4553 }
4554 else // Mark hidden img for fixing.
4555 post.thumb_class += " bbb-hidden-thumb";
4556 }
4557 }
4558
4559 function fixHiddenThumbs() {
4560 // Fix hidden thumbnails by fetching the info from a page.
4561 if (bbb.flags.hidden_xml)
4562 return;
4563
4564 var hiddenImgs = document.getElementsByClassName("bbb-hidden-thumb");
4565
4566 if (hiddenImgs[0]) {
4567 if (!bbb.cache.save_enabled) {
4568 window.addEventListener("beforeunload", updateThumbCache);
4569 bbb.cache.save_enabled = true;
4570 }
4571
4572 searchPages("hidden", hiddenImgs[0].getAttribute("data-id"));
4573 }
4574 }
4575
4576 function createThumbHTML(post, query) {
4577 // Create a thumbnail HTML string.
4578 return '<article class="post-preview' + post.thumb_class + '" id="post_' + post.id + '" data-id="' + post.id + '" data-has-sound="' + post.has_sound + '" data-tags="' + post.tag_string + '" data-pools="' + post.pool_string + '" data-uploader="' + post.uploader_name + '" data-rating="' + post.rating + '" data-width="' + post.image_width + '" data-height="' + post.image_height + '" data-flags="' + post.flags + '" data-parent-id="' + post.parent_id + '" data-has-children="' + post.has_children + '" data-score="' + post.score + '" data-fav-count="' + post.fav_count + '" data-approver-id="' + post.approver_id + '" data-pixiv-id="' + post.pixiv_id + '" data-md5="' + post.md5 + '" data-file-ext="' + post.file_ext + '" data-file-url="' + post.file_url + '" data-large-file-url="' + post.large_file_url + '" data-preview-file-url="' + post.preview_file_url + '"><a href="/posts/' + post.id + query + '"><img src="' + post.preview_file_url + '" alt="' + post.tag_string + '"></a></article>';
4579 }
4580
4581 function createThumb(post, query) {
4582 // Create a thumbnail element. (lazy method <_<)
4583 var childSpan = document.createElement("span");
4584 childSpan.innerHTML = createThumbHTML(post, query);
4585
4586 return childSpan.firstElementChild;
4587 }
4588
4589 function createThumbListing(posts, orderedIds) {
4590 // Create a listing of thumbnails.
4591 var thumbs = document.createDocumentFragment();
4592 var postHolder = {};
4593 var query = getThumbQuery();
4594 var i, il, thumb; // Loop variables;
4595
4596 // Generate thumbnails.
4597 for (i = 0, il = posts.length; i < il; i++) {
4598 var post = formatInfo(posts[i]);
4599
4600 // Don't display loli/shota/toddlercon/deleted/banned if the user has opted so and skip to the next image.
4601 if ((!show_loli && /(?:^|\s)loli(?:$|\s)/.test(post.tag_string)) || (!show_shota && /(?:^|\s)shota(?:$|\s)/.test(post.tag_string)) || (!show_toddlercon && /(?:^|\s)toddlercon(?:$|\s)/.test(post.tag_string)) || (!show_deleted && post.is_deleted) || (!show_banned && post.is_banned) || safebPostTest(post))
4602 continue;
4603
4604 // Check if the post is hidden.
4605 checkHiddenThumbs(post);
4606
4607 // eek, not so huge line.
4608 thumb = createThumb(post, query);
4609
4610 // Generate output.
4611 if (!orderedIds)
4612 thumbs.appendChild(thumb);
4613 else
4614 postHolder[post.id] = thumb;
4615 }
4616
4617 // Place thumbnails in the correct order for pools.
4618 if (orderedIds) {
4619 for (i = 0, il = orderedIds.length; i < il; i++) {
4620 thumb = postHolder[orderedIds[i]];
4621
4622 if (thumb)
4623 thumbs.appendChild(thumb);
4624 }
4625 }
4626
4627 return thumbs;
4628 }
4629
4630 function updateThumbListing(thumbs) {
4631 // Take a collection of thumbnails and use them to update the original thumbnail listing as appropriate.
4632 var thumbContainer = getThumbContainer(gLoc);
4633 var before = getThumbSibling(gLoc);
4634 var newContainer; // If/else variable.
4635
4636 if (!thumbContainer) {
4637 bbbNotice("Thumbnail section could not be located.", -1);
4638 return;
4639 }
4640
4641 if ((history.state && history.state.bbb_posts_cache) || !isRandomSearch()) {
4642 // New thumbnail container replacement preparation.
4643 var childIndex = 0;
4644
4645 newContainer = thumbContainer.cloneNode(false);
4646
4647 while (thumbContainer.children[childIndex]) {
4648 var child = thumbContainer.children[childIndex];
4649
4650 if (child.tagName !== "ARTICLE")
4651 newContainer.appendChild(child);
4652 else
4653 childIndex++;
4654 }
4655
4656 if (!before)
4657 newContainer.appendChild(thumbs);
4658 else
4659 newContainer.insertBefore(thumbs, before);
4660
4661 // Prepare thumbnails.
4662 prepThumbnails(newContainer);
4663
4664 // Replace results with new results.
4665 thumbContainer.parentNode.replaceChild(newContainer, thumbContainer);
4666 }
4667 else {
4668 // Fill out a random search by appending thumbnails.
4669 var origThumbs = getPosts(thumbs);
4670 var i, il, curThumb; // Loop variables.
4671
4672 newContainer = document.createDocumentFragment();
4673
4674 // Remove existing posts.
4675 for (i = 0, il = origThumbs.length; i < il; i++) {
4676 curThumb = origThumbs[i];
4677
4678 if (getId(curThumb.id))
4679 thumbs.removeChild(curThumb);
4680 }
4681
4682 // Favor hidden posts since they're the most likely reason for the API request.
4683 var noDupThumbs = getPosts(thumbs);
4684 var hiddenSearch = createSearch("~loli ~shota ~toddlercon ~status:deleted ~status:banned");
4685 var limit = getLimit() || (allowUserLimit() ? thumbnail_count : thumbnail_count_default);
4686 var numMissing = limit - getPosts().length;
4687
4688 for (i = 0, il = noDupThumbs.length - 1; i < il; i++) {
4689 curThumb = noDupThumbs[i];
4690
4691 if (numMissing === 0)
4692 break;
4693 else if (thumbSearchMatch(curThumb, hiddenSearch)) {
4694 newContainer.appendChild(curThumb);
4695 numMissing--;
4696 }
4697 }
4698
4699 // Try to fix any shortage of thumbnails.
4700 var leftoverThumbs = getPosts(thumbs);
4701
4702 for (i = 0, il = leftoverThumbs.length - 1; i < il; i++) {
4703 if (numMissing === 0)
4704 break;
4705 else {
4706 newContainer.appendChild(leftoverThumbs[i]);
4707 numMissing--;
4708 }
4709 }
4710
4711 // Prepare thumbnails.
4712 prepThumbnails(newContainer);
4713
4714 // Append listing with new thumbnails.
4715 if (!before)
4716 thumbContainer.appendChild(newContainer);
4717 else
4718 thumbContainer.insertBefore(newContainer, before);
4719 }
4720 }
4721
4722 function loadThumbCache() {
4723 // Initialize or load up the thumbnail cache.
4724 var thumbCache = localStorage.getItem("bbb_thumb_cache");
4725
4726 if (thumbCache !== null)
4727 bbb.cache.stored = JSON.parse(thumbCache);
4728 else {
4729 bbb.cache.stored = {history: [], names: {}};
4730 localStorage.bbbSetItem("bbb_thumb_cache", JSON.stringify(bbb.cache.stored));
4731 }
4732 }
4733
4734 function updateThumbCache() {
4735 // Add the current new thumbnail info to the saved thumbnail information.
4736 if (!bbb.cache.current.history[0] || !thumb_cache_limit)
4737 return;
4738
4739 loadThumbCache();
4740
4741 var bcc = bbb.cache.current;
4742 var bcs = bbb.cache.stored;
4743 var i, il; // Loop variables.
4744
4745 // Make sure we don't have duplicates in the new info.
4746 for (i = 0, il = bcc.history.length; i < il; i++) {
4747 if (bcs.names[bcc.history[i]]) {
4748 delete bcc.names[bcc.history[i]];
4749 bcc.history.splice(i, 1);
4750 il--;
4751 i--;
4752 }
4753 }
4754
4755 // Add the new thumbnail info in.
4756 for (i in bcc.names) {
4757 if (bcc.names.hasOwnProperty(i)) {
4758 bcs.names[i] = bcc.names[i];
4759 }
4760 }
4761
4762 bcs.history = bcs.history.concat(bcc.history);
4763
4764 // Prune the cache if it's larger than the user limit.
4765 if (bcs.history.length > thumb_cache_limit) {
4766 var removedIds = bcs.history.splice(0, bcs.history.length - thumb_cache_limit);
4767
4768 for (i = 0, il = removedIds.length; i < il; i++)
4769 delete bcs.names[removedIds[i]];
4770 }
4771
4772 localStorage.bbbSetItem("bbb_thumb_cache", JSON.stringify(bcs));
4773 bbb.cache.current = {history: [], names: {}};
4774 }
4775
4776 function adjustThumbCache() {
4777 // Prune the cache if it's larger than the user limit.
4778 loadThumbCache();
4779
4780 thumb_cache_limit = bbb.user.thumb_cache_limit;
4781
4782 var bcs = bbb.cache.stored;
4783
4784 if (bcs.history.length > thumb_cache_limit) {
4785 var removedIds = bcs.history.splice(0, bcs.history.length - thumb_cache_limit);
4786
4787 for (var i = 0, il = removedIds.length; i < il; i++)
4788 delete bcs.names[removedIds[i]];
4789 }
4790
4791 localStorage.bbbSetItem("bbb_thumb_cache", JSON.stringify(bcs));
4792 }
4793
4794 function getIdCache() {
4795 // Retrieve the cached list of post IDs used for the pool/favorite group thumbnails.
4796 var collId = /\/(?:pools|favorite_groups)\/(\d+)/.exec(location.href)[1];
4797 var idCache = sessionStorage.getItem("bbb_" + gLoc + "_cache_" + collId);
4798 var curTime = new Date().getTime();
4799 var cacheTime, timeDiff; // If/else variables.
4800
4801 if (idCache) {
4802 idCache = idCache.split(" ");
4803 cacheTime = idCache.shift();
4804 timeDiff = (curTime - cacheTime) / 1000; // Cache age in seconds.
4805 }
4806
4807 if (!idCache || (timeDiff && timeDiff > 600))
4808 return undefined;
4809 else
4810 return idCache.join(" ");
4811 }
4812
4813 function postDDL(target) {
4814 // Add direct downloads to thumbnails.
4815 if (!direct_downloads || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorites" && gLoc !== "favorite_group"))
4816 return;
4817
4818 var posts = getPosts(target);
4819
4820 for (var i = 0, il = posts.length; i < il; i++) {
4821 var post = posts[i];
4822 var postOrigUrl = post.getAttribute("data-file-url") || "";
4823 var postSampUrl = post.getAttribute("data-large-file-url") || "";
4824 var postUrl = (postSampUrl.indexOf(".webm") > -1 ? postSampUrl : postOrigUrl);
4825 var postId = post.getAttribute("data-id");
4826 var ddlLink = post.getElementsByClassName("bbb-ddl")[0];
4827
4828 // If the direct download doesn't already exist, create it.
4829 if (!ddlLink) {
4830 ddlLink = document.createElement("a");
4831 ddlLink.innerHTML = "Direct Download";
4832 ddlLink.className = "bbb-ddl";
4833 post.appendChild(ddlLink);
4834 }
4835
4836 ddlLink.href = postUrl || "/data/DDL unavailable for post " + postId + ".jpg";
4837
4838 // Disable filtered posts.
4839 if (post.bbbHasClass("blacklisted-active", "bbb-quick-search-filtered"))
4840 unsetDDL(ddlLink);
4841 }
4842 }
4843
4844 function enablePostDDL(post) {
4845 // Enable a post's DDL.
4846 var ddlLink = post.getElementsByClassName("bbb-ddl")[0];
4847
4848 if (!direct_downloads || !ddlLink || post.bbbHasClass("blacklisted-active", "bbb-quick-search-filtered"))
4849 return;
4850
4851 ddlLink.href = ddlLink.href.replace("donmai.us/#data", "donmai.us/data");
4852 }
4853
4854 function disablePostDDL(post) {
4855 // Disable a post's DDL.
4856 var ddlLink = post.getElementsByClassName("bbb-ddl")[0];
4857
4858 if (!direct_downloads || !ddlLink)
4859 return;
4860
4861 unsetDDL(ddlLink);
4862 }
4863
4864 function unsetDDL(ddlLink) {
4865 // Disable a DDL URL with an anchor.
4866 ddlLink.href = ddlLink.href.replace("donmai.us/data", "donmai.us/#data");
4867 }
4868
4869 function cleanLinks(target) {
4870 // Remove the query portion of thumbnail links.
4871 if (!clean_links)
4872 return;
4873
4874 var targetContainer; // If/else variable.
4875
4876 if (target)
4877 targetContainer = target;
4878 else if (gLoc === "post")
4879 targetContainer = document.getElementById("content");
4880 else if (gLoc === "pool" || gLoc === "favorite_group") {
4881 targetContainer = document.getElementById("a-show");
4882 targetContainer = (targetContainer ? targetContainer.getElementsByTagName("section")[0] : undefined);
4883 }
4884 else if (gLoc === "search" || gLoc === "favorites")
4885 targetContainer = document.getElementById("posts");
4886 else if (gLoc === "intro")
4887 targetContainer = document.getElementById("a-intro");
4888
4889 if (targetContainer) {
4890 var links = targetContainer.getElementsByTagName("a");
4891
4892 for (var i = 0, il = links.length; i < il; i++) {
4893 var link = links[i];
4894 var linkParent = link.parentNode;
4895
4896 if (linkParent.tagName === "ARTICLE" || linkParent.id.indexOf("nav-link-for-pool-") === 0)
4897 link.href = link.href.split("?", 1)[0];
4898 }
4899 }
4900 }
4901
4902 function danbModeMenu(target) {
4903 // Add mode menu functionality to newly created thumbnails.
4904 var modeSection = document.getElementById("mode-box");
4905
4906 if (!modeSection)
4907 return;
4908
4909 var links = (target || document).getElementsByClassName("bbb-thumb-link");
4910 var menuHandler = function(event) {
4911 if (event.button === 0)
4912 Danbooru.PostModeMenu.click(event);
4913 };
4914
4915 for (var i = 0, il = links.length; i < il; i++)
4916 links[i].addEventListener("click", menuHandler, false);
4917 }
4918
4919 function potentialHiddenPosts(mode, target) {
4920 // Check a normal thumbnail listing for possible hidden posts.
4921 var numPosts = getPosts(target).length;
4922 var noResults = noResultsPage(target);
4923 var limit = getLimit();
4924
4925 if (mode === "search" || mode === "notes" || mode === "favorites") {
4926 var numExpected = (limit !== undefined ? limit : thumbnail_count_default);
4927 var numDesired = (allowUserLimit() ? thumbnail_count : numExpected);
4928
4929 if (!noResults && (numPosts !== numDesired || numPosts < numExpected))
4930 return true;
4931 }
4932 else if (mode === "popular" || mode === "pool" || mode === "favorite_group" || mode === "popular_view") {
4933 if (!noResults && numPosts !== limit)
4934 return true;
4935 }
4936 else if (mode === "comments") {
4937 if (numPosts !== limit)
4938 return true;
4939 }
4940
4941 return false;
4942 }
4943
4944 /* Endless Page functions */
4945 function endlessToggle(event) {
4946 // Toggle endless pages on and off.
4947 if (endless_default === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "favorite_group"))
4948 return;
4949
4950 // Change the default for the duration of the session if necessary.
4951 if (endless_session_toggle) {
4952 var onValue = (bbb.user.endless_default !== "off" ? bbb.user.endless_default : "on");
4953 var newDefault = (bbb.endless.enabled ? "off" : onValue);
4954
4955 endless_default = newDefault;
4956 sessionStorage.bbbSetItem("bbb_endless_default", newDefault);
4957 }
4958
4959 if (bbb.endless.enabled) {
4960 endlessDisable();
4961
4962 if (event && event.type !== "click")
4963 bbbNotice("Endless pages disabled.", 2);
4964 }
4965 else {
4966 endlessEnable();
4967
4968 if (event && event.type !== "click")
4969 bbbNotice("Endless pages enabled.", 2);
4970 }
4971 }
4972
4973 function endlessEnable() {
4974 // Turn on endless pages.
4975 if (endless_default === "disabled" || noXML())
4976 return;
4977
4978 bbb.endless.enabled = true;
4979 bbb.el.endlessEnableDiv.style.display = "none";
4980 bbb.el.endlessLoadDiv.style.display = "inline-block";
4981 bbb.el.endlessLink.style.fontWeight = "bold";
4982
4983 // Check on the next page status.
4984 endlessCheck();
4985
4986 // Add the listeners for detecting the amount of scroll left.
4987 window.addEventListener("scroll", endlessCheck, false);
4988 window.addEventListener("resize", endlessCheck, false);
4989 document.addEventListener("keyup", endlessCheck, false);
4990 document.addEventListener("click", endlessCheck, false);
4991 }
4992
4993 function endlessDisable() {
4994 // Turn off endless pages.
4995 bbb.endless.enabled = false;
4996 bbb.endless.append_page = false;
4997 bbb.el.endlessEnableDiv.style.display = "inline-block";
4998 bbb.el.endlessLoadDiv.style.display = "none";
4999 bbb.el.endlessLink.style.fontWeight = "normal";
5000
5001 // Remove the listeners for detecting the amount of scroll left.
5002 window.removeEventListener("scroll", endlessCheck, false);
5003 window.removeEventListener("resize", endlessCheck, false);
5004 document.removeEventListener("keyup", endlessCheck, false);
5005 document.removeEventListener("click", endlessCheck, false);
5006 }
5007
5008 function endlessInit() {
5009 // Set up and start endless pages.
5010 removeInheritedStorage("bbb_endless_default");
5011
5012 var paginator = getPaginator();
5013
5014 if (endless_default === "disabled" || !paginator || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "favorite_group"))
5015 return;
5016
5017 // Add the endless link to the menu.
5018 var menu = document.getElementById("top");
5019 menu = (menu ? menu.getElementsByTagName("menu")[1] : undefined);
5020
5021 if (menu) {
5022 var menuItems = menu.getElementsByTagName("li");
5023 var numMenuItems = menu.getElementsByTagName("li").length;
5024 var listingItemSibling = menuItems[1];
5025
5026 for (var i = 0; i < numMenuItems; i++) {
5027 var menuLink = menuItems[i];
5028 var nextLink = menuItems[i + 1];
5029
5030 if (menuLink.textContent.indexOf("Listing") > -1) {
5031 if (nextLink)
5032 listingItemSibling = nextLink;
5033 else
5034 listingItemSibling = undefined;
5035
5036 break;
5037 }
5038 }
5039
5040 var link = bbb.el.endlessLink = document.createElement("a");
5041 link.href = "#";
5042 link.innerHTML = "Endless";
5043 link.addEventListener("click", function(event) {
5044 if (event.button !== 0)
5045 return;
5046
5047 endlessToggle();
5048 event.preventDefault();
5049 }, false);
5050
5051 var item = document.createElement("li");
5052 item.style.textAlign = "center";
5053 item.style.display = "inline-block";
5054 item.appendChild(link);
5055
5056 if (listingItemSibling)
5057 menu.insertBefore(item, listingItemSibling);
5058 else
5059 menu.appendChild(item);
5060
5061 link.style.fontWeight = "bold";
5062 item.style.width = item.clientWidth + "px";
5063 link.style.fontWeight = "normal";
5064 }
5065
5066 var paginatorParent = paginator.parentNode;
5067
5068 // Set up the load more button.
5069 var buttonDiv = document.createElement("div");
5070 buttonDiv.id = "bbb-endless-button-div";
5071
5072 var loadButtonDiv = bbb.el.endlessLoadDiv = document.createElement("div");
5073 loadButtonDiv.id = "bbb-endless-load-div";
5074 buttonDiv.appendChild(loadButtonDiv);
5075
5076 var loadButton = bbb.el.endlessLoadButton = document.createElement("a");
5077 loadButton.innerHTML = "Load More";
5078 loadButton.href = "#";
5079 loadButton.id = "bbb-endless-load-button";
5080 loadButton.style.display = "none";
5081 loadButton.addEventListener("click", function(event) {
5082 if (event.button !== 0)
5083 return;
5084
5085 loadButton.style.display = "none";
5086 loadButton.blur();
5087 bbb.endless.paused = false;
5088 bbb.endless.append_page = true;
5089 endlessCheck();
5090 event.preventDefault();
5091 }, false);
5092 loadButtonDiv.appendChild(loadButton);
5093
5094 // Set up the enable button.
5095 var enableButtonDiv = bbb.el.endlessEnableDiv = document.createElement("div");
5096 enableButtonDiv.id = "bbb-endless-enable-div";
5097 buttonDiv.appendChild(enableButtonDiv);
5098
5099 var enableButton = document.createElement("a");
5100 enableButton.innerHTML = "Endless";
5101 enableButton.href = "#";
5102 enableButton.id = "bbb-endless-enable-button";
5103 enableButton.addEventListener("click", function(event) {
5104 if (event.button !== 0)
5105 return;
5106
5107 enableButton.blur();
5108 endlessToggle();
5109 event.preventDefault();
5110 }, false);
5111 enableButtonDiv.appendChild(enableButton);
5112
5113 paginatorParent.insertBefore(buttonDiv, paginator);
5114
5115 // Create the hotkey.
5116 createHotkey("69", endlessToggle); // E
5117
5118 // Check the session default or original default value to see if endless pages should be enabled.
5119 var sessionDefault = sessionStorage.getItem("bbb_endless_default");
5120
5121 if (endless_session_toggle && sessionDefault)
5122 endless_default = sessionDefault;
5123
5124 if (endless_default !== "off")
5125 endlessEnable();
5126 else
5127 endlessDisable();
5128 }
5129
5130 function endlessObjectInit() {
5131 // Initialize the values for the first XML request. Runs separately from endlessInit since it requires the initial page being finalized.
5132 var posts = getPosts();
5133 var numPosts = posts.length;
5134
5135 // Prep the first paginator.
5136 bbb.endless.last_paginator = getPaginator();
5137
5138 // If we're already on the last page, don't continue.
5139 if (endlessLastPage())
5140 return;
5141
5142 // Note the posts that already exist.
5143 if (endless_remove_dup) {
5144 for (var i = 0; i < numPosts; i++) {
5145 var post = posts[i];
5146
5147 bbb.endless.posts[post.id] = post;
5148 }
5149 }
5150
5151 // Create a special "page" for filling out the first page.
5152 if (endless_fill) {
5153 var limit = getLimit() || thumbnail_count_default;
5154
5155 if (numPosts < limit) {
5156 var newPageObject = {
5157 page: document.createDocumentFragment(),
5158 page_num: [(getVar("page") || "1")],
5159 paginator: bbb.endless.last_paginator,
5160 ready: false
5161 };
5162
5163 bbb.endless.fill_first_page = true;
5164 bbb.endless.pages.push(newPageObject);
5165 }
5166 }
5167 }
5168
5169 function endlessCheck() {
5170 // Check whether the current document is ready for a page to be appended.
5171 if (!bbb.endless.enabled)
5172 return;
5173
5174 // Check whether endless pages needs to be paused.
5175 endlessPauseCheck();
5176
5177 // Stop if the check is delayed.
5178 if (bbb.timers.endlessDelay)
5179 return;
5180
5181 // Check whether a user is looking at the "posts tab" and not the "wiki tab" in the main search listing.
5182 var postsDiv = (gLoc === "search" ? document.getElementById("posts") : undefined);
5183 var postsVisible = (!postsDiv || postsDiv.style.display !== "none");
5184
5185 if (bbb.flags.thumbs_xml || bbb.flags.paginator_xml || !postsVisible) // Delay the check until the page is completely ready.
5186 endlessDelay(100);
5187 else {
5188 if (!bbb.endless.last_paginator)
5189 endlessObjectInit();
5190
5191 if (bbb.endless.append_page)
5192 endlessQueueCheck();
5193 else { // Check the amount of space left to scroll and attempt to add a page if we're far enough down.
5194 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
5195 var viewHeight = document.documentElement.clientHeight;
5196 var docHeight = document.documentElement.offsetHeight;
5197
5198 if (docHeight <= viewHeight + scrolled + endless_scroll_limit)
5199 bbb.endless.append_page = true;
5200
5201 endlessQueueCheck();
5202 }
5203 }
5204 }
5205
5206 function endlessQueueCheck() {
5207 // Check the page queue and append or request a page.
5208 if (!bbb.endless.enabled || bbb.endless.paused)
5209 return;
5210
5211 if (bbb.endless.append_page || bbb.endless.fill_first_page) {
5212 if (endlessPageReady())
5213 endlessAppendPage();
5214 else
5215 endlessRequestPage();
5216 }
5217 else if (endless_preload && !endlessPageReady())
5218 endlessRequestPage();
5219 }
5220
5221 function endlessRequestPage() {
5222 // Start an XML request for a new page.
5223 if (bbb.flags.endless_xml || endlessLastPage()) // Retrieve pages one at a time for as long as they exist.
5224 return;
5225
5226 searchPages("endless");
5227 }
5228
5229 function endlessQueuePage(newPage) {
5230 // Take some thumbnails from a page and work them into the queue.
5231 var limit = getLimit() || thumbnail_count_default;
5232 var pageNum = getVar("page", endlessNexURL());
5233 var paginator = bbb.endless.last_paginator = bbb.endless.new_paginator;
5234 var badPaginator = (paginator.textContent.indexOf("Go back") > -1); // Sometimes the paginator sends us to a page with no results.
5235 var lastPage = endlessLastPage() || badPaginator;
5236 var origPosts = getPosts(newPage);
5237 var i, il; // Loop variables.
5238
5239 bbb.endless.new_paginator = undefined;
5240
5241 // Remove duplicates.
5242 if (endless_remove_dup) {
5243 for (i = 0; i < origPosts.length; i++) {
5244 var post = origPosts[i];
5245 var postId = post.id;
5246
5247 if (bbb.endless.posts[postId])
5248 newPage.removeChild(post);
5249 else
5250 bbb.endless.posts[postId] = post;
5251 }
5252 }
5253
5254 // Fill up existing page objects with thumbnails.
5255 var fillPosts = getPosts(newPage);
5256 var leftoverPosts = fillPosts;
5257 var lastPageObject = bbb.endless.pages[bbb.endless.pages.length - 1];
5258
5259 if (endless_fill && lastPageObject && !lastPageObject.ready) {
5260 if (badPaginator) // Paginator isn't accurate. Ignore this page's number and paginator.
5261 lastPageObject.ready = true;
5262 else {
5263 var lastQueuePage = lastPageObject.page;
5264 var lastQueuePosts = getPosts(lastQueuePage);
5265 var fillLimit = (bbb.endless.fill_first_page ? limit - getPosts().length : limit - lastQueuePosts.length);
5266
5267 for (i = 0, il = fillPosts.length; i < il; i++) {
5268 lastQueuePage.appendChild(fillPosts[i]);
5269 fillLimit--;
5270
5271 if (fillLimit === 0) {
5272 lastPageObject.ready = true;
5273 break;
5274 }
5275 }
5276
5277 // If there are no more posts and pages, mark the last page as ready.
5278 leftoverPosts = getPosts(newPage);
5279
5280 if (!lastPageObject.ready && !leftoverPosts[0] && lastPage)
5281 lastPageObject.ready = true;
5282
5283 // Make sure the displayed paginator is always the one from the last retrieved page to have all of it's thumbnails used so the user doesn't click to the next page and skip queued thumbnails that haven't been displayed yet.
5284 if (!leftoverPosts[0])
5285 lastPageObject.paginator = paginator;
5286
5287 lastPageObject.page_num.push(pageNum);
5288 }
5289 }
5290
5291 // Queue up a new page object.
5292 var numNewPosts = leftoverPosts.length;
5293
5294 if (numNewPosts > 0 || (!endless_fill && !badPaginator) || (endless_fill && !bbb.endless.pages[0] && !lastPage)) { // Queue the page if: 1) There are thumbnails. 2) It's normal mode and not a "no results" page. 3) It's fill mode and there is no object to work with for future pages.
5295 var newPageObject = {
5296 page: newPage,
5297 page_num: [pageNum],
5298 paginator: paginator,
5299 ready: (!endless_fill || numNewPosts === limit || lastPage ? true : false)
5300 };
5301
5302 bbb.endless.pages.push(newPageObject);
5303
5304 if (bbb.endless.no_thumb_count < 10)
5305 bbb.endless.no_thumb_count = 0;
5306 }
5307 else if (!badPaginator)
5308 bbb.endless.no_thumb_count++;
5309
5310 // Get rid of the load more button for special circumstances where the paginator isn't accurate.
5311 if (lastPage && !bbb.endless.pages[0])
5312 bbb.el.endlessLoadButton.style.display = "none";
5313
5314 // Warn the user if this is a listing full of hidden posts.
5315 if (bbb.endless.no_thumb_count === 10) {
5316 bbbNotice("There have been no or very few thumbnails detected in the last 10 retrieved pages. Using endless pages with fill mode on this search could potentially be very slow or stall out completely. If you would like to continue, you may click the \"load more\" button near the bottom of the page.", -1);
5317 endlessPause();
5318 }
5319 else
5320 endlessQueueCheck();
5321 }
5322
5323 function endlessAppendPage() {
5324 // Prep the first queued page object and add it to the document.
5325 var firstPageObject = bbb.endless.pages.shift();
5326 var page = firstPageObject.page;
5327 var thumbContainer = getThumbContainer(gLoc);
5328 var before = getThumbSibling(gLoc);
5329
5330 // Prepare thumbnails.
5331 prepThumbnails(page);
5332
5333 // Page separators.
5334 var pageNum = firstPageObject.page_num;
5335 var numPageNum = pageNum.length;
5336 var firstNum = pageNum[0];
5337 var lastNum = (numPageNum > 1 ? pageNum[numPageNum - 1] : undefined);
5338
5339 if (endless_separator === "divider") {
5340 var divider = document.createElement("div");
5341 divider.className = "bbb-endless-divider";
5342
5343 var dividerLink = document.createElement("div");
5344 dividerLink.className = "bbb-endless-divider-link";
5345 divider.appendChild(dividerLink);
5346
5347 dividerLink.innerHTML = '<a href="' + updateURLQuery(location.href, {page: firstNum}) + '">Page ' + firstNum + '</a>' + (lastNum ? ' ~ <a href="' + updateURLQuery(location.href, {page: lastNum}) + '">Page ' + lastNum + '</a>' : '');
5348
5349 if (!bbb.endless.fill_first_page)
5350 page.insertBefore(divider, page.firstElementChild);
5351 else if (lastNum) // Only add the divider for filling the first page when there are actual posts added.
5352 thumbContainer.insertBefore(divider, (getPosts()[0] || before));
5353 }
5354 else if (endless_separator === "marker") {
5355 var markerContainer = document.createElement("article");
5356 markerContainer.className = "bbb-endless-marker-article";
5357
5358 var marker = document.createElement("div");
5359 marker.className = "bbb-endless-marker";
5360 markerContainer.appendChild(marker);
5361
5362 var markerLink = document.createElement("div");
5363 markerLink.className = "bbb-endless-marker-link";
5364 marker.appendChild(markerLink);
5365
5366 markerLink.innerHTML = '<a href="' + updateURLQuery(location.href, {page: firstNum}) + '">Page ' + firstNum + '</a>' + (lastNum ? '<br/>~<br/><a href="' + updateURLQuery(location.href, {page: lastNum}) + '">Page ' + lastNum + '</a>' : '');
5367
5368 if (!bbb.endless.fill_first_page)
5369 page.insertBefore(markerContainer, page.firstElementChild);
5370 else if (lastNum) // Only add the marker for filling the first page when there are actual posts added.
5371 thumbContainer.insertBefore(markerContainer, (getPosts()[0] || before));
5372 }
5373
5374 // Add the new page.
5375 if (!before)
5376 thumbContainer.appendChild(page);
5377 else
5378 thumbContainer.insertBefore(page, before);
5379
5380 // Replace the paginator.
5381 replacePaginator(firstPageObject.paginator);
5382
5383 // Fix hidden thumbnails.
5384 fixHiddenThumbs();
5385 bbbStatus("hidden", "new"); // Update status message with new amount.
5386
5387 if (!bbb.endless.fill_first_page)
5388 bbb.endless.append_page = false;
5389 else {
5390 bbb.endless.fill_first_page = false;
5391 endlessQueueCheck();
5392 }
5393
5394 if (quick_search.indexOf("remove") > -1 && bbb.quick_search !== "")
5395 endlessDelay(1100);
5396
5397 endlessCheck();
5398 }
5399
5400 function endlessNexURL() {
5401 // Get the URL of the next new page.
5402 return getPaginatorNextURL(bbb.endless.last_paginator);
5403 }
5404
5405 function endlessPageReady() {
5406 // Check if the first queued page object is ready to be appended.
5407 var firstPageObject = bbb.endless.pages[0];
5408
5409 return (firstPageObject && firstPageObject.ready);
5410 }
5411
5412 function endlessLastPage() {
5413 // Check if there isn't a next page.
5414 return (!endlessNexURL() || noResultsPage());
5415 }
5416
5417 function endlessPauseCheck() {
5418 // Check if loading needs to be paused due to the interval or default.
5419 if (bbb.endless.append_page)
5420 return;
5421
5422 var numPages = document.getElementsByClassName("bbb-endless-page").length + 1;
5423
5424 if (numPages % endless_pause_interval === 0 || (endless_default === "paused" && numPages === 1))
5425 endlessPause();
5426 }
5427
5428 function endlessPause() {
5429 // Pause endless pages so that it can't add any more pages.
5430 if (bbb.endless.paused || (endlessLastPage() && !bbb.endless.pages[0]))
5431 return;
5432
5433 bbb.endless.paused = true;
5434 bbb.endless.append_page = false;
5435 bbb.el.endlessLoadButton.style.display = "inline-block";
5436 }
5437
5438 function endlessDelay(ms) {
5439 // Delay endless pages for the provided number of milliseconds.
5440 bbb.timers.endlessDelay = window.setTimeout( function() {
5441 bbb.timers.endlessDelay = 0;
5442
5443 endlessCheck();
5444 }, ms);
5445 }
5446
5447 /* Blacklist Functions */
5448 function blacklistInit() {
5449 // Reset the blacklist with the account settings when logged in or script settings when logged out/using the override.
5450 var blacklistTags = accountSettingCheck("script_blacklisted_tags");
5451 var blacklistBox = document.getElementById("blacklist-box");
5452 var blacklistList = document.getElementById("blacklist-list");
5453 var enableLink = document.getElementById("re-enable-all-blacklists");
5454 var disableLink = document.getElementById("disable-all-blacklists");
5455 var blacklistedPosts = document.getElementsByClassName("blacklisted");
5456 var i, il; // Loop variables.
5457
5458 // Reset the list or create it as needed.
5459 if (blacklistBox && blacklistList) {
5460 blacklistBox.style.display = "none";
5461
5462 var childIndex = 0;
5463
5464 while (blacklistList.children[childIndex]) {
5465 var child = blacklistList.children[childIndex];
5466
5467 if (child.getElementsByTagName("a")[0] && child !== enableLink && child !== disableLink)
5468 blacklistList.removeChild(child);
5469 else
5470 childIndex++;
5471 }
5472 }
5473 else if (blacklist_add_bars) {
5474 var target, before; // If/else variables.
5475
5476 if (gLoc === "comment_search") {
5477 target = document.getElementById("a-index");
5478
5479 if (target)
5480 before = target.getElementsByClassName("comments-for-post")[0];
5481 }
5482 else if (gLoc === "comment") {
5483 target = document.getElementById("a-show");
5484
5485 if (target)
5486 before = target.getElementsByClassName("comments-for-post")[0];
5487 }
5488
5489 if (target && before && before.parentNode === target) {
5490 blacklistBox = document.createElement("div");
5491 blacklistBox.id = "blacklist-box";
5492 blacklistBox.className = "bbb-blacklist-box";
5493 blacklistBox.style.display = "none";
5494 blacklistBox.innerHTML = '<strong>Blacklisted: </strong> <ul id="blacklist-list"> <li id="disable-all-blacklists" style="display: inline;"><span class="link">Disable all</span></li> <li id="re-enable-all-blacklists" style="display: none;"><span class="link">Re-enable all</span></li> </ul>';
5495
5496 blacklistList = getId("blacklist-list", blacklistBox);
5497 enableLink = getId("re-enable-all-blacklists", blacklistBox);
5498 disableLink = getId("disable-all-blacklists", blacklistBox);
5499
5500 target.insertBefore(blacklistBox, before);
5501 }
5502 }
5503
5504 // Reset any blacklist info.
5505 if (bbb.blacklist.entries[0]) {
5506 delete bbb.blacklist;
5507 bbb.blacklist = {entries: [], match_list: {}, smart_view_target: undefined};
5508 }
5509
5510 // Reset any blacklisted thumbnails.
5511 var blacklistedPost = blacklistedPosts[0];
5512
5513 while (blacklistedPost) {
5514 blacklistedPost.bbbRemoveClass("blacklisted blacklisted-active");
5515 enablePostDDL(blacklistedPost);
5516 blacklistedPost = blacklistedPosts[0];
5517 }
5518
5519 // Check if there actually are any tags.
5520 if (!blacklistTags || !/[^\s,]/.test(blacklistTags))
5521 return;
5522
5523 // Preserve commas within nested/grouped tags.
5524 var groupsObject = replaceSearchGroups(blacklistTags);
5525 var groups = groupsObject.groups;
5526
5527 blacklistTags = groupsObject.search.replace(/,/g, "%,%");
5528 blacklistTags = restoreSearchGroups(blacklistTags, groups);
5529 blacklistTags = blacklistTags.split("%,%");
5530
5531 // Create the blacklist section.
5532 var cookies = getCookie();
5533 var blacklistDisabled = (cookies.dab === "1" && blacklistBox);
5534
5535 for (i = 0, il = blacklistTags.length; i < il; i++) {
5536 var blacklistTag = blacklistTags[i].bbbSpaceClean();
5537 var blacklistSearch = createSearch(blacklistTag);
5538
5539 if (blacklistSearch[0]) {
5540 var entryHash = blacklistTag.bbbHash();
5541 var entryDisabled = (blacklistDisabled || (blacklist_session_toggle && cookies["b" + entryHash] === "1") ? true : false);
5542 var newEntry = {active: !entryDisabled, tags:blacklistTag, search:blacklistSearch, matches: [], index: i, hash: entryHash};
5543
5544 bbb.blacklist.entries.push(newEntry);
5545
5546 if (blacklistList) {
5547 var blacklistItem = document.createElement("li");
5548 blacklistItem.title = blacklistTag;
5549 blacklistItem.className = "bbb-blacklist-item-" + i;
5550 blacklistItem.style.display = "none";
5551
5552 var blacklistLink = document.createElement("a");
5553 blacklistLink.innerHTML = (blacklistTag.length < 19 ? blacklistTag + " " : blacklistTag.substring(0, 18).bbbSpaceClean() + "... ");
5554 blacklistLink.className = "bbb-blacklist-entry-" + i + (entryDisabled ? " blacklisted-active" : "");
5555 blacklistLink.setAttribute("data-bbb-blacklist-entry", i);
5556 blacklistLink.addEventListener("click", blacklistEntryLinkToggle, false);
5557 blacklistItem.appendChild(blacklistLink);
5558
5559 var blacklistCount = document.createElement("span");
5560 blacklistCount.className = "count";
5561 blacklistCount.innerHTML = "0";
5562 blacklistItem.appendChild(blacklistCount);
5563
5564 blacklistList.appendChild(blacklistItem);
5565 }
5566 }
5567 }
5568
5569 // Replace the disable/enable all blacklist links with our own.
5570 if (enableLink && disableLink) {
5571 var newEnableLink = bbb.el.blacklistEnableLink = enableLink.cloneNode(true);
5572 var newDisableLink = bbb.el.blacklistDisableLink = disableLink.cloneNode(true);
5573
5574 newEnableLink.addEventListener("click", blacklistLinkToggle, false);
5575 newDisableLink.addEventListener("click", blacklistLinkToggle, false);
5576
5577 if (blacklistDisabled) {
5578 newEnableLink.style.display = "inline";
5579 newDisableLink.style.display = "none";
5580 }
5581 else {
5582 newEnableLink.style.display = "none";
5583 newDisableLink.style.display = "inline";
5584 }
5585
5586 enableLink.parentNode.replaceChild(newEnableLink, enableLink);
5587 disableLink.parentNode.replaceChild(newDisableLink, disableLink);
5588 }
5589
5590 // Test all posts on the page for a match and set up the initial blacklist.
5591 blacklistUpdate();
5592 }
5593
5594 function blacklistLinkToggle(event) {
5595 // Event listener function for permanently toggling the entire blacklist.
5596 if (event.button !== 0)
5597 return;
5598
5599 var blacklistDisabled = (getCookie().dab === "1");
5600 var entries = bbb.blacklist.entries;
5601
5602 if (blacklistDisabled) {
5603 bbb.el.blacklistEnableLink.style.display = "none";
5604 bbb.el.blacklistDisableLink.style.display = "inline";
5605 createCookie("dab", 0, 365);
5606 }
5607 else {
5608 bbb.el.blacklistEnableLink.style.display = "inline";
5609 bbb.el.blacklistDisableLink.style.display = "none";
5610 createCookie("dab", 1, 365);
5611 }
5612
5613 for (var i = 0, il = entries.length; i < il; i++) {
5614 var entry = entries[i];
5615
5616 if (blacklistDisabled) {
5617 if (!entry.active)
5618 blacklistEntryToggle(i);
5619 }
5620 else {
5621 if (entry.active)
5622 blacklistEntryToggle(i);
5623
5624 if (blacklist_session_toggle)
5625 createCookie("b" + entry.hash, 0, -1);
5626 }
5627 }
5628
5629 event.preventDefault();
5630 }
5631
5632 function blacklistEntryLinkToggle(event) {
5633 // Event listener function for blacklist entry toggle links.
5634 if (event.button !== 0)
5635 return;
5636
5637 var entryNumber = Number(event.target.getAttribute("data-bbb-blacklist-entry"));
5638
5639 blacklistEntryToggle(entryNumber);
5640
5641 event.preventDefault();
5642 }
5643
5644 function blacklistEntryToggle(entryIndex) {
5645 // Toggle a blacklist entry and adjust all of its related elements.
5646 var entry = bbb.blacklist.entries[entryIndex];
5647 var matches = entry.matches;
5648 var links = document.getElementsByClassName("bbb-blacklist-entry-" + entryIndex);
5649 var blacklistDisabled = (getCookie().dab === "1");
5650 var i, il, j, jl, id, els, matchList; // Loop variables.
5651
5652 if (entry.active) {
5653 entry.active = false;
5654
5655 if (blacklist_session_toggle && !blacklistDisabled)
5656 createCookie("b" + entry.hash, 1);
5657
5658 for (i = 0, il = links.length; i < il; i++)
5659 links[i].bbbAddClass("blacklisted-active");
5660
5661 for (i = 0, il = matches.length; i < il; i++) {
5662 id = matches[i];
5663 matchList = bbb.blacklist.match_list[id];
5664
5665 matchList.count--;
5666
5667 if (!matchList.count && matchList.override !== false) {
5668 if (id === "image-container")
5669 document.getElementById("image-container").bbbRemoveClass("blacklisted-active");
5670 else {
5671 els = document.getElementsByClassName(id);
5672
5673 for (j = 0, jl = els.length; j < jl; j++)
5674 blacklistShowPost(els[j]);
5675 }
5676 }
5677 }
5678 }
5679 else {
5680 entry.active = true;
5681
5682 if (blacklist_session_toggle)
5683 createCookie("b" + entry.hash, 0, -1);
5684
5685 for (i = 0, il = links.length; i < il; i++)
5686 links[i].bbbRemoveClass("blacklisted-active");
5687
5688 for (i = 0, il = matches.length; i < il; i++) {
5689 id = matches[i];
5690 matchList = bbb.blacklist.match_list[id];
5691
5692 matchList.count++;
5693
5694 if (matchList.override !== true) {
5695 if (id === "image-container")
5696 document.getElementById("image-container").bbbAddClass("blacklisted-active");
5697 else {
5698 els = document.getElementsByClassName(id);
5699
5700 for (j = 0, jl = els.length; j < jl; j++)
5701 blacklistHidePost(els[j]);
5702 }
5703 }
5704 }
5705 }
5706 }
5707
5708 function blacklistUpdate(target) {
5709 // Update the blacklists without resetting everything.
5710 if (!bbb.blacklist.entries[0])
5711 return;
5712
5713 // Retrieve the necessary elements from the target element or current document.
5714 var blacklistBox = getId("blacklist-box", target) || document.getElementById("blacklist-box");
5715 var blacklistList = getId("blacklist-list", target) || document.getElementById("blacklist-list");
5716 var imgContainer = getId("image-container", target);
5717 var posts = getPosts(target);
5718
5719 var i, il; // Loop variables.
5720
5721 // Test the image for a match when viewing a post.
5722 if (imgContainer) {
5723 var imgId = imgContainer.getAttribute("data-id");
5724
5725 if (!blacklistSmartViewCheck(imgId))
5726 blacklistTest(imgContainer);
5727 }
5728
5729 // Search the posts for matches.
5730 for (i = 0, il = posts.length; i < il; i++)
5731 blacklistTest(posts[i]);
5732
5733 // Update the blacklist sidebar section match counts and display any blacklist items that have a match.
5734 if (blacklistBox && blacklistList) {
5735 for (i = 0, il = bbb.blacklist.entries.length; i < il; i++) {
5736 var entryLength = bbb.blacklist.entries[i].matches.length;
5737 var item = blacklistList.getElementsByClassName("bbb-blacklist-item-" + i)[0];
5738
5739 if (entryLength) {
5740 blacklistBox.style.display = "block";
5741 item.style.display = "";
5742 item.getElementsByClassName("count")[0].innerHTML = entryLength;
5743 }
5744 }
5745 }
5746 }
5747
5748 function blacklistTest(el) {
5749 // Test a post/image for a blacklist match and use its ID to store its info.
5750 var id = el.id;
5751 var matchList = bbb.blacklist.match_list[id];
5752
5753 // Test posts that haven't been tested yet.
5754 if (typeof(matchList) === "undefined") {
5755 matchList = bbb.blacklist.match_list[id] = {count: undefined, matches: [], override: undefined};
5756
5757 for (var i = 0, il = bbb.blacklist.entries.length; i < il; i++) {
5758 var entry = bbb.blacklist.entries[i];
5759
5760 if (thumbSearchMatch(el, entry.search)) {
5761 if (entry.active)
5762 matchList.count = ++matchList.count || 1;
5763 else
5764 matchList.count = matchList.count || 0;
5765
5766 matchList.matches.push(entry);
5767 entry.matches.push(id);
5768 }
5769 }
5770
5771 if (matchList.count === undefined) // No match.
5772 matchList.count = false;
5773 }
5774
5775 // Check the saved blacklist info for the post and change the thumbnail as needed.
5776 if (matchList.count !== false && !el.bbbHasClass("blacklisted")) {
5777 el.bbbAddClass("blacklisted");
5778
5779 if (matchList.count > 0 && matchList.override !== true)
5780 blacklistHidePost(el);
5781
5782 if (el.id !== "image-container") {
5783 if (blacklist_thumb_controls)
5784 blacklistPostControl(el, matchList);
5785
5786 if (blacklist_smart_view)
5787 blacklistSmartView(el);
5788 }
5789 }
5790 }
5791
5792 function blacklistPostControl(el, matchList) {
5793 // Add the blacklist post controls to a thumbnail.
5794 var target = el.getElementsByClassName("preview")[0] || el;
5795 var id = el.id;
5796 var tip = bbb.el.blacklistTip;
5797
5798 if (!tip) { // Create the tip if it doesn't exist.
5799 tip = bbb.el.blacklistTip = document.createElement("div");
5800 tip.id = "bbb-blacklist-tip";
5801 document.body.appendChild(tip);
5802 }
5803
5804 if (target) {
5805 // Set up the tip events listeners for hiding and displaying it.
5806 target.addEventListener("click", function(event) {
5807 if (event.button !== 0 || event.ctrlKey || event.shiftKey || event.altKey)
5808 return;
5809
5810 var target = event.target;
5811 var blacklistTip = bbb.el.blacklistTip;
5812 var i, il; // Loop variables.
5813
5814 if (!el.bbbHasClass("blacklisted-active") || (target.tagName === "A" && !target.bbbHasClass("bbb-thumb-link"))) // If the thumb isn't currently hidden or a link that isn't the thumb link is clicked, allow the link click.
5815 return;
5816
5817 if (blacklistTip.style.display !== "block") {
5818 var matchEntries = matchList.matches;
5819 var tipContent = document.createDocumentFragment();
5820
5821 var header = document.createElement("b");
5822 header.innerHTML = "Blacklist Matches";
5823 tipContent.appendChild(header);
5824
5825 var list = document.createElement("ul");
5826 tipContent.appendChild(list);
5827
5828 for (i = 0, il = matchEntries.length; i < il; i++) {
5829 var matchEntry = matchEntries[i];
5830 var entryIndex = matchEntry.index;
5831 var blacklistTag = matchEntry.tags;
5832
5833 var blacklistItem = document.createElement("li");
5834 blacklistItem.title = blacklistTag;
5835
5836 var blacklistLink = document.createElement("a");
5837 blacklistLink.href = "#";
5838 blacklistLink.className = "bbb-blacklist-entry-" + entryIndex + (matchEntry.active ? "" : " blacklisted-active");
5839 blacklistLink.setAttribute("data-bbb-blacklist-entry", entryIndex);
5840 blacklistLink.innerHTML = (blacklistTag.length < 51 ? blacklistTag + " " : blacklistTag.substring(0, 50).bbbSpaceClean() + "...");
5841 blacklistLink.addEventListener("click", blacklistEntryLinkToggle, false);
5842 blacklistItem.appendChild(blacklistLink);
5843
5844 list.appendChild(blacklistItem);
5845 }
5846
5847 var viewLinkDiv = document.createElement("div");
5848 viewLinkDiv.style.marginTop = "1em";
5849 viewLinkDiv.style.textAlign = "center";
5850 viewLinkDiv.innerHTML = '<a class="bbb-post-link" id="bbb-blacklist-view-link" href="/posts/' + id.match(/\d+/)[0] + '">View post</a>';
5851 tipContent.appendChild(viewLinkDiv);
5852
5853 if (blacklist_smart_view) {
5854 var viewLink = getId("bbb-blacklist-view-link", viewLinkDiv);
5855
5856 if (viewLink) {
5857 viewLink.addEventListener("click", function(event) {
5858 if (event.button === 0)
5859 blacklistSmartViewUpdate(el);
5860 }, false);
5861 }
5862 }
5863
5864 blacklistShowTip(event, tipContent);
5865 }
5866 else {
5867 var els = document.getElementsByClassName(id);
5868
5869 for (i = 0, il = els.length; i < il; i++)
5870 blacklistShowPost(els[i]);
5871
5872 blacklistHideTip();
5873 bbb.blacklist.match_list[id].override = true;
5874 }
5875
5876 event.preventDefault();
5877 event.stopPropagation();
5878 }, true);
5879 target.addEventListener("mouseleave", function() { bbb.timers.blacklistTip = window.setTimeout(blacklistHideTip, 100); }, false);
5880 tip.addEventListener("mouseover", function() { window.clearTimeout(bbb.timers.blacklistTip); }, false);
5881 tip.addEventListener("mouseleave", blacklistHideTip, false);
5882
5883 // Add the hide button.
5884 var hide = document.createElement("span");
5885 hide.className = "bbb-close-circle";
5886 hide.addEventListener("click", function(event) {
5887 if (event.button !== 0)
5888 return;
5889
5890 var els = document.getElementsByClassName(id);
5891
5892 for (var i = 0, il = els.length; i < il; i++)
5893 blacklistHidePost(els[i]);
5894
5895 bbb.blacklist.match_list[id].override = false;
5896 }, false);
5897 target.appendChild(hide);
5898 }
5899 }
5900
5901 function blacklistShowTip(event, content) {
5902 // Display the blacklist control tip.
5903 var x = event.pageX;
5904 var y = event.pageY;
5905 var tip = bbb.el.blacklistTip;
5906
5907 formatTip(event, tip, content, x, y);
5908 }
5909
5910 function blacklistHideTip() {
5911 // Reset the blacklist control tip to hidden.
5912 var tip = bbb.el.blacklistTip;
5913
5914 if (tip)
5915 tip.removeAttribute("style");
5916 }
5917
5918 function blacklistSmartView(el) {
5919 // Set up the smart view event listeners.
5920 var img = el.getElementsByTagName("img")[0];
5921 var link = (img ? img.parentNode : undefined);
5922
5923 if (!link)
5924 return;
5925
5926 // Normal left click support.
5927 link.addEventListener("click", function(event) {
5928 if (event.button === 0)
5929 blacklistSmartViewUpdate(el);
5930 }, false);
5931
5932 // Right and middle button click support.
5933 link.addEventListener("mousedown", function(event) {
5934 if (event.button === 1)
5935 bbb.blacklist.smart_view_target = link;
5936 }, false);
5937 link.addEventListener("mouseup", function(event) {
5938 if (event.button === 1 && bbb.blacklist.smart_view_target === link)
5939 blacklistSmartViewUpdate(el);
5940 else if (event.button === 2)
5941 blacklistSmartViewUpdate(el);
5942 }, false);
5943 }
5944
5945 function blacklistSmartViewUpdate(el) {
5946 // Update the blacklisted thumbnail info in the smart view object.
5947 var time = new Date().getTime();
5948 var id = el.getAttribute("data-id");
5949 var smartView = localStorage.getItem("bbb_smart_view");
5950
5951 if (smartView === null) // Initialize the object if it doesn't exist.
5952 smartView = {last: time};
5953 else {
5954 smartView = JSON.parse(smartView);
5955
5956 if (time - smartView.last > 60000) // Reset the object if it hasn't been changed within a minute.
5957 smartView = {last: time};
5958 else
5959 smartView.last = time; // Adjust the object.
5960 }
5961
5962 if (!el.bbbHasClass("blacklisted-active"))
5963 smartView[id] = time;
5964 else
5965 delete smartView[id];
5966
5967 localStorage.bbbSetItem("bbb_smart_view", JSON.stringify(smartView));
5968 }
5969
5970 function blacklistSmartViewCheck(id) {
5971 // Check whether to display the post during the blacklist init.
5972 var smartView = localStorage.getItem("bbb_smart_view");
5973
5974 if (!blacklist_smart_view || smartView === null)
5975 return false;
5976 else {
5977 var time = new Date().getTime();
5978
5979 smartView = JSON.parse(smartView);
5980
5981 if (time - smartView.last > 60000) { // Delete the ids if the object hasn't been changed within a minute.
5982 localStorage.removeItem("bbb_smart_view");
5983 return false;
5984 }
5985 else if (!smartView[id]) // Return false if the id isn't found.
5986 return false;
5987 else if (time - smartView[id] > 60000) // Return false if the click is over a minute ago.
5988 return false;
5989 }
5990
5991 return true;
5992 }
5993
5994 function blacklistHidePost(post) {
5995 // Hide blacklisted post content and adjust related content.
5996 post.bbbAddClass("blacklisted-active");
5997 disablePostDDL(post);
5998 }
5999
6000 function blacklistShowPost(post) {
6001 // Reveal blacklisted post content and adjust related content.
6002 post.bbbRemoveClass("blacklisted-active");
6003 enablePostDDL(post);
6004 }
6005
6006 /* Other functions */
6007 function modifyDanbScript() {
6008 // Modify some Danbooru functions so that they don't run unnecessarily.
6009 var loadNotes = Danbooru.Note.load_all;
6010
6011 Danbooru.Note.load_all = function(allow) {
6012 if (allow === "bbb")
6013 loadNotes();
6014 };
6015
6016 Danbooru.Blacklist.initialize_all = function() {
6017 return;
6018 };
6019 }
6020
6021 function modifyPage() {
6022 // Determine what function may be needed to fix/update original content.
6023 checkStateCache();
6024
6025 if (noXML())
6026 return;
6027
6028 var allowAPI = useAPI();
6029 var stateCache = (history.state || {}).bbb_posts_cache;
6030
6031 if (gLoc === "post")
6032 delayMe(parsePost); // Delay is needed to force the script to pause and allow Danbooru to do whatever. It essentially mimics the async nature of the API call.
6033 else if (gLoc === "comment_search" || gLoc === "comment")
6034 delayMe(fixCommentSearch);
6035 else if (stateCache) // Use a cached set of thumbnails.
6036 delayMe(function() { parseListing(JSON.parse(stateCache)); });
6037 else if (allowAPI && potentialHiddenPosts(gLoc)) // API only features.
6038 searchJSON(gLoc);
6039 else if (!allowAPI && allowUserLimit()) // Alternate mode for features.
6040 searchPages(gLoc);
6041 else
6042 saveStateCache();
6043 }
6044
6045 function formatInfo(post) {
6046 // Add information to/alter information in the post object.
6047 if (!post)
6048 return undefined;
6049
6050 // Figure out the thumbnail classes.
6051 var flags = "";
6052 var thumbClass = "";
6053
6054 if (post.is_deleted) {
6055 flags += " deleted";
6056 thumbClass += " post-status-deleted";
6057 }
6058 if (post.is_pending) {
6059 flags += " pending";
6060 thumbClass += " post-status-pending";
6061 }
6062 if (post.is_banned)
6063 flags += " banned";
6064 if (post.is_flagged) {
6065 flags += " flagged";
6066 thumbClass += " post-status-flagged";
6067 }
6068 if (post.has_children && (post.has_active_children || show_deleted))
6069 thumbClass += " post-status-has-children";
6070 if (post.parent_id)
6071 thumbClass += " post-status-has-parent";
6072
6073 // Figure out sample image dimensions and ratio.
6074 post.sample_ratio = (post.image_width > 850 ? 850 / post.image_width : 1);
6075 post.sample_height = Math.round(post.image_height * post.sample_ratio);
6076 post.sample_width = Math.round(post.image_width * post.sample_ratio);
6077
6078 // Hidden post fixes.
6079 post.md5 = post.md5 || "";
6080 post.file_ext = post.file_ext || "";
6081 post.preview_file_url = post.preview_file_url || bbbHiddenImg;
6082 post.large_file_url = post.large_file_url || "";
6083 post.file_url = post.file_url || "";
6084
6085 // Potential null value fixes.
6086 post.approver_id = post.approver_id || "";
6087 post.parent_id = post.parent_id || "";
6088 post.pixiv_id = post.pixiv_id || "";
6089
6090 // Missing API/data fixes.
6091 post.has_sound = (typeof(post.has_sound) === "boolean" ? post.has_sound : /(?:^|\s)(?:video|flash)_with_sound(?:$|\s)/.test(post.tag_string));
6092
6093 post.flags = flags.bbbSpaceClean();
6094 post.thumb_class = thumbClass;
6095
6096 return post;
6097 }
6098
6099 function fixPaginator(target) {
6100 // Determine whether the paginator needs to be updated and request one as needed.
6101 var paginator = getPaginator(target);
6102
6103 if (!paginator || gLoc === "pool" || gLoc === "favorite_group" || !allowUserLimit())
6104 return;
6105
6106 if (/\d/.test(paginator.textContent)) { // Fix numbered paginators.
6107 // Fix existing paginator with user's custom limit.
6108 var pageLinks = paginator.getElementsByTagName("a");
6109
6110 for (var i = 0, il = pageLinks.length; i < il; i++) {
6111 var pageLink = pageLinks[i];
6112 pageLink.href = updateURLQuery(pageLink.href, {limit: thumbnail_count});
6113 }
6114
6115 searchPages("paginator");
6116 }
6117 else { // Fix next/previous paginators.
6118 paginator.innerHTML = "<p>Loading...</p>"; // Disable the paginator while fixing it.
6119
6120 searchPages("paginator");
6121 }
6122 }
6123
6124 function fixCommentSearch() {
6125 // Fix the thumbnails for hidden posts in the comment search.
6126 var posts = getPosts();
6127
6128 for (var i = 0, il = posts.length; i < il; i++) {
6129 var post = posts[i];
6130 var hasImg = post.getElementsByTagName("img")[0];
6131 var tags = post.getAttribute("data-tags");
6132 var previewUrl = post.getAttribute("data-preview-file-url");
6133
6134 // If the information for fixing the thumbnails is missing, stop checking.
6135 if (!hasImg && !previewUrl)
6136 return;
6137
6138 // Skip posts with content the user doesn't want or that already have images.
6139 if (hasImg || (!show_loli && /(?:^|\s)loli(?:$|\s)/.test(tags)) || (!show_shota && /(?:^|\s)shota(?:$|\s)/.test(tags)) || (!show_toddlercon && /(?:^|\s)toddlercon(?:$|\s)/.test(tags)) || (!show_banned && /(?:^|\s)banned(?:$|\s)/.test(post.getAttribute("data-flags"))) || safebPostTest(post))
6140 continue;
6141
6142 var preview = post.getElementsByClassName("preview")[0];
6143 var before = preview.firstElementChild;
6144
6145 var thumb = document.createElement("a");
6146 thumb.href = "/posts/" + post.getAttribute("data-id");
6147 thumb.innerHTML = '<img src="' + previewUrl + '" alt="' + post.getAttribute("data-md5") + '" title="' + tags + ' user:' + post.getAttribute("data-uploader") + ' rating:' + post.getAttribute("data-rating") + ' score:' + post.getAttribute("data-score") + '">';
6148
6149 if (before)
6150 preview.insertBefore(thumb, before);
6151 else
6152 preview.appendChild(thumb);
6153
6154 prepThumbnails(post);
6155 }
6156 }
6157
6158 function bbbNotice(txt, noticeType) {
6159 // Display the notice or add information to it if it already exists.
6160 // A secondary number argument can be provided: -1 = error, 0 = permanent, >0 = temporary (disappears after X seconds where X equals the number provided), "no number" = temporary for 6 seconds
6161 var notice = bbb.el.notice;
6162 var noticeMsg = bbb.el.noticeMsg;
6163 var type = (typeof(noticeType) !== "undefined" ? noticeType : 6);
6164
6165 var msg = document.createElement("div");
6166 msg.className = "bbb-notice-msg-entry";
6167 msg.innerHTML = txt;
6168 msg.style.color = (type === -1 ? "#FF0000" : "#000000");
6169
6170 if (!notice) {
6171 var noticeContainer = document.createElement("span"); // Will contain the Danbooru notice and BBB notice so that they can "stack" and reposition as the other disappears.
6172 noticeContainer.id = "bbb-notice-container";
6173
6174 var danbNotice = document.getElementById("notice");
6175
6176 // Override Danbooru notice styling to make it get along with the notice container.
6177 if (danbNotice) {
6178 danbNotice.style.marginBottom = "10px";
6179 danbNotice.style.width = "100%";
6180 danbNotice.style.position = "relative";
6181 danbNotice.style.top = "0px";
6182 danbNotice.style.left = "0px";
6183 noticeContainer.appendChild(danbNotice);
6184 }
6185
6186 notice = bbb.el.notice = document.createElement("div");
6187 notice.id = "bbb-notice";
6188 notice.innerHTML = '<div style="position:absolute; left: 3px; font-weight: bold;">BBB:</div><div id="bbb-notice-msg"></div><div style="position: absolute; top: 3px; right: 3px; cursor: pointer;" class="close-button ui-icon ui-icon-closethick" id="bbb-notice-close"></div>';
6189 noticeContainer.appendChild(notice);
6190
6191 noticeMsg = bbb.el.noticeMsg = getId("bbb-notice-msg", notice);
6192
6193 getId("bbb-notice-close", notice).addEventListener("click", function(event) {
6194 if (event.button !== 0)
6195 return;
6196
6197 closeBbbNotice();
6198 event.preventDefault();
6199 }, false);
6200
6201 document.body.appendChild(noticeContainer);
6202 }
6203
6204 if (bbb.timers.keepBbbNotice)
6205 window.clearTimeout(bbb.timers.keepBbbNotice);
6206
6207 if (notice.style.display === "block" && /\S/.test(noticeMsg.textContent)) { // Insert new text at the top if the notice is already open and has an actual message.
6208 noticeMsg.insertBefore(msg, noticeMsg.firstElementChild);
6209
6210 // Don't allow the notice to be closed via clicking for half a second. Prevents accidental message closing.
6211 bbb.timers.keepBbbNotice = window.setTimeout(function() {
6212 bbb.timers.keepBbbNotice = 0;
6213 }, 500);
6214 }
6215 else { // Make sure the notice is clear and put in the first message.
6216 noticeMsg.innerHTML = "";
6217 noticeMsg.appendChild(msg);
6218 }
6219
6220 // Hide the notice after a certain number of seconds.
6221 if (type > 0) {
6222 window.setTimeout(function() {
6223 closeBbbNoticeMsg(msg);
6224 }, type * 1000);
6225 }
6226
6227 notice.style.display = "block";
6228
6229 return msg;
6230 }
6231
6232 function closeBbbNotice() {
6233 // Click handler for closing the notice.
6234 if (bbb.timers.keepBbbNotice)
6235 return;
6236
6237 bbb.el.notice.style.display = "none";
6238 }
6239
6240 function closeBbbNoticeMsg(el) {
6241 // Closes the provided notice message or the whole notice if there is only one message.
6242 var notice = bbb.el.notice;
6243 var target = el;
6244 var targetParent = target.parentNode;
6245
6246 if (notice.getElementsByClassName("bbb-notice-msg-entry").length < 2)
6247 closeBbbNotice();
6248 else if (targetParent)
6249 targetParent.removeChild(target);
6250 }
6251
6252 function bbbStatus(mode, xmlState) {
6253 // Updates the BBB status message.
6254 // xmlState: "new" = opening an XML request, "done" = closing an xml request, "error" = xml request failed.
6255 if (!enable_status_message)
6256 return;
6257
6258 var status = bbb.el.status;
6259
6260 // Set up the status message if it isn't ready.
6261 if (!status) {
6262 bbb.status = { // Status messages.
6263 msg: {
6264 post_comments: {txt: "Fixing hidden comments... ", count: 0},
6265 hidden: {txt: "Fixing hidden thumbnails... ", count: 0, queue: document.getElementsByClassName("bbb-hidden-thumb")}, // Hidden thumbnail message.
6266 posts: {txt: "Loading post info... ", count: 0} // General message for XML requests for hidden posts.
6267 },
6268 count: 0
6269 };
6270
6271 var msgList = bbb.status.msg;
6272
6273 status = bbb.el.status = document.createElement("div");
6274 status.id = "bbb-status";
6275
6276 for (var i in msgList) {
6277 if (msgList.hasOwnProperty(i)) {
6278 var curMsg = msgList[i];
6279
6280 var msgDiv = curMsg.el = document.createElement("div");
6281 msgDiv.style.display = "none";
6282 msgDiv.innerHTML = curMsg.txt;
6283 status.appendChild(msgDiv);
6284
6285 curMsg.info = document.createElement("span");
6286 msgDiv.appendChild(curMsg.info);
6287 }
6288 }
6289
6290 document.body.appendChild(status);
6291 }
6292
6293 var msg = bbb.status.msg[mode];
6294 var newCount = 0;
6295
6296 if (msg.queue) { // If the xml requests are queued, use the queue length as the current remaining value.
6297 newCount = (xmlState === "error" ? 0 : msg.queue.length);
6298 bbb.status.count += newCount - msg.count;
6299 msg.count = newCount;
6300 msg.info.innerHTML = newCount; // Update the displayed number of requests remaining.
6301 }
6302 else { // For simultaneous xml requests, just increment/decrement.
6303 if (xmlState === "new")
6304 newCount = 1;
6305 else if (xmlState === "done" || xmlState === "error")
6306 newCount = -1;
6307
6308 bbb.status.count += newCount;
6309 msg.count += newCount;
6310 }
6311
6312 if (msg.count)
6313 msg.el.style.display = "block";
6314 else
6315 msg.el.style.display = "none";
6316
6317 if (bbb.status.count) // If requests are pending, display the notice.
6318 status.style.display = "block";
6319 else // If requests are done, hide the notice.
6320 status.style.display = "none";
6321 }
6322
6323 function bbbDialog(content, properties) {
6324 // Open a dialog window that can have a predefined ok button (default) and/or cancel button. The properties object specifies dialog behavior and has the following values:
6325 // ok/cancel: true to display the button, false to hide the button, function to display the button and specify a custom function for it
6326 // condition: string to name a basic flag that will be checked/set by a dialog before displaying it, function to check custom conditions for a dialog before displaying it
6327 // important: true to prioritize a dialog if it goes in the queue, false to allow a dialog to go to the end of the queue as normal
6328
6329 var prop = properties || {};
6330 var okButton = (prop.ok === undefined ? true : prop.ok);
6331 var cancelButton = (prop.cancel === undefined ? false : prop.cancel);
6332 var condition = (prop.condition === undefined ? false : prop.condition);
6333 var important = (prop.important === undefined ? false : prop.important);
6334
6335 // Queue the dialog window if one is already open.
6336 if (document.getElementById("bbb-dialog-blocker")) {
6337 if (important)
6338 bbb.dialog.queue.unshift({content: content, properties: properties});
6339 else
6340 bbb.dialog.queue.push({content: content, properties: properties});
6341
6342 return;
6343 }
6344
6345 // Test whether the dialog window should be allowed to display.
6346 if (condition) {
6347 var conditionType = typeof(condition);
6348
6349 if ((conditionType === "string" && bbb.flags[condition]) || (conditionType === "function" && condition())) {
6350 nextBbbDialog();
6351 return;
6352 }
6353 else if (conditionType === "string")
6354 bbb.flags[condition] = true;
6355 }
6356
6357 // Create the dialog window.
6358 var blockDiv = document.createElement("div");
6359 blockDiv.id = "bbb-dialog-blocker";
6360
6361 var windowDiv = document.createElement("div");
6362 windowDiv.id = "bbb-dialog-window";
6363 windowDiv.tabIndex = "-1";
6364 blockDiv.appendChild(windowDiv);
6365
6366 var contentDiv = windowDiv;
6367
6368 if (okButton) {
6369 var ok = document.createElement("a");
6370 ok.innerHTML = "OK";
6371 ok.href = "#";
6372 ok.className = "bbb-dialog-button";
6373
6374 if (typeof(okButton) === "function")
6375 ok.addEventListener("click", okButton, false);
6376
6377 ok.addEventListener("click", closeBbbDialog, false);
6378
6379 okButton = ok;
6380 }
6381
6382 if (cancelButton) {
6383 var cancel = document.createElement("a");
6384 cancel.innerHTML = "Cancel";
6385 cancel.href = "#";
6386 cancel.className = "bbb-dialog-button";
6387 cancel.style.cssFloat = "right";
6388
6389 if (typeof(cancelButton) === "function")
6390 cancel.addEventListener("click", cancelButton, false);
6391
6392 cancel.addEventListener("click", closeBbbDialog, false);
6393
6394 cancelButton = cancel;
6395 }
6396
6397 if (okButton || cancelButton) {
6398 contentDiv = document.createElement("div");
6399 contentDiv.className = "bbb-dialog-content-div";
6400 windowDiv.appendChild(contentDiv);
6401
6402 var buttonDiv = document.createElement("div");
6403 buttonDiv.className = "bbb-dialog-button-div";
6404 windowDiv.appendChild(buttonDiv);
6405
6406 if (okButton)
6407 buttonDiv.appendChild(okButton);
6408
6409 if (cancelButton)
6410 buttonDiv.appendChild(cancelButton);
6411
6412 // Only allow left clicks to trigger the prompt buttons.
6413 buttonDiv.addEventListener("click", function(event) {
6414 if (event.button !== 0)
6415 event.stopPropagation();
6416 }, true);
6417 }
6418
6419 if (typeof(content) === "string")
6420 contentDiv.innerHTML = content;
6421 else
6422 contentDiv.appendChild(content);
6423
6424 document.body.appendChild(blockDiv);
6425
6426 (okButton || cancelButton || windowDiv).focus();
6427 }
6428
6429 function closeBbbDialog(event) {
6430 // Close the current dialog window.
6431 var dialogBlocker = document.getElementById("bbb-dialog-blocker");
6432
6433 if (dialogBlocker)
6434 document.body.removeChild(dialogBlocker);
6435
6436 nextBbbDialog();
6437
6438 event.preventDefault();
6439 }
6440
6441 function nextBbbDialog() {
6442 // Open the next queued dialog window.
6443 var nextDialog = bbb.dialog.queue.shift();
6444
6445 if (nextDialog)
6446 bbbDialog(nextDialog.content, nextDialog.properties);
6447 }
6448
6449 function thumbSearchMatch(post, searchArray) {
6450 // Take search objects and test them against a thumbnail's info.
6451 if (!searchArray[0])
6452 return false;
6453
6454 var postInfo; // If/else variable.
6455
6456 if (post instanceof Element) {
6457 var tags = post.getAttribute("data-tags");
6458 var flags = post.getAttribute("data-flags") || "active";
6459 var rating = " rating:" + post.getAttribute("data-rating");
6460 var status = " status:" + (flags === "flagged" ? flags + " active" : flags).replace(/\s/g, " status:");
6461 var user = " user:" + post.getAttribute("data-uploader").replace(/\s/g, "_").toLowerCase();
6462 var poolData = " " + post.getAttribute("data-pools");
6463 var pools = (/pool:\d+/.test(poolData) && !/pool:(collection|series)/.test(poolData) ? poolData + " pool:inactive" : poolData);
6464 var score = post.getAttribute("data-score");
6465 var favcount = post.getAttribute("data-fav-count");
6466 var id = post.getAttribute("data-id");
6467 var width = post.getAttribute("data-width");
6468 var height = post.getAttribute("data-height");
6469 var parentId = post.getAttribute("data-parent-id");
6470 var parent = (parentId ? " parent:" + parentId : "");
6471 var hasChildren = post.getAttribute("data-has-children");
6472 var child = (hasChildren === "true" ? " child:true" : "");
6473
6474 postInfo = {
6475 tags: tags.bbbSpacePad(),
6476 metatags:(rating + status + user + pools + parent + child).bbbSpacePad(),
6477 score: Number(score),
6478 favcount: Number(favcount),
6479 id: Number(id),
6480 width: Number(width),
6481 height: Number(height)
6482 };
6483 }
6484 else
6485 postInfo = post;
6486
6487 var j, jl, searchTerm; // Loop variables.
6488
6489 for (var i = 0, il = searchArray.length; i < il; i++) {
6490 var searchObject = searchArray[i];
6491 var all = searchObject.all;
6492 var any = searchObject.any;
6493
6494 // Continue to the next matching rule if there are no tags to test.
6495 if (!any.total && !all.total)
6496 continue;
6497
6498 if (any.total) {
6499 var anyResult = false;
6500
6501 // Loop until one positive match is found.
6502 for (j = 0, jl = any.includes.length; j < jl; j++) {
6503 searchTerm = any.includes[j];
6504
6505 if (thumbTagMatch(postInfo, searchTerm)) {
6506 anyResult = true;
6507 break;
6508 }
6509 }
6510
6511 // If we don't have a positive match yet, loop through the excludes.
6512 if (!anyResult) {
6513 for (j = 0, jl = any.excludes.length; j < jl; j++) {
6514 searchTerm = any.excludes[j];
6515
6516 if (!thumbTagMatch(postInfo, searchTerm)) {
6517 anyResult = true;
6518 break;
6519 }
6520 }
6521 }
6522
6523 // Continue to the next matching rule if none of the "any" tags matched.
6524 if (!anyResult)
6525 continue;
6526 }
6527
6528 if (all.total) {
6529 var allResult = true;
6530
6531 // Loop until a negative match is found.
6532 for (j = 0, jl = all.includes.length; j < jl; j++) {
6533 searchTerm = all.includes[j];
6534
6535 if (!thumbTagMatch(postInfo, searchTerm)) {
6536 allResult = false;
6537 break;
6538 }
6539 }
6540
6541 // If we still have a positive match, loop through the excludes.
6542 if (allResult) {
6543 for (j = 0, jl = all.excludes.length; j < jl; j++) {
6544 searchTerm = all.excludes[j];
6545
6546 if (thumbTagMatch(postInfo, searchTerm)) {
6547 allResult = false;
6548 break;
6549 }
6550 }
6551 }
6552
6553 // Continue to the next matching rule if one of the "all" tags didn't match.
6554 if (!allResult)
6555 continue;
6556 }
6557
6558 // Loop completed without a negative match so return true.
6559 return true;
6560 }
6561
6562 // If we haven't managed a positive match for any rules, return false.
6563 return false;
6564 }
6565
6566 function thumbTagMatch(postInfo, tag) {
6567 // Test thumbnail info for a tag match.
6568 var targetTags; // If/else variable.
6569
6570 if (typeof(tag) === "string") { // Check regular tags and metatags with string values.
6571 targetTags = (isMetatag(tag) ? postInfo.metatags : postInfo.tags);
6572
6573 if (targetTags.indexOf(tag) > -1)
6574 return true;
6575 else
6576 return false;
6577 }
6578 else if (tag instanceof RegExp) { // Check wildcard tags.
6579 targetTags = (isMetatag(tag.source) ? postInfo.metatags : postInfo.tags);
6580
6581 return tag.test(targetTags);
6582 }
6583 else if (typeof(tag) === "object") {
6584 if (tag instanceof Array) // Check grouped tags.
6585 return thumbSearchMatch(postInfo, tag);
6586 else { // Check numeric metatags.
6587 var tagsMetaValue = postInfo[tag.tagName];
6588
6589 if (tag.equals !== undefined) {
6590 if (tagsMetaValue !== tag.equals)
6591 return false;
6592 }
6593 else {
6594 if (tag.greater !== undefined && tagsMetaValue <= tag.greater)
6595 return false;
6596
6597 if (tag.less !== undefined && tagsMetaValue >= tag.less)
6598 return false;
6599 }
6600
6601 return true;
6602 }
6603 }
6604 }
6605
6606 function createSearch(search) {
6607 // Take search strings, turn them into search objects, and pass back the objects in an array.
6608 if (!/[^\s,]/.test(search))
6609 return [];
6610
6611 var groupsObject = replaceSearchGroups(search);
6612 var groups = groupsObject.groups;
6613 var searchStrings = groupsObject.search.toLowerCase().replace(/\b(rating:[qes])\w+/g, "$1").split(",");
6614 var searches = [];
6615
6616 // Sort through each matching rule.
6617 for (var i = 0, il = searchStrings.length; i < il; i++) {
6618 var searchString = searchStrings[i].split(" ");
6619 var searchObject = {
6620 all: {includes: [], excludes: [], total: 0},
6621 any: {includes: [], excludes: [], total: 0}
6622 };
6623
6624 // Divide the tags into any and all sets with excluded and included tags.
6625 for (var j = 0, jl = searchString.length; j < jl; j++) {
6626 var searchTerm = searchString[j];
6627 var primaryMode = "all";
6628 var secondaryMode = "includes";
6629
6630 while (searchTerm.charAt(0) === "~" || searchTerm.charAt(0) === "-") {
6631 switch (searchTerm.charAt(0)) {
6632 case "~":
6633 primaryMode = "any";
6634 break;
6635 case "-":
6636 secondaryMode = "excludes";
6637 break;
6638 }
6639
6640 searchTerm = searchTerm.slice(1);
6641 }
6642
6643 if (!searchTerm.length) // Stop if there is no actual tag.
6644 continue;
6645
6646 var mode = searchObject[primaryMode][secondaryMode];
6647
6648 if (isNumMetatag(searchTerm)) { // Parse numeric metatags and turn them into objects.
6649 var tagArray = searchTerm.split(":");
6650 var metaObject = {
6651 tagName: tagArray[0],
6652 equals: undefined,
6653 greater: undefined,
6654 less: undefined
6655 };
6656 var numSearch = tagArray[1];
6657 var numArray, equals, greater, less; // If/else variables.
6658
6659 if (numSearch.indexOf("<=") === 0 || numSearch.indexOf("..") === 0) { // Less than or equal to. (tag:<=# & tag:..#)
6660 less = parseInt(numSearch.slice(2), 10);
6661
6662 if (!isNaN(less)) {
6663 metaObject.less = less + 1;
6664 mode.push(metaObject);
6665 }
6666 }
6667 else if (numSearch.indexOf(">=") === 0) { // Greater than or equal to. (tag:>=#)
6668 greater = parseInt(numSearch.slice(2), 10);
6669
6670 if (!isNaN(greater)) {
6671 metaObject.greater = greater - 1;
6672 mode.push(metaObject);
6673 }
6674 }
6675 else if (numSearch.length > 2 && numSearch.indexOf("..") === numSearch.length - 2) { // Greater than or equal to. (tag:#..)
6676 greater = parseInt(numSearch.slice(0, -2), 10);
6677
6678 if (!isNaN(greater)) {
6679 metaObject.greater = greater - 1;
6680 mode.push(metaObject);
6681 }
6682 }
6683 else if (numSearch.charAt(0) === "<") { // Less than. (tag:<#)
6684 less = parseInt(numSearch.slice(1), 10);
6685
6686 if (!isNaN(less)) {
6687 metaObject.less = less;
6688 mode.push(metaObject);
6689 }
6690 }
6691 else if (numSearch.charAt(0) === ">") { // Greater than. (tag:>#)
6692 greater = parseInt(numSearch.slice(1), 10);
6693
6694 if (!isNaN(greater)) {
6695 metaObject.greater = greater;
6696 mode.push(metaObject);
6697 }
6698 }
6699 else if (numSearch.indexOf("..") > -1) { // Greater than or equal to and less than or equal to range. (tag:#..#)
6700 numArray = numSearch.split("..");
6701 greater = parseInt(numArray[0], 10);
6702 less = parseInt(numArray[1], 10);
6703
6704 if (!isNaN(greater) && !isNaN(less)) {
6705 metaObject.greater = greater - 1;
6706 metaObject.less = less + 1;
6707 mode.push(metaObject);
6708 }
6709 }
6710 else { // Exact number. (tag:#)
6711 equals = parseInt(numSearch, 10);
6712
6713 if (!isNaN(equals)) {
6714 metaObject.equals = equals;
6715 mode.push(metaObject);
6716 }
6717 }
6718 }
6719 else if (searchTerm.indexOf("*") > -1) // Prepare wildcard tags as regular expressions.
6720 mode.push(new RegExp(escapeRegEx(searchTerm).replace(/\*/g, "\S*").bbbSpacePad())); // Don't use "\\S*" here since escapeRegEx replaces * with \*. That escape carries over to the next replacement and makes us end up with "\\S*".
6721 else if (/%\d+%/.test(searchTerm)) { // Prepare grouped tags as a search object.
6722 var groupIndex = Number(searchTerm.match(/\d+/)[0]);
6723
6724 mode.push(createSearch(groups[groupIndex]));
6725 }
6726 else if (typeof(searchTerm) === "string") { // Add regular tags.
6727 if (isMetatag(searchTerm)) {
6728 var tagObject = searchTerm.split(/:(.+)/, 2);
6729 var tagName = tagObject[0];
6730 var tagValue = tagObject[1];
6731
6732 // Drop metatags with no value.
6733 if (!tagValue)
6734 continue;
6735
6736 if (tagValue === "any" && (tagName === "pool" || tagName === "parent" || tagName === "child"))
6737 mode.push(new RegExp((tagName + ":\\S*").bbbSpacePad()));
6738 else if (tagValue === "none" && (tagName === "pool" || tagName === "parent" || tagName === "child")) {
6739 secondaryMode = (secondaryMode === "includes" ? "excludes" : "includes"); // Flip the include/exclude mode.
6740 mode = searchObject[primaryMode][secondaryMode];
6741
6742 mode.push(new RegExp((tagName + ":\\S*").bbbSpacePad()));
6743 }
6744 else if (tagValue === "active" && tagName === "pool")
6745 mode.push(new RegExp((tagName + ":(collection|series)").bbbSpacePad()));
6746 else // Allow all other values through (ex: parent:# & pool:series).
6747 mode.push(searchTerm.bbbSpacePad());
6748 }
6749 else
6750 mode.push(searchTerm.bbbSpacePad());
6751 }
6752 }
6753
6754 searchObject.all.total = searchObject.all.includes.length + searchObject.all.excludes.length;
6755 searchObject.any.total = searchObject.any.includes.length + searchObject.any.excludes.length;
6756
6757 if (searchObject.all.total || searchObject.any.total)
6758 searches.push(searchObject);
6759 }
6760
6761 return searches;
6762 }
6763
6764 function replaceSearchGroups(search) {
6765 // Collect all the nested/grouped tags in a search and replace them with placeholders.
6766 if (search.indexOf("%") < 0)
6767 return {search: search, groups: []};
6768
6769 var searchString = search;
6770 var parens = searchString.match(/\(%|%\)/g);
6771
6772 // Remove unpaired opening parentheses near the end of the search.
6773 while (parens[parens.length - 1] === "(%") {
6774 searchString = searchString.replace(/^(.*\s)?[~-]*\(%/, "$1");
6775 parens.pop();
6776 }
6777
6778 // Take the remaining parentheses and figure out how to pair them up.
6779 var startCount = 0;
6780 var endCount = 0;
6781 var groupStartIndex = 0;
6782 var groups = [];
6783
6784 for (var i = 0, il = parens.length; i < il; i++) {
6785 var paren = parens[i];
6786 var nextParen = parens[i + 1];
6787
6788 if (paren === "(%")
6789 startCount++;
6790 else
6791 endCount++;
6792
6793 if (endCount > startCount) { // Remove unpaired closing parentheses near the start of the string.
6794 searchString = searchString.replace(/^(.*?)%\)/, "$1");
6795 endCount = 0;
6796 groupStartIndex++;
6797 }
6798 else if (startCount === endCount || (!nextParen && endCount > 0 && startCount > endCount)) { // Replace evenly paired parentheses with a placeholder.
6799 var groupRegex = new RegExp(parens.slice(groupStartIndex, i + 1).join(".*?").replace(/[\(\)]/g, "\\$&"));
6800 var groupMatch = searchString.match(groupRegex)[0];
6801
6802 searchString = searchString.replace(groupMatch, "%" + groups.length + "%");
6803 startCount = 0;
6804 endCount = 0;
6805 groupStartIndex = i + 1;
6806 groups.push(groupMatch.substring(2, groupMatch.length - 2));
6807 }
6808 else if (!nextParen && startCount > 0 && endCount === 0 ) // Remove leftover unpaired opening parentheses.
6809 searchString = searchString.replace(/^(.*\s)?[~-]*\(%/, "$1");
6810 }
6811
6812 return {search: searchString, groups: groups};
6813 }
6814
6815 function restoreSearchGroups(search, groups) {
6816 // Replace all group placeholders with their corresponding group.
6817 var restoredSearch = search;
6818
6819 for (var i = 0, il = groups.length; i < il; i++) {
6820 var groupPlaceholder = new RegExp("%" + i + "%");
6821
6822 restoredSearch = restoredSearch.replace(groupPlaceholder, "(%" + groups[i] + "%)");
6823 }
6824
6825 return restoredSearch;
6826 }
6827
6828 function cleanSearchGroups(string) {
6829 // Take a search string and clean up extra spaces, commas, and any parentheses that are missing their opening/closing parenthesis.
6830 var groupObject = replaceSearchGroups(string);
6831 var groups = groupObject.groups;
6832 var searchString = groupObject.search;
6833
6834 for (var i = 0, il = groups.length; i < il; i++)
6835 groups[i] = cleanSearchGroups(groups[i]);
6836
6837 searchString = restoreSearchGroups(searchString, groups).bbbTagClean();
6838
6839 return searchString;
6840 }
6841
6842 function searchSingleToMulti(string) {
6843 // Take a single line search and format it into multiple lines for a textarea.
6844 var groupsObject = replaceSearchGroups(cleanSearchGroups(string));
6845 var searchString = groupsObject.search;
6846 var groups = groupsObject.groups;
6847 var searchText = searchString.replace(/,\s*/g, "\r\n\r\n");
6848
6849 searchText = restoreSearchGroups(searchText, groups);
6850
6851 return searchText;
6852 }
6853
6854 function searchMultiToSingle(multi) {
6855 // Take a multiple line search from a textarea and format it into a single line.
6856 var searchStrings = multi.split(/[\r\n]+/g);
6857
6858 for (var i = 0, il = searchStrings.length; i < il; i++)
6859 searchStrings[i] = cleanSearchGroups(searchStrings[i]);
6860
6861 var searchString = searchStrings.join(", ");
6862
6863 return searchString;
6864 }
6865
6866 function trackNew() {
6867 // Set up the track new option and manage the search.
6868 var header = document.getElementById("top");
6869
6870 if (!track_new || !header)
6871 return;
6872
6873 var activeMenu = header.getElementsByClassName("current")[0];
6874 var secondMenu = header.getElementsByTagName("menu")[1];
6875
6876 // Insert new posts link.
6877 if (activeMenu && activeMenu.textContent === "Posts" && secondMenu) {
6878 var menuItems = secondMenu.getElementsByTagName("li");
6879 var numMenuItems = secondMenu.getElementsByTagName("li").length;
6880 var listingItemSibling = menuItems[1];
6881
6882 for (var i = 0; i < numMenuItems; i++) {
6883 var menuLink = menuItems[i];
6884 var nextLink = menuItems[i + 1];
6885
6886 if (menuLink.textContent.indexOf("Listing") > -1) {
6887 if (nextLink)
6888 listingItemSibling = nextLink;
6889 else
6890 listingItemSibling = undefined;
6891
6892 break;
6893 }
6894 }
6895
6896 var link = document.createElement("a");
6897 link.href = "/posts?new_posts=redirect&page=b1";
6898 link.innerHTML = "New";
6899 link.addEventListener("click", function(event) {
6900 if (event.button !== 0)
6901 return;
6902
6903 trackNewLoad();
6904 event.preventDefault();
6905 }, false);
6906
6907 var item = document.createElement("li");
6908 item.appendChild(link);
6909
6910 if (listingItemSibling)
6911 secondMenu.insertBefore(item, listingItemSibling);
6912 else
6913 secondMenu.appendChild(item);
6914 }
6915
6916 if (gLoc === "search") {
6917 var info = track_new_data;
6918 var mode = getVar("new_posts");
6919 var postsDiv = document.getElementById("posts");
6920 var postSections = document.getElementById("post-sections");
6921 var firstPost = getPosts()[0];
6922
6923 if (mode === "init" && !info.viewed && !getVar("tags") && !getVar("page")) { // Initialize.
6924 if (firstPost) {
6925 info.viewed = Number(firstPost.getAttribute("data-id"));
6926 info.viewing = 1;
6927 saveSettings();
6928 bbbNotice("New post tracking initialized. Tracking will start with new posts after the current last image.", 8);
6929 }
6930 }
6931 else if (mode === "redirect") { // Bookmarkable redirect link. (http://danbooru.donmai.us/posts?new_posts=redirect&page=b1)
6932 if (postsDiv)
6933 postsDiv.innerHTML = "<b>Redirecting...</b>";
6934
6935 trackNewLoad();
6936 }
6937 else if (mode === "list") {
6938 var limitNum = getLimit() || thumbnail_count || thumbnail_count_default;
6939 var currentPage = Number(getVar("page")) || 1;
6940 var savedPage = Math.ceil((info.viewing - limitNum) / limitNum) + 1;
6941 var currentViewed = Number(/id:>(\d+)/.exec(decodeURIComponent(location.search))[1]);
6942 var paginator = getPaginator();
6943
6944 // Replace the chickens message on the first page with a more specific message.
6945 if (!firstPost && currentPage < 2) {
6946 if (postsDiv && postsDiv.firstElementChild)
6947 postsDiv.firstElementChild.innerHTML = "No new posts.";
6948 }
6949
6950 // Update the saved page information.
6951 if (savedPage !== currentPage && info.viewed === currentViewed) {
6952 info.viewing = (currentPage - 1) * limitNum + 1;
6953 saveSettings();
6954 }
6955
6956 // Modify new post searches with a mark as viewed link.
6957 if (postSections) {
6958 var markSection = document.createElement("li");
6959
6960 var markLink = document.createElement("a");
6961 markLink.innerHTML = (currentPage > 1 ? "Mark pages 1-" + currentPage + " viewed" : "Mark page 1 viewed");
6962 markLink.href = "#";
6963 markSection.appendChild(markLink);
6964 postSections.appendChild(markSection);
6965
6966 markLink.addEventListener("click", function(event) {
6967 if (event.button !== 0)
6968 return;
6969
6970 trackNewMark();
6971 event.preventDefault();
6972 }, false);
6973
6974 var resetSection = document.createElement("li");
6975 resetSection.style.cssFloat = "right";
6976
6977 var resetLink = document.createElement("a");
6978 resetLink.innerHTML = "Reset (Mark all viewed)";
6979 resetLink.href = "#";
6980 resetLink.style.color = "#FF1100";
6981 resetSection.appendChild(resetLink);
6982 postSections.appendChild(resetSection);
6983
6984 resetLink.addEventListener("click", function(event) {
6985 if (event.button !== 0)
6986 return;
6987
6988 trackNewReset();
6989 event.preventDefault();
6990 }, false);
6991
6992 // Update the mark link if the paginator updates.
6993 if (paginator) {
6994 paginator.bbbWatchNodes(function() {
6995 var activePage = paginator.getElementsByTagName("span")[0];
6996
6997 if (activePage)
6998 markLink.innerHTML = "Mark pages 1-" + activePage.textContent.bbbSpaceClean() + " viewed";
6999 });
7000 }
7001 }
7002 }
7003 }
7004 }
7005
7006 function trackNewLoad() {
7007 // Create the search URL and load it.
7008 var info = bbb.user.track_new_data;
7009 var limitNum = bbb.user.thumbnail_count || thumbnail_count_default;
7010 var savedPage = Math.ceil((info.viewing - limitNum) / limitNum) + 1;
7011
7012 if (info.viewed)
7013 location.href = "/posts?new_posts=list&tags=order:id_asc+id:>" + info.viewed + "&page=" + savedPage + "&limit=" + limitNum;
7014 else
7015 location.href = "/posts?new_posts=init&limit=" + limitNum;
7016 }
7017
7018 function trackNewReset() {
7019 // Reinitialize settings/Mark all viewed.
7020 loadSettings();
7021
7022 var limitNum = bbb.user.thumbnail_count || thumbnail_count_default;
7023
7024 bbb.user.track_new_data = bbb.options.track_new_data.def;
7025 saveSettings();
7026
7027 bbbNotice("Reinitializing new post tracking. Please wait.", 0);
7028 location.href = "/posts?new_posts=init&limit=" + limitNum;
7029 }
7030
7031 function trackNewMark() {
7032 // Mark the current images and older as viewed.
7033 loadSettings();
7034
7035 var info = bbb.user.track_new_data;
7036 var limitNum = getLimit() || bbb.user.thumbnail_count || thumbnail_count_default;
7037 var posts = getPosts();
7038 var lastPost = posts[posts.length - 1];
7039 var lastId = (lastPost ? Number(lastPost.getAttribute("data-id")) : null );
7040
7041 if (!lastPost)
7042 bbbNotice("Unable to mark as viewed. No posts detected.", -1);
7043 else if (info.viewed >= lastId)
7044 bbbNotice("Unable to mark as viewed. Posts have already been marked.", -1);
7045 else {
7046 info.viewed = Number(lastPost.getAttribute("data-id"));
7047 info.viewing = 1;
7048 saveSettings();
7049
7050 bbbNotice("Posts marked as viewed. Please wait while the pages are updated.", 0);
7051 location.href = "/posts?new_posts=list&tags=order:id_asc+id:>" + info.viewed + "&page=1&limit=" + limitNum;
7052 }
7053 }
7054
7055 function customCSS() {
7056 var i; // Loop variable.
7057 var customStyles = document.createElement("style");
7058 customStyles.type = "text/css";
7059
7060 var styles = '#bbb-menu {background-color: #FFFFFF; border: 1px solid #CCCCCC; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5); padding: 15px; position: fixed; top: 25px; left: 50%; z-index: 9001;}' +
7061 '#bbb-menu *, #bbb-dialog-window * {font-size: 14px; line-height: 16px; outline: 0px none; border: 0px none; margin: 0px; padding: 0px;}' + // Reset some base settings.
7062 '#bbb-menu h1, #bbb-dialog-window h1 {font-size: 24px; line-height: 42px;}' +
7063 '#bbb-menu h2, #bbb-dialog-window h2 {font-size: 16px; line-height: 25px;}' +
7064 '#bbb-menu input, #bbb-menu select, #bbb-menu textarea, #bbb-dialog-window input, #bbb-dialog-window select, #bbb-dialog-window textarea {border: #CCCCCC 1px solid;}' +
7065 '#bbb-menu input {height: 17px; padding: 1px 0px; margin-top: 4px; vertical-align: top;}' +
7066 '#bbb-menu input[type="checkbox"] {margin: 0px; vertical-align: middle; position: relative; bottom: 2px;}' +
7067 '#bbb-menu .bbb-general-input input[type="text"], #bbb-menu .bbb-general-input select {width: 175px;}' +
7068 '#bbb-menu select {height: 21px; margin-top: 4px; vertical-align: top;}' +
7069 '#bbb-menu option {padding: 0px 3px;}' +
7070 '#bbb-menu textarea, #bbb-dialog-window textarea {padding: 2px; resize: none;}' +
7071 '#bbb-menu ul, #bbb-menu ol, #bbb-dialog-window ul, #bbb-dialog-window ol {list-style: outside disc none; margin-top: 0px; margin-bottom: 0px; margin-left: 20px; display: block;}' +
7072 '#bbb-menu .bbb-scroll-div {border: 1px solid #CCCCCC; margin: -1px 0px 5px 0px; padding: 5px 0px; overflow-y: auto;}' +
7073 '#bbb-menu .bbb-page {position: relative; display: none;}' +
7074 '#bbb-menu .bbb-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 5px;}' +
7075 '#bbb-menu .bbb-tab {border-top-left-radius: 5px; border-top-right-radius: 5px; display: inline-block; padding: 5px; border: 1px solid #CCCCCC; margin-right: -1px;}' +
7076 '#bbb-menu .bbb-active-tab {background-color: #FFFFFF; border-bottom-width: 0px; padding-bottom: 6px;}' +
7077 '#bbb-menu .bbb-header {border-bottom: 2px solid #CCCCCC; margin-bottom: 5px; width: 700px;}' +
7078 '#bbb-menu .bbb-toc {list-style-type: upper-roman; margin-left: 30px;}' +
7079 '#bbb-menu .bbb-section-options, #bbb-menu .bbb-section-text {margin-bottom: 5px; max-width: 902px;}' +
7080 '#bbb-menu .bbb-section-options-left, #bbb-menu .bbb-section-options-right {display: inline-block; vertical-align: top; width: 435px;}' +
7081 '#bbb-menu .bbb-section-options-left {border-right: 1px solid #CCCCCC; margin-right: 15px; padding-right: 15px;}' +
7082 '#bbb-menu .bbb-general-label {display: block; height: 29px; padding: 0px 5px;}' +
7083 '#bbb-menu .bbb-general-label:hover {background-color: #EEEEEE;}' +
7084 '#bbb-menu .bbb-general-text {line-height: 29px;}' +
7085 '#bbb-menu .bbb-general-input {float: right; line-height: 29px;}' +
7086 '#bbb-menu .bbb-expl-link {font-size: 12px; font-weight: bold; margin-left: 5px; padding: 2px;}' +
7087 '#bbb-menu .bbb-border-div {background-color: #EEEEEE; padding: 2px; margin: 0px 5px 0px 0px;}' +
7088 '#bbb-menu .bbb-border-bar, #bbb-menu .bbb-border-settings {height: 29px; padding: 0px 2px; overflow: hidden;}' +
7089 '#bbb-menu .bbb-border-settings {background-color: #FFFFFF;}' +
7090 '#bbb-menu .bbb-border-div label, #bbb-menu .bbb-border-div span {display: inline-block; line-height: 29px;}' +
7091 '#bbb-menu .bbb-border-name {text-align: left; width: 540px;}' +
7092 '#bbb-menu .bbb-border-name input {width:460px;}' +
7093 '#bbb-menu .bbb-border-color {text-align: center; width: 210px;}' +
7094 '#bbb-menu .bbb-border-color input {width: 148px;}' +
7095 '#bbb-menu .bbb-border-style {float: right; text-align: right; width: 130px;}' +
7096 '#bbb-menu .bbb-border-divider {height: 4px;}' +
7097 '#bbb-menu .bbb-insert-highlight .bbb-border-divider {background-color: blue; cursor: pointer;}' +
7098 '#bbb-menu .bbb-no-highlight .bbb-border-divider {background-color: transparent; cursor: auto;}' +
7099 '#bbb-menu .bbb-border-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 2px; margin: 0px 2px;}' +
7100 '#bbb-menu .bbb-border-spacer {display: inline-block; height: 12px; width: 0px; border-right: 1px solid #CCCCCC; margin: 0px 5px;}' +
7101 '#bbb-menu .bbb-backup-area {height: 300px; width: 896px; margin-top: 2px;}' +
7102 '#bbb-menu .bbb-blacklist-area {height: 300px; width: 896px; margin-top: 2px;}' +
7103 '#bbb-menu .bbb-edit-link {background-color: #FFFFFF; border: 1px solid #CCCCCC; display: inline-block; height: 19px; line-height: 19px; margin-left: -1px; padding: 0px 2px; margin-top: 4px; text-align: center; vertical-align: top;}' +
7104 '#bbb-expl {background-color: #CCCCCC; border: 1px solid #000000; display: none; font-size: 12px; padding: 5px; position: fixed; max-width: 488px; width: 488px; overflow: hidden; z-index: 9002; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5);}' +
7105 '#bbb-expl * {font-size: 12px;}' +
7106 '#bbb-expl tiphead {display: block; font-weight: bold; text-decoration: underline; font-size: 13px; margin-top: 12px;}' +
7107 '#bbb-expl tipdesc {display: inline; font-weight: bold;}' +
7108 '#bbb-expl tipdesc:before {content: "\\A0"; display: block; height: 12px; clear: both;}' + // Simulate a double line break.
7109 '#bbb-status {background-color: rgba(255, 255, 255, 0.75); border: 1px solid rgba(204, 204, 204, 0.75); font-size: 12px; font-weight: bold; text-align: right; display: none; padding: 3px; position: fixed; bottom: 0px; right: 0px; z-index: 9002;}' +
7110 '#bbb-notice-container {position: fixed; top: 0.5em; left: 25%; width: 50%; z-index: 9002;}' +
7111 '#bbb-notice {padding: 3px; width: 100%; display: none; position: relative; border-radius: 2px; border: 1px solid #000000; background-color: #CCCCCC;}' +
7112 '#bbb-notice-msg {margin: 0px 25px 0px 55px; max-height: 200px; overflow: auto;}' +
7113 '#bbb-notice-msg .bbb-notice-msg-entry {border-bottom: solid 1px #000000; margin-bottom: 5px; padding-bottom: 5px;}' +
7114 '#bbb-notice-msg .bbb-notice-msg-entry:last-child {border-bottom: none 0px; margin-bottom: 0px; padding-bottom: 0px;}' +
7115 '#bbb-dialog-blocker {display: block; position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.33); z-index: 9003; text-align: center;}' +
7116 '#bbb-dialog-blocker:before {content: ""; display: inline-block; height: 100%; vertical-align: middle;}' + // Helps vertically center an element with unknown dimensions: https://css-tricks.com/centering-in-the-unknown/
7117 '#bbb-dialog-window {display: inline-block; display: inline-flex; flex-flow: column; position: relative; background-color: #FFFFFF; border: 10px solid #FFFFFF; outline: 1px solid #CCC; box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.5); color: #000000; max-width: 940px; max-height: 90%; overflow-x: hidden; overflow-y: auto; text-align: left; vertical-align: middle; line-height: initial;}' +
7118 '#bbb-dialog-window .bbb-header {border-bottom: 2px solid #CCCCCC; margin-bottom: 5px; margin-right: 100px; padding-right: 50px; white-space: nowrap;}' +
7119 '#bbb-dialog-window .bbb-dialog-button {border: 1px solid #CCCCCC; border-radius: 5px; display: inline-block; padding: 5px; margin: 0px 5px;}' +
7120 '#bbb-dialog-window .bbb-dialog-content-div {padding: 5px; overflow-x: hidden; overflow-y: auto;}' +
7121 '#bbb-dialog-window .bbb-dialog-button-div {padding-top: 10px; flex-grow: 0; flex-shrink: 0; overflow: hidden;}' +
7122 '#bbb-dialog-window .bbb-edit-area {height: 300px; width: 800px;}';
7123
7124 // Provide a little extra space for listings that allow thumbnail_count.
7125 if (thumbnail_count && (gLoc === "search" || gLoc === "notes" || gLoc === "favorites")) {
7126 styles += 'div#page {margin: 0px 10px 0px 20px !important;}' +
7127 'section#content {padding: 0px !important;}';
7128 }
7129
7130 // Calculate some dimensions.
7131 var totalBorderWidth = (custom_tag_borders ? border_width * 2 + (border_spacing * 2 || 1) : border_width + border_spacing);
7132 var thumbMaxWidth = 150 + totalBorderWidth * 2;
7133 var thumbMaxHeight = thumbMaxWidth;
7134 var listingExtraSpace = (14 - totalBorderWidth * 2 > 2 ? 14 - totalBorderWidth * 2 : 2);
7135 var commentExtraSpace = 34 - totalBorderWidth * 2;
7136 var customBorderSpacing = (border_spacing || 1);
7137
7138 if (thumb_info === "below")
7139 thumbMaxHeight += 18; // Add some extra height for the info.
7140
7141 // Border setup.
7142 var sbsl = status_borders.length;
7143 var statusBorderItem; // Loop variable.
7144
7145 styles += 'article.post-preview a.bbb-thumb-link, .post-preview div.preview a.bbb-thumb-link {display: inline-block !important;}' +
7146 'article.post-preview {height: ' + thumbMaxHeight + 'px !important; width: ' + thumbMaxWidth + 'px !important; margin: 0px ' + listingExtraSpace + 'px ' + listingExtraSpace + 'px 0px !important;}' +
7147 'article.post-preview.pooled {height: ' + (thumbMaxHeight + 60) + 'px !important;}' + // Pool gallery view thumb height adjustment.
7148 '#has-parent-relationship-preview article.post-preview, #has-children-relationship-preview article.post-preview {padding: 5px 5px 10px !important; width: auto !important; max-width: ' + thumbMaxWidth + 'px !important; margin: 0px !important;}' +
7149 'article.post-preview a.bbb-thumb-link {line-height: 0px !important;}' +
7150 '.post-preview div.preview {height: ' + thumbMaxHeight + 'px !important; width: ' + thumbMaxWidth + 'px !important; margin-right: ' + commentExtraSpace + 'px !important;}' +
7151 '.post-preview div.preview a.bbb-thumb-link {line-height: 0px !important;}' +
7152 '.post-preview a.bbb-thumb-link img {border-width: ' + border_width + 'px !important; padding: ' + border_spacing + 'px !important;}' +
7153 'a.bbb-thumb-link.bbb-custom-tag {border-width: ' + border_width + 'px !important;}';
7154
7155 if (custom_status_borders) {
7156 var activeStatusStyles = "";
7157 var statusBorderInfo = {};
7158
7159 for (i = 0; i < sbsl; i++) {
7160 statusBorderItem = status_borders[i];
7161 statusBorderInfo[statusBorderItem.tags] = statusBorderItem;
7162 }
7163
7164 for (i = 0; i < sbsl; i++) {
7165 statusBorderItem = status_borders[i];
7166
7167 if (single_color_borders) {
7168 if (statusBorderItem.is_enabled)
7169 activeStatusStyles = '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}' + activeStatusStyles;
7170 else
7171 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: transparent !important;}'; // Disable status border by resetting it to transparent.
7172 }
7173 else {
7174 if (statusBorderItem.is_enabled) {
7175 if (statusBorderItem.tags === "parent") {
7176 styles += '.post-preview.post-status-has-children a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}'; // Parent only status border.
7177
7178 if (statusBorderInfo.child.is_enabled)
7179 styles += '.post-preview.post-status-has-children.post-status-has-parent a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' ' + statusBorderInfo.child.border_color + ' ' + statusBorderInfo.child.border_color + ' ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' ' + statusBorderInfo.child.border_style + ' ' + statusBorderInfo.child.border_style + ' ' + statusBorderItem.border_style + ' !important;}'; // Parent and child status border.
7180 }
7181 else if (statusBorderItem.tags === "child")
7182 styles += '.post-preview.post-status-has-parent a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}'; // Child only status border.
7183 else {
7184 activeStatusStyles = '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}' + activeStatusStyles; // Deleted/pending/flagged only status border.
7185
7186 if (statusBorderInfo.parent.is_enabled)
7187 activeStatusStyles = '.post-preview.post-status-has-children.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderInfo.parent.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderInfo.parent.border_color + ' !important; border-style: ' + statusBorderInfo.parent.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderInfo.parent.border_style + ' !important;}' + activeStatusStyles; // Deleted/pending/flagged and parent status border.
7188
7189 if (statusBorderInfo.child.is_enabled)
7190 activeStatusStyles = '.post-preview.post-status-has-parent.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderInfo.child.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderInfo.child.border_color + ' !important; border-style: ' + statusBorderInfo.child.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderInfo.child.border_style + ' !important;}' + activeStatusStyles; // Deleted/pending/flagged and child status border.
7191
7192 if (statusBorderInfo.child.is_enabled && statusBorderInfo.parent.is_enabled)
7193 activeStatusStyles = '.post-preview.post-status-has-children.post-status-has-parent.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderInfo.parent.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderItem.border_color + ' ' + statusBorderInfo.child.border_color + ' !important; border-style: ' + statusBorderInfo.parent.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderItem.border_style + ' ' + statusBorderInfo.child.border_style + ' !important;}' + activeStatusStyles; // Deleted/pending/flagged, parent, and child status border.
7194 }
7195 }
7196 else
7197 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: transparent !important;}'; // Disable status border by resetting it to transparent.
7198 }
7199 }
7200
7201 styles += activeStatusStyles;
7202 }
7203 else if (single_color_borders) { // Allow single color borders when not using custom status borders. Works off of the old border hierarchy: Deleted > Flagged > Pending > Child > Parent
7204 var defaultStatusBorders = bbb.options.status_borders;
7205
7206 for (i = defaultStatusBorders.length - 1; i >= 0; i--) {
7207 statusBorderItem = defaultStatusBorders[i];
7208
7209 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link img {border-color: ' + statusBorderItem.border_color + ' !important; border-style: ' + statusBorderItem.border_style + ' !important;}';
7210 }
7211 }
7212
7213 if (custom_tag_borders) {
7214 styles += '.post-preview a.bbb-thumb-link.bbb-custom-tag img {border-width: 0px !important;}' + // Remove the transparent border for images that get custom tag borders.
7215 'article.post-preview a.bbb-thumb-link, .post-preview div.preview a.bbb-thumb-link {margin-top: ' + (border_width + customBorderSpacing) + 'px !important;}'; // Align one border images with two border images.
7216
7217 for (i = 0; i < sbsl; i++) {
7218 statusBorderItem = status_borders[i];
7219
7220 if (statusBorderItem.is_enabled)
7221 styles += '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link.bbb-custom-tag {margin: 0px !important; padding: ' + customBorderSpacing + 'px !important;}' + // Remove margin alignment and add border padding for images that have status and custom tag borders.
7222 '.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link.bbb-custom-tag img {border-width: ' + border_width + 'px !important;}'; // Override the removal of the transparent border for images that have status borders and custom tag borders.
7223 }
7224 }
7225
7226 // Overlay setup.
7227 styles += 'article.post-preview:before, div.post.post-preview div.preview:before {content: none !important;}' + // Disable original Danbooru animated overlay.
7228 'article.post-preview[data-tags~="animated"] a.bbb-thumb-link:before, article.post-preview[data-file-ext="swf"] a.bbb-thumb-link:before, article.post-preview[data-file-ext="webm"] a.bbb-thumb-link:before, article.post-preview[data-file-ext="mp4"] a.bbb-thumb-link:before, div.post.post-preview[data-tags~="animated"] div.preview a.bbb-thumb-link:before, div.post.post-preview[data-file-ext="swf"] div.preview a.bbb-thumb-link:before, div.post.post-preview[data-file-ext="webm"] div.preview a.bbb-thumb-link:before, div.post.post-preview[data-file-ext="mp4"] div.preview a.bbb-thumb-link:before {content: "\\25BA"; position: absolute; width: 20px; height: 20px; color: #FFFFFF; background-color: rgba(0, 0, 0, 0.5); line-height: 20px; top: 0px; left: 0px;}' + // Recreate Danbooru animated overlay.
7229 'article.post-preview[data-has-sound="true"] a.bbb-thumb-link:before, div.post.post-preview[data-has-sound="true"] div.preview a.bbb-thumb-link:before {content: "\\266A"; position: absolute; width: 20px; height: 20px; color: #FFFFFF; background-color: rgba(0, 0, 0, 0.5); line-height: 20px; top: 0px; left: 0px;}' + // Recreate Danbooru audio overlay.
7230 'article.post-preview.blacklisted a.bbb-thumb-link:after, article.post-preview a.bbb-thumb-link:before, div.post.post-preview.blacklisted div.preview a.bbb-thumb-link:after, div.post.post-preview div.preview a.bbb-thumb-link:before {margin: ' + (border_width + border_spacing) + 'px;}' + // Margin applies to posts with no borders or only a status border.
7231 'article.post-preview.blacklisted a.bbb-thumb-link.bbb-custom-tag:after, article.post-preview a.bbb-thumb-link.bbb-custom-tag:before, div.post.post-preview.blacklisted div.preview a.bbb-thumb-link.bbb-custom-tag:after, div.post.post-preview div.preview a.bbb-thumb-link.bbb-custom-tag:before {margin: ' + border_spacing + 'px;}' + // Margin applies to posts with only a custom border.
7232 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link:after, article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link:before, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link:after, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link:before {content: none;}' + // Don't display when actively blacklisted.
7233 'article.post-preview a.bbb-thumb-link, div.post.post-preview div.preview a.bbb-thumb-link {position: relative;}'; // Allow the overlays to position relative to the link.
7234
7235 for (i = 0; i < sbsl; i++) {
7236 statusBorderItem = status_borders[i];
7237
7238 if (statusBorderItem.is_enabled)
7239 styles += 'article.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link.bbb-custom-tag:after, article.post-preview.' + statusBorderItem.class_name + ' a.bbb-thumb-link.bbb-custom-tag:before, div.post.post-preview.' + statusBorderItem.class_name + ' div.preview a.bbb-thumb-link.bbb-custom-tag:after, div.post.post-preview.' + statusBorderItem.class_name + ' div.preview a.bbb-thumb-link.bbb-custom-tag:before {margin: ' + (border_width + border_spacing + customBorderSpacing) + 'px !important}'; // Margin applies to posts with a status and custom border.
7240 }
7241
7242 // Thumbnail info.
7243 var thumbInfoStyle = "height: 18px; font-size: 14px; line-height: 18px; text-align: center;";
7244
7245 if (thumb_info !== "disabled")
7246 styles += '.bbb-thumb-info-parent.blacklisted.blacklisted-active .bbb-thumb-info {display: none;}';
7247
7248 if (thumb_info === "below")
7249 styles += '.bbb-thumb-info-parent .bbb-thumb-info {display: block;' + thumbInfoStyle + '}';
7250 else if (thumb_info === "hover") {
7251 styles += '.bbb-thumb-info-parent .bbb-thumb-info {display: none; position: relative; bottom: 18px; background-color: rgba(255, 255, 255, 0.9);' + thumbInfoStyle + '}' +
7252 '.bbb-thumb-info-parent:hover .bbb-thumb-info {display: block;}' +
7253 '#has-children-relationship-preview article.post-preview.bbb-thumb-info-parent, #has-parent-relationship-preview article.post-preview.bbb-thumb-info-parent {min-width: 130px !important;}' + // Give parent/child notice thumbs a minimum width to prevent element shifting upon hover.
7254 '.bbb-thumb-info-parent:hover .bbb-thumb-info.bbb-thumb-info-short {bottom: 0px;}'; // Short thumbnails get no overlapping.
7255 }
7256
7257 // Endless
7258 if (endless_default !== "disabled") {
7259 styles += 'div.paginator {padding: 3em 0px 0px;}' +
7260 '#bbb-endless-button-div {width: 100%; height: 0px; overflow: visible; clear: both; text-align: center;}' +
7261 '#bbb-endless-load-div, #bbb-endless-enable-div {display: none; position: absolute;}' +
7262 '#bbb-endless-load-button, #bbb-endless-enable-button {position: relative; left: -50%; border: 1px solid #EAEAEA; border-radius: 5px; display: inline-block; padding: 5px; margin-top: 3px;}';
7263
7264 if (endless_separator === "divider") {
7265 styles += '.bbb-endless-page {display: block; clear: both;}' +
7266 '.bbb-endless-divider {display: block; border: 1px solid #CCCCCC; height: 0px; margin: 15px 0px; width: 100%; float: left;}' +
7267 '.bbb-endless-divider-link {position: relative; top: -16px; display: inline-block; height: 32px; margin-left: 5%; padding: 0px 5px; font-size: 14px; font-weight: bold; line-height: 32px; background-color: #FFFFFF; color: #CCCCCC;}';
7268 }
7269 else if (endless_separator === "marker") {
7270 styles += '.bbb-endless-page {display: inline;}' +
7271 'article.bbb-endless-marker-article {height: ' + thumbMaxHeight + 'px !important; width: ' + thumbMaxWidth + 'px !important; margin: 0px ' + listingExtraSpace + 'px ' + listingExtraSpace + 'px 0px !important; float: left; overflow: hidden; text-align: center; vertical-align: baseline; position: relative;}' +
7272 '.bbb-endless-marker {display: inline-block; border: 1px solid #CCCCCC; height: 148px; width: 148px; line-height: 148px; text-align: center; margin-top: ' + totalBorderWidth + 'px;}' +
7273 '.bbb-endless-marker-link {display: inline-block; font-size: 14px; font-weight: bold; line-height: 14px; vertical-align: middle; color: #CCCCCC;}';
7274 }
7275 else if (endless_separator === "none")
7276 styles += '.bbb-endless-page {display: inline;}';
7277 }
7278
7279 // Hide sidebar.
7280 if (autohide_sidebar) {
7281 styles += 'div#page {margin: 0px 10px 0px 20px !important;}' +
7282 'aside#sidebar {background-color: transparent !important; border-width: 0px !important; height: 100% !important; width: 250px !important; position: fixed !important; left: -285px !important; opacity: 0 !important; overflow: hidden !important; padding: 0px 25px !important; top: 0px !important; z-index: 2001 !important;}' +
7283 'aside#sidebar.bbb-sidebar-show, aside#sidebar:hover {background-color: #FFFFFF !important; border-right: 1px solid #CCCCCC !important; left: 0px !important; opacity: 1 !important; overflow-y: auto !important; padding: 0px 15px !important;}' +
7284 'section#content {margin-left: 0px !important;}' +
7285 '.ui-autocomplete {z-index: 2002 !important;}';
7286 }
7287
7288 // Collapse sidebar sections.
7289 if (collapse_sidebar) {
7290 styles += '#sidebar ul.bbb-collapsed-sidebar, #sidebar form.bbb-collapsed-sidebar {display: block !important; height: 0px !important; margin: 0px !important; padding: 0px !important; overflow: hidden !important;}' + // Hide the element without changing the display to "none" since that interferes with some of Danbooru's JS.
7291 '#sidebar h1, #sidebar h2 {display: inline-block !important;}'; // Inline-block is possible here due to not using display in the previous rule.
7292 }
7293
7294 // Additional blacklist bars.
7295 if (blacklist_add_bars) {
7296 styles += '#blacklist-box.bbb-blacklist-box {margin-bottom: 1em;}' +
7297 '#blacklist-box.bbb-blacklist-box ul {display: inline;}' +
7298 '#blacklist-box.bbb-blacklist-box li {display: inline; margin-right: 1em;}' +
7299 '#blacklist-box.bbb-blacklist-box li a, #blacklist-box.bbb-blacklist-box li span.link {color: #0073FF; cursor: pointer;}' +
7300 '#blacklist-box.bbb-blacklist-box li span {color: #AAAAAA;}';
7301 }
7302
7303 // Blacklist thumbnail display.
7304 if (blacklist_post_display === "removed") {
7305 styles += 'article.post-preview.blacklisted {display: inline-block !important;}' +
7306 'article.post-preview.blacklisted.blacklisted-active {display: none !important;}' +
7307 'div.post.post-preview.blacklisted {display: block !important;}' + // Comment listing override.
7308 'div.post.post-preview.blacklisted.blacklisted-active {display: none !important;}';
7309 }
7310 else if (blacklist_post_display === "hidden") {
7311 styles += 'article.post-preview.blacklisted.blacklisted-active {display: inline-block !important;}' +
7312 'div.post.post-preview.blacklisted {display: block !important;}' + // Comments.
7313 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link {visibility: hidden !important;}';
7314 }
7315 else if (blacklist_post_display === "replaced") {
7316 styles += 'article.post-preview.blacklisted.blacklisted-active, div.post.post-preview.blacklisted.blacklisted-active {display: inline-block !important; background-position: ' + totalBorderWidth + 'px ' + totalBorderWidth + 'px !important; background-repeat: no-repeat !important; background-image: url(' + bbbBlacklistImg + ') !important;}' +
7317 '#has-parent-relationship-preview article.post-preview.blacklisted.blacklisted-active, #has-children-relationship-preview article.post-preview.blacklisted.blacklisted-active {background-position: ' + (totalBorderWidth + 5) + 'px ' + (totalBorderWidth + 5) + 'px !important;}' + // Account for relation notice padding.
7318 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link img, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link img {opacity: 0.0 !important; height: 150px !important; width: 150px !important; border-width: 0px !important; padding: 0px !important;}' + // Remove all status border space.
7319 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link {padding: 0px !important; margin: ' + totalBorderWidth + 'px !important; margin-bottom: 0px !important;}' + // Align no border thumbs with custom/single border thumbs.
7320 'article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link.bbb-custom-tag, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link.bbb-custom-tag {padding: ' + border_spacing + 'px !important; margin: ' + (border_width + customBorderSpacing) + 'px !important; margin-bottom: 0px !important;}' +
7321 'div.post.post-preview.blacklisted {display: block !important;}' +
7322 'div.post.post-preview.blacklisted.blacklisted-active {display: block !important;}';
7323 }
7324
7325 // Blacklist marking.
7326 if (blacklist_thumb_mark === "icon") {
7327 styles += 'article.post-preview.blacklisted a.bbb-thumb-link:after, div.post.post-preview.blacklisted div.preview a.bbb-thumb-link:after {content: "\\A0"; position: absolute; bottom: 0px; right: 0px; height: 20px; width: 20px; line-height: 20px; font-weight: bold; color: #FFFFFF; background: rgba(0, 0, 0, 0.5) url(\'' + bbbBlacklistIcon + '\');}'; // Create blacklist overlay.
7328 }
7329 else if (blacklist_thumb_mark === "highlight") {
7330 styles += 'article.post-preview.blacklisted, div.post.post-preview.blacklisted div.preview {background-color: ' + blacklist_highlight_color + ' !important;}' +
7331 'article.post-preview.blacklisted.blacklisted-active, div.post.post-preview.blacklisted.blacklisted-active div.preview {background-color: transparent !important;}' +
7332 'article.post-preview.blacklisted.blacklisted-active.current-post {background-color: rgba(0, 0, 0, 0.1) !important}';
7333 }
7334
7335 // Blacklist post controls.
7336 if (blacklist_thumb_controls) {
7337 styles += '#bbb-blacklist-tip {background-color: #FFFFFF; border: 1px solid #000000; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.5); display: none; font-size: 12px; line-height: 14px; padding: 5px; position: absolute; max-width: 420px; width: 420px; overflow: hidden; z-index: 9002;}' +
7338 '#bbb-blacklist-tip * {font-size: 12px; line-height: 14px;}' +
7339 '#bbb-blacklist-tip .blacklisted-active {text-decoration: line-through; font-weight: normal;}' +
7340 '#bbb-blacklist-tip ul {list-style: outside disc none; margin-top: 0px; margin-bottom: 0px; margin-left: 15px;}' +
7341 'article.post-preview.blacklisted.blacklisted-active, div.post.post-preview.blacklisted.blacklisted-active div.preview, article.post-preview.blacklisted.blacklisted-active a.bbb-thumb-link, div.post.post-preview.blacklisted.blacklisted-active div.preview a.bbb-thumb-link {cursor: help !important;}' +
7342 'article.post-preview.blacklisted.blacklisted-active a, div.post.post-preview.blacklisted.blacklisted-active div.preview a {cursor: pointer !important;}' +
7343 'article.post-preview.blacklisted, div.post.post-preview.blacklisted div.preview {position: relative !important;}' +
7344 'article.post-preview.blacklisted:hover .bbb-close-circle, div.post.post-preview.blacklisted:hover div.preview .bbb-close-circle {display: block; position: absolute; top: 0px; right: 0px; z-index: 9002 ; cursor: pointer; background-image: url(\'/images/ui-icons_222222_256x240.png\'); background-repeat: no-repeat; background-color: #FFFFFF; background-position: -32px -192px; width: 16px; height: 16px; border-radius: 8px; overflow: hidden;}' +
7345 'article.post-preview.blacklisted.blacklisted-active:hover .bbb-close-circle, div.post.post-preview.blacklisted.blacklisted-active:hover div.preview .bbb-close-circle {display: none;}' +
7346 'article.post-preview.blacklisted .bbb-close-circle, div.post.post-preview.blacklisted div.preview .bbb-close-circle {display: none;}';
7347 }
7348
7349 // Move save search to the sidebar.
7350 if (move_save_search) {
7351 styles += '.bbb-saved-search-item #saved-searches-nav, .bbb-saved-search-item #saved-searches-nav * {background-color: transparent; color: #0073FF; display: inline; font-family: Verdana,Helvetica,sans-serif; line-height: 1.25em; padding: 0px; margin: 0px; border: none;}' +
7352 '.bbb-saved-search-item #saved-searches-nav input:hover, .bbb-saved-search-item #saved-searches-nav button:hover {color: #80b9ff;}' +
7353 '.bbb-saved-search-item #saved-searches-nav input:focus, .bbb-saved-search-item #saved-searches-nav button:focus {outline: thin dotted;}';
7354 }
7355
7356 // Quick search styles.
7357 if (quick_search !== "disabled") {
7358 styles += '#bbb-quick-search {position: fixed; top: 0px; right: 0px; z-index: 2001; overflow: auto; padding: 2px; background-color: #FFFFFF; border-bottom: 1px solid #CCCCCC; border-left: 1px solid #CCCCCC; border-bottom-left-radius: 10px;}' +
7359 '#bbb-quick-search-form {display: none;}' +
7360 '.bbb-quick-search-show #bbb-quick-search-form {display: inline;}' +
7361 '#bbb-quick-search-status, #bbb-quick-search-pin {border: none; width: 16px; height: 16px; background-color: transparent; background-repeat: no-repeat; background-color: transparent; background-image: url(\'/images/ui-icons_222222_256x240.png\');}' +
7362 '#bbb-quick-search-status {background-position: -160px -112px;}' + // Magnifying glass.
7363 '.bbb-quick-search-active #bbb-quick-search-status, .bbb-quick-search-show.bbb-quick-search-active.bbb-quick-search-pinned #bbb-quick-search-status {background-position: -128px -112px;}' + // Plus magnifying glass.
7364 '#bbb-quick-search-pin {background-position: -128px -145px;}' + // Horizontal pin.
7365 '.bbb-quick-search-pinned #bbb-quick-search-pin, .bbb-quick-search-active.bbb-quick-search-pinned #bbb-quick-search-status {background-position: -145px -145px;}' + // Vertical pin.
7366 '#bbb-quick-search.bbb-quick-search-active {background-color: #DDDDDD;}' +
7367 '#bbb-quick-search.bbb-quick-search-active.bbb-quick-search-show {background-color: #FFFFFF;}' +
7368 '#bbb-quick-search-pin:focus, #bbb-quick-search-pin:hover {background-color: #CCCCCC;}' +
7369 '#news-updates {padding-right: 25px !important;}';
7370
7371 if (quick_search.indexOf("remove") > -1)
7372 styles += 'article.post-preview.bbb-quick-search-filtered, article.post.post-preview.blacklisted.bbb-quick-search-filtered, article.post-preview.blacklisted.blacklisted-active.bbb-quick-search-filtered {display: none !important;}';
7373 else if (quick_search.indexOf("fade") > -1)
7374 styles += 'article.post-preview.bbb-quick-search-filtered {opacity: 0.1;}';
7375 }
7376
7377 if (resize_link_style === "minimal")
7378 styles += '.bbb-resize-link {display: inline-block; text-align: center; margin-right: 2px; font-size: 87.5%;}';
7379
7380 if (search_add === "remove")
7381 styles += '.search-inc-tag, .search-exl-tag {display: none !important;}';
7382
7383 if (direct_downloads)
7384 styles += '.bbb-ddl {display: none !important;}';
7385
7386 if (post_tag_scrollbars)
7387 styles += '#tag-list ul {max-height: ' + post_tag_scrollbars + 'px !important; overflow-y: auto !important; font-size: 87.5% !important;}';
7388
7389 if (search_tag_scrollbars)
7390 styles += '#tag-box ul {max-height: ' + search_tag_scrollbars + 'px !important; overflow-y: auto !important; font-size: 87.5% !important; margin-right: 2px !important;}';
7391
7392 if (hide_tos_notice && document.getElementById("tos-notice")) {
7393 styles += '#tos-notice {display: none !important;}';
7394
7395 if (manage_cookies)
7396 createCookie("accepted_tos", 1, 365);
7397 }
7398
7399 if (hide_sign_up_notice && document.getElementById("sign-up-notice")) {
7400 styles += '#sign-up-notice {display: none !important;}';
7401
7402 if (manage_cookies)
7403 createCookie("hide_sign_up_notice", 1, 7);
7404 }
7405
7406 if (hide_upgrade_notice && document.getElementById("upgrade-account-notice")) {
7407 styles += '#upgrade-account-notice {display: none !important;}';
7408
7409 if (manage_cookies)
7410 createCookie("hide_upgrade_account_notice", 1, 7);
7411 }
7412
7413 if (hide_ban_notice)
7414 styles += '#ban-notice {display: none !important;}';
7415
7416 if (hide_comment_notice) {
7417 var commentGuide, commentGuideParent; // If/else variables.
7418
7419 if (gLoc === "post") {
7420 commentGuide = document.querySelector("#comments h2 a[href*='howto']");
7421 commentGuideParent = (commentGuide ? commentGuide.parentNode : undefined);
7422
7423 if (commentGuideParent && commentGuideParent.textContent === "Before commenting, read the how to comment guide.")
7424 commentGuideParent.style.display = "none";
7425 }
7426 else if (gLoc === "comments") {
7427 commentGuide = document.querySelector("#a-index div h2 a[href*='howto']");
7428 commentGuideParent = (commentGuide ? commentGuide.parentNode : undefined);
7429
7430 if (commentGuideParent && commentGuideParent.textContent === "Before commenting, read the how to comment guide.")
7431 commentGuideParent.style.display = "none";
7432 }
7433 }
7434
7435 if (hide_tag_notice && gLoc === "post") {
7436 var tagGuide = document.querySelector("#edit div p a[href*='howto']");
7437 var tagGuideParent = (tagGuide ? tagGuide.parentNode : undefined);
7438
7439 if (tagGuideParent && tagGuideParent.textContent === "Before editing, read the how to tag guide.")
7440 tagGuideParent.style.display = "none";
7441 }
7442
7443 if (hide_upload_notice && gLoc === "upload")
7444 styles += '#upload-guide-notice {display: none !important;}';
7445
7446 if (hide_pool_notice && gLoc === "new_pool") {
7447 var poolGuide = document.querySelector("#c-new p a[href*='howto']");
7448 var poolGuideParent = (poolGuide ? poolGuide.parentNode : undefined);
7449
7450 if (poolGuideParent && poolGuideParent.textContent === "Before creating a pool, read the pool guidelines.")
7451 poolGuideParent.style.display = "none";
7452 }
7453
7454 customStyles.innerHTML = styles;
7455 document.getElementsByTagName("head")[0].appendChild(customStyles);
7456 }
7457
7458 function pageCounter() {
7459 // Set up the page counter.
7460 var pageEl = document.getElementById("page");
7461 var paginator = getPaginator();
7462
7463 if (!page_counter || !paginator || !pageEl)
7464 return;
7465
7466 var numString = "";
7467 var lastNumString; // If/else variable.
7468
7469 // Provide page number info if available.
7470 if (/\d/.test(paginator.textContent)) {
7471 var activePage = paginator.getElementsByTagName("span")[0];
7472 var pageItems = paginator.getElementsByTagName("li");
7473 var numPageItems = pageItems.length;
7474 var lastPageItem = pageItems[numPageItems - 1];
7475 var activeNum = activePage.textContent.bbbSpaceClean();
7476 var lastNum; // If/else variable.
7477
7478 if (activePage.parentNode === lastPageItem) // Last/only page case.
7479 lastNum = activeNum;
7480 else { // In all other cases, there should always be a next page button and at least two other page items (1-X).
7481 lastNum = lastPageItem.previousElementSibling.textContent.bbbSpaceClean();
7482
7483 if (!bbbIsNum(lastNum)) // Too many pages for the current user to view.
7484 lastNum = "";
7485 }
7486
7487 lastNumString = (lastNum ? " of " + lastNum : "");
7488 numString = 'Page ' + activeNum + '<span id="bbb-page-counter-last">' + lastNumString + '</span> | ';
7489 }
7490
7491 var pageNav = bbb.el.pageCounter;
7492 var pageInput = bbb.el.pageCounterInput;
7493
7494 if (!pageNav) { // Create the page nav.
7495 var pageNavString = '<div id="bbb-page-counter" style="float: right; font-size: 87.5%;">' + numString + '<form id="bbb-page-counter-form" style="display: inline;"><input id="bbb-page-counter-input" size="4" placeholder="Page#" type="text"> <input type="submit" value="Go"></form></div>';
7496
7497 pageNav = bbb.el.pageCounter = document.createElement("div");
7498 pageNav.innerHTML = pageNavString;
7499
7500 pageInput = bbb.el.pageCounterInput = getId("bbb-page-counter-input", pageNav);
7501
7502 getId("bbb-page-counter-form", pageNav).addEventListener("submit", function(event) {
7503 var value = pageInput.value.bbbSpaceClean();
7504
7505 if (value !== "")
7506 location.href = updateURLQuery(location.href, {page:value});
7507
7508 event.preventDefault();
7509 }, false);
7510
7511 if (numString)
7512 paginator.bbbWatchNodes(pageCounter);
7513
7514 pageEl.insertBefore(pageNav, pageEl.firstElementChild);
7515 }
7516 else // Update the last page in the page nav.
7517 document.getElementById("bbb-page-counter-last").innerHTML = lastNumString;
7518 }
7519
7520 function quickSearch() {
7521 // Set up quick search.
7522 removeInheritedStorage("bbb_quick_search");
7523
7524 if (quick_search === "disabled" || (gLoc !== "search" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "pool" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorite_group"))
7525 return;
7526
7527 var allowAutocomplete = (getMeta("enable-auto-complete") === "true");
7528
7529 // Create the quick search.
7530 var searchDiv = bbb.el.quickSearchDiv = document.createElement("div");
7531 searchDiv.id = "bbb-quick-search";
7532 searchDiv.innerHTML = '<input id="bbb-quick-search-status" type="button" value=""><form id="bbb-quick-search-form"><input id="bbb-quick-search-input" size="75" placeholder="Tags" autocomplete="' + (allowAutocomplete ? "off" : "on") + '" type="text"> <input id="bbb-quick-search-pin" type="button" value=""> <input id="bbb-quick-search-submit" type="submit" value="Go"></form>';
7533
7534 var searchForm = bbb.el.quickSearchForm = getId("bbb-quick-search-form", searchDiv);
7535 var searchInput = bbb.el.quickSearchInput = getId("bbb-quick-search-input", searchDiv);
7536 var searchPin = bbb.el.quickSearchPin = getId("bbb-quick-search-pin", searchDiv);
7537 var searchSubmit = bbb.el.quickSearchSubmit = getId("bbb-quick-search-submit", searchDiv);
7538 var searchStatus = bbb.el.quickSearchStatus = getId("bbb-quick-search-status", searchDiv);
7539
7540 // Make the submit event search posts or reset the search.
7541 searchForm.addEventListener("submit", function(event) {
7542 var oldValue = bbb.quick_search.bbbSpaceClean();
7543 var curValue = searchInput.value.bbbSpaceClean();
7544
7545 if (curValue === "" || curValue === oldValue)
7546 quickSearchReset();
7547 else {
7548 bbb.quick_search = bbb.el.quickSearchInput.value;
7549
7550 if (searchDiv.bbbHasClass("bbb-quick-search-pinned"))
7551 sessionStorage.bbbSetItem("bbb_quick_search", bbb.quick_search);
7552 else if (quick_search.indexOf("pinned") > -1)
7553 quickSearchPinEnable();
7554
7555 quickSearchTest();
7556 }
7557
7558 // Make autocomplete close without getting too tricky.
7559 searchSubmit.focus();
7560 delayMe(function() { searchInput.focus(); }); // Delay this so the blur event has time to register properly.
7561
7562 event.preventDefault();
7563 }, false);
7564
7565 // Hide the search div if the new focus isn't one of the inputs.
7566 searchDiv.addEventListener("blur", function(event) {
7567 var target = event.target;
7568
7569 delayMe(function() {
7570 var active = document.activeElement;
7571
7572 if (active === target || (active !== searchInput && active !== searchSubmit && active !== searchStatus && active !== searchPin))
7573 searchDiv.bbbRemoveClass("bbb-quick-search-show");
7574 });
7575 }, true);
7576
7577 // If a mouse click misses an input within the quick search div, cancel it so the quick search doesn't minimize.
7578 searchDiv.addEventListener("mousedown", function(event) {
7579 var target = event.target;
7580
7581 if (target === searchDiv || target === searchForm)
7582 event.preventDefault();
7583 }, false);
7584
7585 // Hide the search div if the escape key is pressed while using it and autocomplete isn't open.
7586 searchDiv.addEventListener("keydown", function(event) {
7587 if (event.keyCode === 27) {
7588 var jQueryMenu = (searchInput.bbbHasClass("ui-autocomplete-input") ? $("#bbb-quick-search-input").autocomplete("widget")[0] : undefined);
7589
7590 if (jQueryMenu && jQueryMenu.style.display !== "none")
7591 return;
7592
7593 document.activeElement.blur();
7594 event.preventDefault();
7595 }
7596 }, true);
7597
7598 // Show/hide the search div via a left click on the main status icon. If the shift key is held down, toggle the pinned status.
7599 searchStatus.addEventListener("click", function(event) {
7600 if (event.button === 0) {
7601 if (event.shiftKey)
7602 quickSearchPinToggle();
7603 else if (!searchDiv.bbbHasClass("bbb-quick-search-show"))
7604 quickSearchOpen();
7605 else
7606 searchDiv.bbbRemoveClass("bbb-quick-search-show");
7607 }
7608
7609 event.preventDefault();
7610 }, false);
7611
7612 // Reset via a right click on the main status icon.
7613 searchStatus.addEventListener("mouseup", function(event) {
7614 if (event.button === 2 && searchDiv.bbbHasClass("bbb-quick-search-active"))
7615 quickSearchReset();
7616
7617 event.preventDefault();
7618 }, false);
7619
7620 // Stop the context menu on the status icon.
7621 searchStatus.addEventListener("contextmenu", disableEvent, false);
7622
7623 // Make the pin input toggle the pinned status.
7624 searchPin.addEventListener("click", function(event) {
7625 if (event.button === 0)
7626 quickSearchPinToggle();
7627 }, false);
7628
7629 // Watch the input value and adjust the quick search as needed.
7630 searchInput.addEventListener("input", quickSearchCheck, false);
7631 searchInput.addEventListener("keyup", quickSearchCheck, false);
7632 searchInput.addEventListener("cut", quickSearchCheck, false);
7633 searchInput.addEventListener("paste", quickSearchCheck, false);
7634 searchInput.addEventListener("change", quickSearchCheck, false);
7635
7636 document.body.insertBefore(searchDiv, document.body.firstElementChild);
7637
7638 // Force the submit button to retain its width.
7639 searchDiv.bbbAddClass("bbb-quick-search-show");
7640 searchSubmit.style.width = searchSubmit.offsetWidth + "px";
7641 searchDiv.bbbRemoveClass("bbb-quick-search-show");
7642
7643 // Take of a copy of Danbooru's autocomplete and modify it for the search.
7644 if (allowAutocomplete && Danbooru.Autocomplete && Danbooru.Autocomplete.initialize_tag_autocomplete) {
7645 try {
7646 var autoComplete = Danbooru.Autocomplete.initialize_tag_autocomplete.toString().match(/\{([\s\S]*)\}/)[1];
7647 var searchAutoComplete = autoComplete.replace(/(,)#tags|#tags(,)/, "$1#tags,#bbb-quick-search-input$2"); // /\$\([\s\S]*?#tags[\s\S]*?\)([\s\S]*?)\$\([\s\S]*?#artist_name[\s\S]*?\)/, '$("#bbb-quick-search-input")$1$()'
7648 var autoInit = new Function(searchAutoComplete);
7649
7650 autoInit();
7651
7652 // Counter normal autocomplete getting turned back on after submitting an input.
7653 document.body.addEventListener("focus", function(event) {
7654 var target = event.target;
7655
7656 if (target.bbbHasClass("ui-autocomplete-input"))
7657 target.setAttribute("autocomplete", "off");
7658 }, true);
7659
7660 // Make autocomplete fixed like the quick search.
7661 $(searchInput).autocomplete("widget").css("position", "fixed");
7662 }
7663 catch (error) {
7664 bbbNotice("Unexpected error while trying to initialize autocomplete for the quick search. (Error: " + error.message + ")", -1);
7665 }
7666 }
7667
7668 // Check if the quick search has been pinned for this session.
7669 var pinnedSearch = sessionStorage.getItem("bbb_quick_search");
7670
7671 if (pinnedSearch) {
7672 bbb.quick_search = pinnedSearch;
7673 searchInput.value = pinnedSearch;
7674 searchDiv.bbbAddClass("bbb-quick-search-pinned");
7675 quickSearchTest();
7676 }
7677
7678 // Create the hotkeys.
7679 createHotkey("70", quickSearchOpen); // F
7680 createHotkey("s70", quickSearchReset); // SHIFT + F
7681 }
7682
7683 function quickSearchCheck() {
7684 // Check the input value and adjust the submit button appearance accordingly.
7685 var input = bbb.el.quickSearchInput;
7686 var submit = bbb.el.quickSearchSubmit;
7687 var oldValue = bbb.quick_search.bbbSpaceClean();
7688 var curValue = input.value.bbbSpaceClean();
7689
7690 if (oldValue === curValue && curValue !== "")
7691 submit.value = "X";
7692 else
7693 submit.value = "Go";
7694 }
7695
7696 function quickSearchReset() {
7697 // Completely reset the quick search.
7698 var filteredPosts = document.getElementsByClassName("bbb-quick-search-filtered");
7699 var filteredPost = filteredPosts[0];
7700
7701 bbb.quick_search = "";
7702 bbb.el.quickSearchInput.value = "";
7703 bbb.el.quickSearchSubmit.value = "Go";
7704 bbb.el.quickSearchStatus.title = "";
7705 sessionStorage.removeItem("bbb_quick_search");
7706 bbb.el.quickSearchDiv.bbbRemoveClass("bbb-quick-search-active bbb-quick-search-pinned");
7707
7708 while (filteredPost) {
7709 filteredPost.bbbRemoveClass("bbb-quick-search-filtered");
7710 enablePostDDL(filteredPost);
7711 filteredPost = filteredPosts[0];
7712 }
7713 }
7714
7715 function quickSearchTest(target) {
7716 // Test posts to see if they match the search.
7717 var value = bbb.quick_search.bbbSpaceClean();
7718
7719 if (value === "")
7720 return;
7721
7722 var posts = getPosts(target);
7723 var search = createSearch(value);
7724
7725 bbb.el.quickSearchSubmit.value = "X";
7726 bbb.el.quickSearchStatus.title = value;
7727 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-active");
7728
7729 for (var i = 0, il = posts.length; i < il; i++) {
7730 var post = posts[i];
7731
7732 if (!thumbSearchMatch(post, search)) {
7733 post.bbbAddClass("bbb-quick-search-filtered");
7734 disablePostDDL(post);
7735 }
7736 else {
7737 post.bbbRemoveClass("bbb-quick-search-filtered");
7738 enablePostDDL(post);
7739 }
7740 }
7741 }
7742
7743 function quickSearchOpen() {
7744 // Open the quick search div and place focus on the input.
7745 var searchInput = bbb.el.quickSearchInput;
7746
7747 searchInput.value = bbb.quick_search;
7748 quickSearchCheck();
7749 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-show");
7750 searchInput.focus();
7751 }
7752
7753 function quickSearchPinToggle() {
7754 // Toggle the quick search between pinned and not pinned for the session.
7755 var searchDiv = bbb.el.quickSearchDiv;
7756
7757 if (searchDiv.bbbHasClass("bbb-quick-search-show", "bbb-quick-search-active")) {
7758 if (!searchDiv.bbbHasClass("bbb-quick-search-pinned"))
7759 quickSearchPinEnable();
7760 else
7761 quickSearchPinDisable();
7762 }
7763 }
7764
7765 function quickSearchPinEnable() {
7766 // Enable the quick search pin.
7767 bbb.el.quickSearchDiv.bbbAddClass("bbb-quick-search-pinned");
7768
7769 if (bbb.quick_search)
7770 sessionStorage.bbbSetItem("bbb_quick_search", bbb.quick_search);
7771 }
7772
7773 function quickSearchPinDisable() {
7774 // Disable the quick search pin.
7775 bbb.el.quickSearchDiv.bbbRemoveClass("bbb-quick-search-pinned");
7776 sessionStorage.removeItem("bbb_quick_search");
7777 }
7778
7779 function moveSaveSearch() {
7780 // Move the "save search" div into the sidebar related section and style it as a link.
7781 var saveSearchDiv = document.getElementById("saved-searches-nav");
7782 var relatedSection = document.getElementById("related-box");
7783
7784 if (!move_save_search || !saveSearchDiv || !relatedSection)
7785 return;
7786
7787 saveSearchDiv.parentNode.removeChild(saveSearchDiv);
7788
7789 var relatedSectionMenu = relatedSection.getElementsByTagName("ul")[0];
7790
7791 var saveSearchItem = document.createElement("li");
7792 saveSearchItem.className = "bbb-saved-search-item";
7793 saveSearchItem.appendChild(saveSearchDiv);
7794
7795 relatedSectionMenu.insertBefore(saveSearchItem, relatedSectionMenu.firstElementChild);
7796 }
7797
7798 function commentScoreInit() {
7799 // Set up the initial comment scores and get ready to handle new comments.
7800 if (!comment_score || (gLoc !== "comments" && gLoc !== "comment_search" && gLoc !== "comment" && gLoc !== "post"))
7801 return;
7802
7803 var paginator = getPaginator();
7804 var watchedNode = (paginator ? paginator.parentNode : document.body);
7805
7806 commentScore();
7807 watchedNode.bbbWatchNodes(commentScore);
7808 }
7809
7810 function commentScore() {
7811 // Add score links to comments that link directly to that comment.
7812 var comments = document.getElementsByClassName("comment");
7813 var scoredComments = document.getElementsByClassName("bbb-comment-score");
7814
7815 // If all the comments are scored, just stop.
7816 if (comments.length === scoredComments.length)
7817 return;
7818
7819 for (var i = 0, il = comments.length; i < il; i++) {
7820 var comment = comments[i];
7821
7822 // Skip if the comment is already scored.
7823 if (comment.getElementsByClassName("bbb-comment-score")[0])
7824 continue;
7825
7826 var score = comment.getAttribute("data-score");
7827 var commentId = comment.getAttribute("data-comment-id");
7828 var content = comment.getElementsByClassName("content")[0];
7829 var menu = (content ? content.getElementsByTagName("menu")[0] : undefined);
7830
7831 if (content && !menu) {
7832 menu = document.createElement("menu");
7833 content.appendChild(menu);
7834 }
7835
7836 var menuItems = menu.getElementsByTagName("li");
7837 var listingItemSibling = menuItems[1];
7838
7839 for (var j = 0, jl = menuItems.length; j < jl; j++) {
7840 var menuItem = menuItems[j];
7841 var nextItem = menuItems[j + 1];
7842
7843 if (menuItem.textContent.indexOf("Reply") > -1) {
7844 if (nextItem)
7845 listingItemSibling = nextItem;
7846 else
7847 listingItemSibling = undefined;
7848 }
7849 }
7850
7851 var scoreItem = document.createElement("li");
7852 scoreItem.className = "bbb-comment-score";
7853
7854 var scoreLink = document.createElement("a");
7855 scoreLink.innerHTML = "Score: " + score;
7856 scoreLink.href = "/comments/" + commentId;
7857 scoreItem.appendChild(scoreLink);
7858
7859 if (listingItemSibling)
7860 menu.insertBefore(scoreItem, listingItemSibling);
7861 else
7862 menu.appendChild(scoreItem);
7863 }
7864 }
7865
7866 function thumbInfo(target) {
7867 // Add score, favorite count, and rating info to thumbnails.
7868 var posts = getPosts(target);
7869
7870 if (thumb_info === "disabled")
7871 return;
7872
7873 for (var i = 0, il = posts.length; i < il; i++) {
7874 var post = posts[i];
7875
7876 // Skip thumbnails that already have the info added.
7877 if (post.getElementsByClassName("bbb-thumb-info")[0])
7878 continue;
7879
7880 var score = Number(post.getAttribute("data-score"));
7881 var favCount = post.getAttribute("data-fav-count");
7882 var rating = post.getAttribute("data-rating").toUpperCase();
7883 var width = Number(post.getAttribute("data-width"));
7884 var height = Number(post.getAttribute("data-height"));
7885 var tooShort = (150 / width * height < 30); // Short thumbnails will need the info div position adjusted.
7886
7887 if (gLoc === "comments") { // Add favorites info to the existing info in the comments listing.
7888 var firstInfo = post.getElementsByClassName("info")[0];
7889 var infoParent = (firstInfo ? firstInfo.parentNode : undefined);
7890
7891 if (infoParent) {
7892 var favSpan = document.createElement("span");
7893 favSpan.className = "info bbb-thumb-info";
7894 favSpan.innerHTML = '<strong>Favorites</strong> ' + favCount;
7895 infoParent.appendChild(favSpan);
7896 }
7897 }
7898 else { // Add extra information inside of the thumbnail's parent element.
7899 var thumbImg = post.getElementsByTagName("img")[0];
7900
7901 // Don't add the info if there isn't a thumbnail.
7902 if (!thumbImg)
7903 continue;
7904
7905 var thumbEl = post.getElementsByClassName("preview")[0] || post;
7906 thumbEl.bbbAddClass("bbb-thumb-info-parent");
7907
7908 var postLink = thumbEl.getElementsByTagName("a")[0];
7909 var before = (postLink ? postLink.nextElementSibling : undefined);
7910
7911 score = (score < 0 ? '<span style="color: #CC0000;">' + score + '</span>' : score);
7912
7913 var infoDiv = document.createElement("div");
7914 infoDiv.className = "bbb-thumb-info" + (tooShort ? " bbb-thumb-info-short" : "");
7915 infoDiv.innerHTML = "★" + score + " ♥" + favCount + (location.host.indexOf("safebooru") < 0 ? " " + rating : "");
7916
7917 if (before)
7918 thumbEl.insertBefore(infoDiv, before);
7919 else
7920 thumbEl.appendChild(infoDiv);
7921 }
7922 }
7923 }
7924
7925 function postLinkNewWindow() {
7926 // Make thumbnail clicks open in a new tab/window.
7927 if (post_link_new_window === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "popular" && gLoc !== "popular_view" && gLoc !== "favorite_group"))
7928 return;
7929
7930 document.addEventListener("click", function(event) {
7931 var bypass = (event.shiftKey && event.ctrlKey);
7932 var modeSection = document.getElementById("mode-box");
7933 var danbMode = getCookie().mode || "view";
7934
7935 if (event.button !== 0 || event.altKey || !bypass && (event.shiftKey || event.ctrlKey) || (modeSection && danbMode !== "view"))
7936 return;
7937
7938 var runEndless = (post_link_new_window.indexOf("endless") > -1);
7939 var runNormal = (post_link_new_window.indexOf("normal") > -1);
7940
7941 if ((bbb.endless.enabled && !runEndless) || (!bbb.endless.enabled && !runNormal))
7942 return;
7943
7944 var target = event.target;
7945 var targetTag = target.tagName;
7946 var url; // If/else variable.
7947
7948 if (targetTag === "IMG" && target.parentNode)
7949 url = target.parentNode.href;
7950 else if (targetTag === "A" && target.bbbHasClass("bbb-post-link", "bbb-thumb-link"))
7951 url = target.href;
7952
7953 if (url && /\/posts\/\d+/.test(url)) {
7954 if (bypass)
7955 location.href = url;
7956 else
7957 window.open(url);
7958
7959 event.preventDefault();
7960 }
7961 }, false);
7962 }
7963
7964 function formatTip(event, el, content, x, y) {
7965 // Position + resize the tip and display it.
7966 var tip = el;
7967 var windowX = event.clientX;
7968 var windowY = event.clientY;
7969 var topOffset = 0;
7970 var leftOffset = 0;
7971
7972 if (typeof(content) === "string")
7973 tip.innerHTML = content;
7974 else {
7975 tip.innerHTML = "";
7976 tip.appendChild(content);
7977 }
7978
7979 tip.style.visibility = "hidden";
7980 tip.style.display = "block";
7981
7982 // Resize the tip to minimize blank space.
7983 var origHeight = tip.clientHeight;
7984 var padding = tip.bbbGetPadding();
7985 var paddingWidth = padding.width;
7986
7987 while (origHeight >= tip.clientHeight && tip.clientWidth > 15)
7988 tip.style.width = tip.clientWidth - paddingWidth - 2 + "px";
7989
7990 tip.style.width = tip.clientWidth - paddingWidth + 2 + "px";
7991
7992 if (tip.scrollWidth > tip.clientWidth)
7993 tip.style.width = "auto";
7994
7995 // Don't allow the tip to go above the top of the window.
7996 if (windowY - tip.offsetHeight < 5)
7997 topOffset = windowY - tip.offsetHeight - 5;
7998
7999 // Don't allow the tip to go beyond the left edge of the window.
8000 if (windowX - tip.offsetWidth < 5)
8001 leftOffset = tip.offsetWidth + 1;
8002
8003 tip.style.left = x - tip.offsetWidth + leftOffset + "px";
8004 tip.style.top = y - tip.offsetHeight - topOffset + "px";
8005 tip.style.visibility = "visible";
8006 }
8007
8008 function bbbHotkeys() {
8009 // Handle keydown events not taking place in text inputs and check if they're a hotkey.
8010 document.addEventListener("keydown", function(event) {
8011 var active = document.activeElement;
8012 var activeTag = active.tagName;
8013 var activeType = active.type;
8014
8015 if (activeTag === "SELECT" || activeTag === "TEXTAREA" || (activeTag === "INPUT" && !/^(?:button|checkbox|file|hidden|image|radio|reset|submit)$/.test(activeType))) // Input types: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes
8016 return;
8017
8018 var loc = (gLoc === "post" ? "post" : "other");
8019 var hotkeyCode = createHotkeyCode(event);
8020 var hotkey = bbb.hotkeys[loc][hotkeyCode];
8021
8022 if (hotkey) {
8023 var customHandler = hotkey.custom_handler;
8024 customHandler = (typeof(customHandler) !== "boolean" || customHandler !== true ? false : true);
8025
8026 hotkey.func(event); // The event object will always be the first argument passed to the provided function (previously declared or anonymous).
8027
8028 if (!customHandler) {
8029 event.preventDefault();
8030 event.stopPropagation();
8031 }
8032 }
8033 }, true);
8034 }
8035
8036 function createHotkeyCode(event) {
8037 // Take a keyboard event and create a code for its key combination.
8038 // Examples: s49 = Shift + "1", a50 = Alt + "2", cs51 = Control + Shift + "3"
8039 // Alt, control, meta, and shift abbreviations should be alphabetical. Keycode numbers: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
8040 var hotkeycode = "";
8041
8042 if (event.altKey)
8043 hotkeycode += "a";
8044
8045 if (event.ctrlKey)
8046 hotkeycode += "c";
8047
8048 if (event.metaKey)
8049 hotkeycode += "m";
8050
8051 if (event.shiftKey)
8052 hotkeycode += "s";
8053
8054 if (event.keyCode)
8055 hotkeycode += event.keyCode;
8056
8057 return (hotkeycode || undefined);
8058 }
8059
8060 function createHotkey(hotkeyCode, func, propObject) {
8061 // Create hotkeys or override Danbooru's existing ones. Creating a hotkey for a hotkey that already exists will replace it.
8062 var loc = (gLoc === "post" ? "post" : "other");
8063 var hotkeyObject = {func: func};
8064
8065 if (propObject) {
8066 for (var i in propObject) {
8067 if (propObject.hasOwnProperty(i))
8068 hotkeyObject[i] = propObject[i];
8069 }
8070 }
8071
8072 bbb.hotkeys[loc][hotkeyCode] = hotkeyObject;
8073 }
8074
8075 function removeHotkey(hotkeyCode) {
8076 // Remove a hotkey.
8077 var loc = (gLoc === "post" ? "post" : "other");
8078
8079 delete bbb.hotkeys[loc][hotkeyCode];
8080 }
8081
8082 function resizeHotkey(event) {
8083 // Handle the resize post hotkeys and make sure they don't interfere with the favorite group dialog box hotkeys.
8084 var favGroup = document.querySelector("div[aria-describedby='add-to-favgroup-dialog']");
8085
8086 if (favGroup && favGroup.style.display !== "none")
8087 return;
8088
8089 var keyCode = event.keyCode;
8090 var mode; // Switch variable.
8091
8092 switch (keyCode) {
8093 case 49:
8094 mode = "all";
8095 break;
8096 case 50:
8097 mode = "width";
8098 break;
8099 case 51:
8100 mode = "height";
8101 break;
8102 case 52:
8103 default:
8104 mode = "none";
8105 break;
8106 }
8107
8108 resizePost(mode);
8109 event.preventDefault();
8110 event.stopPropagation();
8111 }
8112
8113 function fixLimit(limit) {
8114 // Add the limit variable to link URLs that are not thumbnails.
8115 if (!thumbnail_count && limit === undefined)
8116 return;
8117
8118 var newLimit = (limit === undefined ? thumbnail_count : limit) || undefined;
8119 var page = document.getElementById("page");
8120 var header = document.getElementById("top");
8121 var searchParent = document.getElementById("search-box") || document.getElementById("a-intro");
8122 var i, il, links, link, linkHref; // Loop variables.
8123
8124 if (page) {
8125 links = page.getElementsByTagName("a");
8126
8127 for (i = 0, il = links.length; i < il; i++) {
8128 link = links[i];
8129 linkHref = link.getAttribute("href"); // Use getAttribute so that we get the exact value. "link.href" adds in the domain.
8130
8131 if (linkHref && !/page=/.test(linkHref) && (linkHref.indexOf("/posts?") === 0 || linkHref.indexOf("/favorites?") === 0))
8132 link.href = updateURLQuery(linkHref, {limit: newLimit});
8133 }
8134 }
8135
8136 if (header) {
8137 links = header.getElementsByTagName("a");
8138
8139 for (i = 0, il = links.length; i < il; i++) {
8140 link = links[i];
8141 linkHref = link.getAttribute("href");
8142
8143 if (linkHref && (linkHref.indexOf("limit=") > -1 || linkHref.indexOf("/posts") === 0 || linkHref === "/" || linkHref === "/notes?group_by=post" || linkHref === "/favorites"))
8144 link.href = updateURLQuery(linkHref, {limit: newLimit});
8145 }
8146 }
8147
8148 // Fix the search.
8149 if (searchParent && (gLoc === "search" || gLoc === "post" || gLoc === "intro" || gLoc === "favorites")) {
8150 var search = searchParent.getElementsByTagName("form")[0];
8151
8152 if (search) {
8153 var limitInput = bbb.el.limitInput;
8154
8155 if (!limitInput) {
8156 limitInput = bbb.el.limitInput = document.createElement("input");
8157 limitInput.name = "limit";
8158 limitInput.value = newLimit;
8159 limitInput.type = "hidden";
8160 search.appendChild(limitInput);
8161
8162 // Change the form action if on the favorites page. It uses "/favorites", but that just goes to the normal "/posts" search while stripping out the limit.
8163 search.action = "/posts";
8164
8165 // Remove the user's default limit if the user tries to specify a limit value in the tags.
8166 var tagsInput = document.getElementById("tags");
8167
8168 if (tagsInput) {
8169 search.addEventListener("submit", function() {
8170 if (/(?:^|\s)limit:/.test(tagsInput.value))
8171 search.removeChild(limitInput);
8172 else if (limitInput.parentNode !== search)
8173 search.appendChild(limitInput);
8174 }, false);
8175 }
8176 }
8177 else
8178 limitInput.value = newLimit || thumbnail_count_default;
8179 }
8180 }
8181 }
8182
8183 function fixURLLimit() {
8184 // Update the URL limit value with the user's limit.
8185 if (allowUserLimit()) {
8186 var state = history.state;
8187 var url = updateURLQuery(location.search, {limit: thumbnail_count});
8188
8189 history.replaceState(state, "", url);
8190 location.replace(location.href.split("#", 1)[0] + "#"); // Force browser caching to cooperate.
8191 history.replaceState(state, "", url);
8192 }
8193 }
8194
8195 function saveStateCache() {
8196 // Cache a search's thumbnails to history state to prevent replaceState/browser caching issues.
8197 var state = history.state || {};
8198
8199 if (isRandomSearch() && !state.bbb_posts_cache) {
8200 var posts = getPosts();
8201 var postsObject = [];
8202
8203 for (var i = 0, il = posts.length; i < il; i++)
8204 postsObject.push(scrapeThumb(posts[i]));
8205
8206 state.bbb_posts_cache = JSON.stringify(postsObject);
8207 sessionStorage.bbbSetItem("bbb_posts_cache", state.bbb_posts_cache.bbbHash()); // Key used to detect if the page is reloaded/re-entered.
8208 history.replaceState(state, "");
8209 }
8210 }
8211
8212 function checkStateCache() {
8213 // Check for the history state cache and erase it if the page is reloaded or re-entered.
8214 removeInheritedStorage("bbb_posts_cache");
8215
8216 var historyHash = sessionStorage.getItem("bbb_posts_cache") || "";
8217 var state = history.state || {};
8218
8219 if (state.bbb_posts_cache) {
8220 var stateHash = state.bbb_posts_cache.bbbHash();
8221
8222 if (historyHash === String(stateHash)) { // Reloaded. Remove everything since we're on the same page.
8223 delete state.bbb_posts_cache;
8224 history.replaceState(state, "");
8225 sessionStorage.removeItem("bbb_posts_cache");
8226 }
8227 else // Returned. Set the hash again since we're back.
8228 sessionStorage.bbbSetItem("bbb_posts_cache", stateHash);
8229 }
8230 else if (historyHash) // Back/forward. Remove the hash since we're on a new page.
8231 sessionStorage.removeItem("bbb_posts_cache");
8232 }
8233
8234 function arrowNav() {
8235 // Bind the arrow keys to Danbooru's page navigation.
8236 var paginator = getPaginator();
8237
8238 if (!arrow_nav || (!paginator && gLoc !== "popular")) // If the paginator exists, arrow navigation should be applicable.
8239 return;
8240
8241 // Create the hotkeys for the left and right arrows.
8242 createHotkey("37", function() { danbooruNav("prev"); });
8243 createHotkey("39", function() { danbooruNav("next"); });
8244 }
8245
8246 function danbooruNav(dir) {
8247 // Determine the correct Danbooru page function and use it.
8248 if (gLoc === "popular") {
8249 if (dir === "prev")
8250 Danbooru.PostPopular.nav_prev();
8251 else if (dir === "next")
8252 Danbooru.PostPopular.nav_next();
8253 }
8254 else {
8255 if (dir === "prev")
8256 Danbooru.Paginator.prev_page();
8257 else if (dir === "next")
8258 Danbooru.Paginator.next_page();
8259 }
8260 }
8261
8262 function autohideSidebar() {
8263 // Show the sidebar when it gets focus, hide it when it loses focus, and only allow select elements to retain focus.
8264 var sidebar = document.getElementById("sidebar");
8265
8266 if (!autohide_sidebar || !sidebar)
8267 return;
8268
8269 sidebar.addEventListener("click", function(event) {
8270 var target = event.target;
8271
8272 if (event.button === 0 && target.id !== "tags")
8273 target.blur();
8274 }, false);
8275 sidebar.addEventListener("mouseup", function(event) {
8276 var target = event.target;
8277
8278 if (event.button !== 0 && target.id !== "tags")
8279 target.blur();
8280 }, false);
8281 sidebar.addEventListener("focus", function() {
8282 sidebar.bbbAddClass("bbb-sidebar-show");
8283 }, true);
8284 sidebar.addEventListener("blur", function() {
8285 sidebar.bbbRemoveClass("bbb-sidebar-show");
8286 }, true);
8287 }
8288
8289 function fixedSidebar() {
8290 // Fix the scrollbar to the top/bottom of the window when it would normally scroll out of view.
8291 var sidebar = bbb.fixed_sidebar.sidebar = document.getElementById("sidebar");
8292 var content = bbb.fixed_sidebar.content = document.getElementById("content");
8293 var comments = document.getElementById("comments");
8294
8295 if (!fixed_sidebar || autohide_sidebar || !sidebar || !content || (gLoc === "post" && !comments))
8296 return;
8297
8298 var docRect = document.documentElement.getBoundingClientRect();
8299 var sidebarRect = sidebar.getBoundingClientRect();
8300 var sidebarTop = bbb.fixed_sidebar.top = sidebarRect.top - docRect.top;
8301 var sidebarLeft = bbb.fixed_sidebar.left = sidebarRect.left - docRect.left;
8302 var sidebarHeight = sidebarRect.height;
8303
8304 content.style.minHeight = sidebarHeight - 1 + "px";
8305 sidebar.style.overflow = "hidden"; // There are some cases where text overflows.
8306
8307 if (comments)
8308 comments.style.overflow = "auto"; // Force the contained float elements to affect the dimensions.
8309
8310 fixedSidebarCheck();
8311 document.body.bbbWatchNodes(fixedSidebarCheck);
8312 document.addEventListener("keyup", fixedSidebarCheck, false);
8313 document.addEventListener("click", fixedSidebarCheck, false);
8314 window.addEventListener("scroll", fixedSidebarCheck, false);
8315 window.addEventListener("resize", fixedSidebarCheck, false);
8316 }
8317
8318 function fixedSidebarCheck() {
8319 // Event handler for adjusting the sidebar position.
8320 var sidebar = bbb.fixed_sidebar.sidebar;
8321 var content = bbb.fixed_sidebar.content;
8322 var sidebarTop = bbb.fixed_sidebar.top;
8323 var sidebarLeft = bbb.fixed_sidebar.left;
8324 var verScrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
8325 var horScrolled = window.payeXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
8326 var sidebarHeight = sidebar.clientHeight; // Height can potentially change (blacklist update, etc.) so always recalculate it.
8327 var contentHeight = content.clientHeight;
8328 var viewHeight = document.documentElement.clientHeight;
8329 var sidebarBottom = sidebarTop + sidebarHeight;
8330 var contentBottom = sidebarTop + contentHeight;
8331 var viewportBottom = verScrolled + viewHeight;
8332
8333 if (sidebarHeight > contentHeight) // Don't fix to window if there's no space for it to scroll.
8334 sidebar.style.position = "static";
8335 else if (sidebarHeight < viewHeight) { // Fix to the top of the window if not too tall and far enough down.
8336 if (contentBottom < verScrolled + sidebarHeight) {
8337 sidebar.style.position = "absolute";
8338 sidebar.style.bottom = viewHeight - contentBottom + "px";
8339 sidebar.style.top = "auto";
8340 }
8341 else if (sidebarTop < verScrolled ) {
8342 sidebar.style.position = "fixed";
8343 sidebar.style.bottom = "auto";
8344 sidebar.style.top = "0px";
8345 }
8346 else
8347 sidebar.style.position = "static";
8348 }
8349 else { // If too tall, fix the sidebar bottom to the viewport bottom to avoid putting part of the sidebar permanently beyond reach.
8350 if (viewportBottom > contentBottom) {
8351 sidebar.style.position = "absolute";
8352 sidebar.style.bottom = viewHeight - contentBottom + "px";
8353 sidebar.style.top = "auto";
8354 }
8355 else if (sidebarTop > verScrolled || sidebarTop > viewportBottom - sidebarHeight)
8356 sidebar.style.position = "static";
8357 else if (sidebarBottom < viewportBottom) {
8358 sidebar.style.position = "fixed";
8359 sidebar.style.bottom = "0px";
8360 sidebar.style.top = "auto";
8361 }
8362 }
8363
8364 // Maintain horizontal position in the document.
8365 if (horScrolled && sidebar.style.position !== "absolute")
8366 sidebar.style.left = (sidebarLeft - horScrolled) + "px";
8367 else
8368 sidebar.style.left = sidebarLeft + "px";
8369 }
8370
8371 function fixedPaginator() {
8372 // Set up the fixed paginator.
8373 if (fixed_paginator === "disabled" || (gLoc !== "search" && gLoc !== "pool" && gLoc !== "notes" && gLoc !== "favorites" && gLoc !== "favorite_group"))
8374 return;
8375
8376 var paginator = getPaginator();
8377 var paginatorMenu = (paginator ? paginator.getElementsByTagName("menu")[0] : undefined);
8378 var paginatorLink = (paginatorMenu ? (paginatorMenu.getElementsByTagName("a")[0] || paginatorMenu.getElementsByTagName("span")[0]) : undefined);
8379
8380 if (!paginatorLink)
8381 return;
8382
8383 // Get all our measurements.
8384 var docRect = document.documentElement.getBoundingClientRect();
8385 var docWidth = docRect.width;
8386 var docBottom = docRect.bottom;
8387
8388 var paginatorRect = paginator.getBoundingClientRect();
8389 var paginatorBottom = paginatorRect.bottom;
8390 var paginatorLeft = paginatorRect.left;
8391 var paginatorRight = docWidth - paginatorRect.right;
8392 var paginatorHeight = paginatorRect.height;
8393
8394 var menuRect = paginatorMenu.getBoundingClientRect();
8395 var menuBottom = menuRect.bottom;
8396
8397 var linkRect = paginatorLink.getBoundingClientRect();
8398 var linkBottom = linkRect.bottom;
8399
8400 var paginatorMargAdjust = (paginatorLeft - paginatorRight) / 2;
8401 var menuBottomAdjust = linkBottom - menuBottom;
8402
8403 var paginatorSpacer = document.createElement("div"); // Prevents the document height form changing when the paginator is fixed to the bottom of the window.
8404 paginatorSpacer.id = "bbb-fixed-paginator-spacer";
8405
8406 var paginatorSibling = paginator.nextElementSibling;
8407
8408 if (paginatorSibling)
8409 paginator.parentNode.insertBefore(paginatorSpacer, paginatorSibling);
8410 else
8411 paginator.parentNode.appendChild(paginatorSpacer);
8412
8413 // Create the CSS for the fixed paginator separately from the main one since it needs to know what the page's final layout will be with the main CSS applied.
8414 var style = document.createElement("style");
8415 style.type = "text/css";
8416 style.innerHTML = '.bbb-fixed-paginator div.paginator {position: fixed; padding: 0px; margin: 0px; bottom: 0px; left: 50%; margin-left: ' + paginatorMargAdjust + 'px;}' +
8417 '.bbb-fixed-paginator div.paginator menu {position: relative; left: -50%; padding: ' + menuBottomAdjust + 'px 0px; background-color: #FFFFFF;}' +
8418 '.bbb-fixed-paginator div.paginator menu li:first-child {padding-left: 0px;}' +
8419 '.bbb-fixed-paginator div.paginator menu li:first-child > * {margin-left: 0px;}' +
8420 '.bbb-fixed-paginator div.paginator menu li:last-child {padding-right: 0px;}' +
8421 '.bbb-fixed-paginator div.paginator menu li:last-child > * {margin-right: 0px;}' +
8422 '#bbb-fixed-paginator-spacer {display: none; height: ' + paginatorHeight + 'px; clear: both; width: 100%;}' +
8423 '.bbb-fixed-paginator #bbb-fixed-paginator-spacer {display: block;}';
8424
8425 if (fixed_paginator.indexOf("minimal") > -1) {
8426 style.innerHTML += '.bbb-fixed-paginator div.paginator menu {padding: 3px 0px;}' +
8427 '.bbb-fixed-paginator div.paginator menu li a, .bbb-fixed-paginator div.paginator menu li span {padding: 2px; margin: 0px 2px 0px 0px;}' +
8428 '.bbb-fixed-paginator div.paginator menu li {padding: 0px;}' +
8429 '.bbb-fixed-paginator div.paginator menu li a {border-color: #CCCCCC;}';
8430 }
8431
8432 document.getElementsByTagName("head")[0].appendChild(style);
8433
8434 bbb.fixed_paginator_space = docBottom - paginatorBottom - menuBottomAdjust; // Store the amount of space between the bottom of the page and the paginator.
8435
8436 document.body.bbbWatchNodes(fixedPaginatorCheck);
8437 document.addEventListener("keyup", fixedPaginatorCheck, false);
8438 document.addEventListener("click", fixedPaginatorCheck, false);
8439 window.addEventListener("scroll", fixedPaginatorCheck, false);
8440 window.addEventListener("resize", fixedPaginatorCheck, false);
8441
8442 fixedPaginatorCheck();
8443 }
8444
8445 function fixedPaginatorCheck() {
8446 // Check if the paginator needs to be in its default position or fixed to the window.
8447 if (!bbb.fixed_paginator_space)
8448 return;
8449
8450 var runEndless = (fixed_paginator.indexOf("endless") > -1);
8451 var runNormal = (fixed_paginator.indexOf("normal") > -1);
8452 var docHeight = document.documentElement.scrollHeight;
8453 var scrolled = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
8454 var viewHeight = document.documentElement.clientHeight;
8455
8456 if (viewHeight + scrolled < docHeight - bbb.fixed_paginator_space && ((runEndless && bbb.endless.enabled) || (runNormal && !bbb.endless.enabled)))
8457 document.body.bbbAddClass("bbb-fixed-paginator");
8458 else
8459 document.body.bbbRemoveClass("bbb-fixed-paginator");
8460 }
8461
8462 function collapseSidebar() {
8463 // Allow clicking on headers to collapse and expand their respective sections.
8464 var sidebar = document.getElementById("sidebar");
8465
8466 if (!collapse_sidebar || !sidebar)
8467 return;
8468
8469 var dataLoc = (gLoc === "post" ? "post" : "thumb");
8470 var data = collapse_sidebar_data[dataLoc];
8471 var tagTypes = ["h1", "h2"]; // Tags that will be allowed to toggle.
8472 var nameList = " ";
8473 var removedOld = false;
8474 var i, il; // Loop variables.
8475
8476 // Grab the desired tags and turn them into toggle elements for their section.
8477 for (i = 0, il = tagTypes.length; i < il; i++) {
8478 var tags = sidebar.getElementsByTagName(tagTypes[i]);
8479
8480 for (var j = 0, jl = tags.length; j < jl; j++) {
8481 var tag = tags[j];
8482 var name = tag.textContent.bbbSpaceClean().replace(" ", "_");
8483 var collapse = data[name];
8484 var sibling = tag.nextElementSibling;
8485 nameList += name + " ";
8486
8487 tag.addEventListener("click", collapseSidebarToggle, false);
8488 tag.addEventListener("mouseup", collapseSidebarDefaultToggle.bind(null, name), false);
8489 tag.addEventListener("contextmenu", disableEvent, false);
8490
8491 if (collapse && sibling)
8492 sibling.bbbAddClass("bbb-collapsed-sidebar");
8493 }
8494 }
8495
8496 // Clean up potential old section names.
8497 for (i in data) {
8498 if (data.hasOwnProperty(i)) {
8499 if (nameList.indexOf(i.bbbSpacePad()) < 0) {
8500 removedOld = true;
8501 delete data[i];
8502 }
8503 }
8504 }
8505
8506 if (removedOld) {
8507 loadSettings();
8508 bbb.user.collapse_sidebar_data[dataLoc] = data;
8509 saveSettings();
8510 }
8511 }
8512
8513 function collapseSidebarToggle(event) {
8514 // Collapse/expand a sidebar section.
8515 var target = event.target;
8516 var sibling = target.nextElementSibling;
8517
8518 if (event.button !== 0 || !sibling)
8519 return;
8520
8521 if (sibling.bbbHasClass("bbb-collapsed-sidebar"))
8522 sibling.bbbRemoveClass("bbb-collapsed-sidebar");
8523 else
8524 sibling.bbbAddClass("bbb-collapsed-sidebar");
8525
8526 event.preventDefault();
8527 }
8528
8529 function collapseSidebarDefaultToggle(name, event) {
8530 // Make a sidebar section expand/collapse by default.
8531 if (event.button !== 2)
8532 return;
8533
8534 var dataLoc = (gLoc === "post" ? "post" : "thumb");
8535 var data = collapse_sidebar_data[dataLoc];
8536 var collapse = data[name];
8537
8538 loadSettings();
8539
8540 if (collapse) {
8541 delete bbb.user.collapse_sidebar_data[dataLoc][name];
8542 delete data[name];
8543 }
8544 else
8545 bbb.user.collapse_sidebar_data[dataLoc][name] = data[name] = true;
8546
8547 saveSettings();
8548
8549 bbbNotice("The \"" + name + "\" section will now " + (!collapse ? "collapse" : "expand") + " by default.", 3);
8550
8551 event.preventDefault();
8552 }
8553
8554 function allowUserLimit() {
8555 // Allow use of the user thumbnail limit on the first page if there isn't a search limit and the current limit doesn't equal the user limit.
8556 var page = Number(getVar("page")) || 1; // When set to 0 or undefined, the first page is shown.
8557 var queryLimit = getQueryLimit();
8558 var searchLimit = getSearchLimit();
8559 var limit = (queryLimit !== undefined ? queryLimit : searchLimit) || thumbnail_count_default;
8560 var allowedLoc = (gLoc === "search" || gLoc === "notes" || gLoc === "favorites");
8561
8562 if (allowedLoc && thumbnail_count && thumbnail_count !== limit && page === 1 && (searchLimit === undefined || queryLimit !== undefined))
8563 return true;
8564 else
8565 return false;
8566 }
8567
8568 function noResultsPage(pageEl) {
8569 // Check whether a page has zero results on it.
8570 var target = pageEl || document.body;
8571 var numPosts = getPosts(target).length;
8572 var thumbContainer = getThumbContainer(gLoc, target) || target;
8573 var thumbContainerText = (thumbContainer ? thumbContainer.textContent : "");
8574
8575 if (!numPosts && thumbContainerText.indexOf("Nobody here but us chickens") > -1)
8576 return true;
8577 else
8578 return false;
8579 }
8580
8581 function danbLoc(url) {
8582 // Test a URL to find which section of Danbooru the script is running on.
8583 var target; // If/else variable.
8584
8585 if (url) {
8586 target = document.createElement("a");
8587 target.href = url;
8588 }
8589 else
8590 target = location;
8591
8592 var path = target.pathname;
8593 var query = target.search;
8594
8595 if (/\/posts\/\d+/.test(path))
8596 return "post";
8597 else if (/^\/(?:posts|$)/.test(path))
8598 return "search";
8599 else if (/^\/notes\/?$/.test(path) && query.indexOf("group_by=note") < 0)
8600 return "notes";
8601 else if (/\/comments\/\d+/.test(path))
8602 return "comment";
8603 else if (/^\/comments\/?$/.test(path)) {
8604 if (query.indexOf("group_by=comment") < 0)
8605 return "comments";
8606 else // This may need to be more specific in the future.
8607 return "comment_search";
8608 }
8609 else if (/\/explore\/posts\/popular(?:\/?$|\?)/.test(path))
8610 return "popular";
8611 // else if (/\/explore\/posts\/popular_view(?:\/?$|\?)/.test(path))
8612 // return "popular_view";
8613 else if (/\/pools\/\d+(?:\/?$|\?)/.test(path))
8614 return "pool";
8615 else if (/\/favorite_groups\/\d+(?:\/?$|\?)/.test(path))
8616 return "favorite_group";
8617 else if (/\/pools\/gallery/.test(path))
8618 return "pool_gallery";
8619 else if (path.indexOf("/favorites") === 0)
8620 return "favorites";
8621 else if (path.indexOf("/uploads/new") === 0)
8622 return "upload";
8623 else if (path.indexOf("/pools/new") === 0)
8624 return "new_pool";
8625 else if (/\/forum_topics\/\d+/.test(path))
8626 return "topic";
8627 else if (path.indexOf("/explore/posts/intro") === 0)
8628 return "intro";
8629 else
8630 return undefined;
8631 }
8632
8633 function isLoggedIn() {
8634 // Use Danbooru's meta tags to determine if a use is logged in.
8635 if (getMeta("current-user-id") !== "")
8636 return true;
8637 else
8638 return false;
8639 }
8640
8641 function noXML() {
8642 // Don't use XML requests on certain pages where it won't do any good.
8643 var limit = getLimit();
8644 var pageNum = getVar("page");
8645 var paginator = getPaginator();
8646 var thumbContainer = getThumbContainer(gLoc);
8647 var imgContainer = getPostContent().container;
8648
8649 if (!paginator && !thumbContainer && !imgContainer)
8650 return true;
8651 else if (gLoc === "search" || gLoc === "favorites") {
8652 if (limit === 0 || pageNum === "b1" || noResultsPage() || safebSearchTest())
8653 return true;
8654 }
8655 else if (gLoc === "notes") {
8656 if (limit === 0 || noResultsPage())
8657 return true;
8658 }
8659 else if (gLoc === "comments") {
8660 if (pageNum === "b1" || noResultsPage())
8661 return true;
8662 }
8663 else if (gLoc === "pool" || gLoc === "favorite_group" || gLoc === "popular" || gLoc === "popular_view") {
8664 if (noResultsPage())
8665 return true;
8666 }
8667
8668 return false;
8669 }
8670
8671 function useAPI() {
8672 // Determine whether any options that require the API are enabled.
8673 if ((show_loli || show_shota || show_toddlercon || (show_deleted && !deleted_shown) || show_banned) && (isLoggedIn() || !bypass_api))
8674 return true;
8675 else
8676 return false;
8677 }
8678
8679 function isRandomSearch() {
8680 // Check whether the search uses "order:random" in it.
8681 return ((getTagVar("order") || "").toLowerCase() === "random");
8682 }
8683
8684 function removeInheritedStorage(key) {
8685 // Remove an inherited sessionStorage key for a new tab/window.
8686 if (window.opener && history.length === 1) {
8687 var state = history.state || {};
8688 var stateProperty = key + "_reset";
8689
8690 if (!state[stateProperty]) {
8691 sessionStorage.removeItem(key);
8692 state[stateProperty] = true;
8693 history.replaceState(state, "");
8694 }
8695 }
8696 }
8697
8698 function accountSettingCheck(scriptSetting) {
8699 // Determine whether the script setting or account/anonymous setting should be used.
8700 var loggedIn = isLoggedIn();
8701 var setting; // If/else variable.
8702
8703 if (scriptSetting === "script_blacklisted_tags") {
8704 if ((loggedIn && override_blacklist === "always") || (!loggedIn && override_blacklist !== "disabled"))
8705 setting = bbb.user.script_blacklisted_tags;
8706 else
8707 setting = getMeta("blacklisted-tags") || "";
8708 }
8709 else if (scriptSetting === "post_resize") {
8710 if (loggedIn && !override_resize)
8711 setting = (getMeta("always-resize-images") === "true");
8712 else
8713 setting = bbb.user.post_resize;
8714 }
8715 else if (scriptSetting === "load_sample_first") {
8716 if (loggedIn && !override_sample)
8717 setting = (getMeta("default-image-size") === "large");
8718 else
8719 setting = bbb.user.load_sample_first;
8720 }
8721
8722 return setting;
8723 }
8724
8725 function safebPostTest(post) {
8726 // Test Safebooru's posts to see if they're censored or not.
8727 if (location.host.indexOf("safebooru") < 0)
8728 return false;
8729
8730 var postObject = post;
8731
8732 // If dealing with an element, turn it into a simple post info object.
8733 if (postObject instanceof HTMLElement) {
8734 if (postObject.tagName === "ARTICLE" || postObject.id === "image-container") {
8735 var tags = postObject.getAttribute("data-tags");
8736 var rating = postObject.getAttribute("data-rating");
8737
8738 postObject = {rating: rating, tag_string: tags};
8739 }
8740 }
8741
8742 // Posts with an explicit/questionable rating or censored tags are bad.
8743 if (post.rating !== "s" || safebCensorTagTest(post.tag_string))
8744 return true;
8745 else
8746 return false;
8747 }
8748
8749 function safebSearchTest() {
8750 // Test Safebooru's current search tags to see if there will be no results.
8751 if (location.host.indexOf("safebooru") < 0)
8752 return false;
8753
8754 var tags = getVar("tags");
8755 var i, il, tag; // Loop variables.
8756
8757 if (!tags)
8758 return false;
8759
8760 tags = decodeURIComponent(tags.replace(/(\+|%20)/g, " ")).split(" ");
8761
8762 // Split up the "any" (~) tags and "all" tags for testing.
8763 var allTags = "";
8764 var anyTags = [];
8765
8766 for (i = 0, il = tags.length; i < il; i++) {
8767 tag = tags[i];
8768
8769 if (tag.charAt(0) === "~")
8770 anyTags.push(tag.slice(1));
8771 else
8772 allTags += " " + tag;
8773 }
8774
8775 // If any one "all" tag is censored, all posts will be bad.
8776 if (safebCensorTagTest(allTags))
8777 return true;
8778
8779 // If any one "any" tag isn't censored, not all posts will be bad.
8780 var anyMatch = !!anyTags[0];
8781
8782 for (i = 0, il = anyTags.length; i < il; i++) {
8783 tag = anyTags[i];
8784
8785 if (!safebCensorTagTest(tag)) {
8786 anyMatch = false;
8787 break;
8788 }
8789 }
8790
8791 return anyMatch;
8792 }
8793
8794 function safebCensorTagTest(string) {
8795 // Test a tag string or search string on Safebooru to see if it contains any bad tags.
8796 if (typeof(string) !== "string")
8797 return false;
8798 else
8799 return /(?:^|\s)(?:toddlercon|toddler|diaper|tentacle|rape|bestiality|beastiality|lolita|loli|nude|shota|pussy|penis|-rating:s\S+|rating:(?:e|q)\S+)(?:$|\s)/i.test(string);
8800 }
8801
8802 function searchAdd() {
8803 // Choose the appropriate search link option to run.
8804 if (search_add === "disabled" || (gLoc !== "search" && gLoc !== "post" && gLoc !== "favorites"))
8805 return;
8806
8807 searchAddRemove();
8808
8809 if (search_add === "link")
8810 searchAddLink();
8811 else if (search_add === "toggle")
8812 searchAddToggleLink();
8813 }
8814
8815 function searchAddRemove() {
8816 // Completely remove existing + and - tag links along with the whitespace after them.
8817 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
8818
8819 if (!tagList)
8820 return;
8821
8822 var addLinks = tagList.getElementsByClassName("search-inc-tag");
8823 var subLinks = tagList.getElementsByClassName("search-exl-tag");
8824 var blankRegEx = /^\s*$/;
8825
8826 while (addLinks[0]) {
8827 var addLink = addLinks[0];
8828 var addSibling = addLink.nextSibling;
8829 var addParent = addSibling.parentNode;
8830
8831 if (addSibling && addSibling.nodeType === 3 && blankRegEx.test(addSibling.nodeValue))
8832 addParent.removeChild(addSibling);
8833
8834 addParent.removeChild(addLink);
8835 }
8836
8837 while (subLinks[0]) {
8838 var subLink = subLinks[0];
8839 var subSibling = subLink.nextSibling;
8840 var subParent = subSibling.parentNode;
8841
8842 if (subSibling && subSibling.nodeType === 3 && blankRegEx.test(subSibling.nodeValue))
8843 subParent.removeChild(subSibling);
8844
8845 subParent.removeChild(subLink);
8846 }
8847 }
8848
8849 function searchAddLink() {
8850 // Add + and - links to the sidebar tag list for modifying searches.
8851 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
8852
8853 if (!tagList)
8854 return;
8855
8856 var tagItems = tagList.getElementsByTagName("li");
8857 var curTag = getCurTags();
8858 var curTagString = (curTag ? "+" + curTag : "");
8859
8860 for (var i = 0, il = tagItems.length; i < il; i++) {
8861 var tagItem = tagItems[i];
8862 var tagLink = tagItem.getElementsByClassName("search-tag")[0];
8863 var tagString = getVar("tags", tagLink.href);
8864 var tagFrag = document.createDocumentFragment();
8865
8866 var addTag = document.createElement("a");
8867 addTag.href = "/posts?tags=" + tagString + curTagString;
8868 addTag.innerHTML = "+";
8869 addTag.className = "search-inc-tag";
8870 tagFrag.appendChild(addTag);
8871
8872 var addSpace = document.createTextNode(" ");
8873 tagFrag.appendChild(addSpace);
8874
8875 var subTag = document.createElement("a");
8876 subTag.href = "/posts?tags=-" + tagString + curTagString;
8877 subTag.innerHTML = "–";
8878 subTag.className = "search-exl-tag";
8879 tagFrag.appendChild(subTag);
8880
8881 var subSpace = document.createTextNode(" ");
8882 tagFrag.appendChild(subSpace);
8883
8884 tagItem.insertBefore(tagFrag, tagLink);
8885 }
8886 }
8887
8888 function searchAddToggleLink() {
8889 // Add toggle links to the sidebar tag list for modifying the search box value.
8890 var tagList = document.getElementById("tag-box") || document.getElementById("tag-list");
8891
8892 if (!tagList)
8893 return;
8894
8895 var tagItems = tagList.getElementsByTagName("li");
8896 var firstItem = tagItems[0];
8897 var toggleWidth; // If/else variable.
8898
8899 // Find a set width for the toggle link.
8900 if (firstItem) {
8901 var testItem = document.createElement("li");
8902 testItem.className = "category-0";
8903 testItem.style.height = "0px";
8904 testItem.style.visibility = "hidden";
8905
8906 var testLink = document.createElement("a");
8907 testLink.href = "#";
8908 testLink.style.display = "inline-block";
8909 testItem.appendChild(testLink);
8910
8911 firstItem.parentNode.appendChild(testItem);
8912
8913 testLink.innerHTML = "-";
8914 var subWidth = testLink.clientWidth;
8915
8916 testLink.innerHTML = "+";
8917 var addWidth = testLink.clientWidth;
8918
8919 testLink.innerHTML = "~";
8920 var orWidth = testLink.clientWidth;
8921
8922 toggleWidth = Math.max(subWidth, addWidth, orWidth);
8923
8924 firstItem.parentNode.removeChild(testItem);
8925 }
8926
8927 // Create and insert the toggle links.
8928 for (var i = 0, il = tagItems.length; i < il; i++) {
8929 var tagItem = tagItems[i];
8930 var tagLink = tagItem.getElementsByClassName("search-tag")[0];
8931 var tagString = decodeURIComponent(getVar("tags", tagLink.href));
8932 var tagFrag = document.createDocumentFragment();
8933 var tagFunc = searchAddToggle.bind(null, tagString);
8934
8935 var toggleTag = document.createElement("a");
8936 toggleTag.href = "/posts?tags=" + tagString;
8937 toggleTag.innerHTML = "»";
8938 toggleTag.style.display = "inline-block";
8939 toggleTag.style.textAlign = "center";
8940 toggleTag.style.width = toggleWidth + "px";
8941 toggleTag.addEventListener("click", tagFunc, false);
8942 toggleTag.addEventListener("mouseup", tagFunc, false);
8943 toggleTag.addEventListener("contextmenu", disableEvent, false);
8944 tagFrag.appendChild(toggleTag);
8945
8946 var toggleSpace = document.createTextNode(" ");
8947 tagFrag.appendChild(toggleSpace);
8948
8949 tagItem.insertBefore(tagFrag, tagLink);
8950 bbb.search_add.links[tagString] = toggleTag;
8951 }
8952
8953 // Watch various actions on the search box.
8954 var tagsInput = document.getElementById("tags");
8955
8956 if (tagsInput && (gLoc === "search" || gLoc === "post" || gLoc === "intro" || gLoc === "favorites")) {
8957 searchAddToggleCheck();
8958 tagsInput.addEventListener("input", searchAddToggleCheck, false);
8959 tagsInput.addEventListener("keyup", searchAddToggleCheck, false);
8960 tagsInput.addEventListener("cut", searchAddToggleCheck, false);
8961 tagsInput.addEventListener("paste", searchAddToggleCheck, false);
8962 tagsInput.addEventListener("change", searchAddToggleCheck, false);
8963 $(tagsInput).on("autocompleteselect", function(event) { delayMe(function(event) { searchAddToggleCheck(event); }); }); // Delayed to allow autocomplete to change the input.
8964 }
8965 }
8966
8967 function searchAddToggleCheck(event) {
8968 // Watch the search box value, test it upon changes, and update the tag links accordingly.
8969 var input = (event ? event.target : document.getElementById("tags"));
8970 var value = input.value;
8971 var oldValue = bbb.search_add.old;
8972 var i, il; // Loop variables.
8973
8974 if (oldValue !== value) {
8975 var tags = value.toLowerCase().bbbSpaceClean().split(/\s+/);
8976 var activeLinks = bbb.search_add.active_links;
8977
8978 for (i in activeLinks) {
8979 if (activeLinks.hasOwnProperty(i)) {
8980 var activeRegEx = new RegExp("(?:^|\\s)[-~]*" + escapeRegEx(i) + "(?:$|\\s)", "gi");
8981
8982 if (!activeRegEx.test(value))
8983 activeLinks[i].innerHTML = "»";
8984 }
8985 }
8986
8987 for (i = 0, il = tags.length; i < il; i++) {
8988 var tag = tags[i];
8989 var tagChar = tag.charAt(0);
8990 var tagType = "+";
8991
8992 if (tagChar === "-" || tagChar === "~") {
8993 tagType = (tagChar === "-" ? "–" : tagChar);
8994 tag = tag.slice(1);
8995 }
8996
8997 var tagLink = bbb.search_add.links[tag];
8998
8999 if (tagLink) {
9000 bbb.search_add.links[tag].innerHTML = tagType;
9001 bbb.search_add.active_links[tag] = tagLink;
9002 }
9003 }
9004
9005 bbb.search_add.old = value;
9006 }
9007 }
9008
9009 function searchAddToggle(tag, event) {
9010 // Modify the search box value based upon the tag link clicked and the tag's current state.
9011 var link = event.target;
9012 var button = event.button;
9013 var type = event.type;
9014 var linkType = link.innerHTML;
9015 var input = document.getElementById("tags");
9016 var inputValue = input.value;
9017 var tagRegEx = new RegExp("(^|\\s)[-~]*" + escapeRegEx(tag) + "(?=$|\\s)", "gi");
9018 var angleQuotes = String.fromCharCode(187);
9019 var enDash = String.fromCharCode(8211);
9020
9021 if ((type === "mouseup" && button !== 2) || (type === "click" && button !== 0)) // Don't respond to middle click and filter out duplicate user actions.
9022 return;
9023
9024 if (button === 2) // Immediately remove the tag upon a right click.
9025 linkType = "~";
9026
9027 // Each case changes the tag's toggle link display and updates the search box.
9028 switch (linkType) {
9029 case angleQuotes: // Tag currently not present.
9030 link.innerHTML = "+";
9031
9032 if (tagRegEx.test(inputValue))
9033 input.value = inputValue.replace(tagRegEx, tag).bbbSpaceClean();
9034 else
9035 input.value = (inputValue + " " + tag).bbbSpaceClean();
9036 break;
9037 case "+": // Tag currently included.
9038 link.innerHTML = "–";
9039
9040 if (tagRegEx.test(inputValue))
9041 input.value = inputValue.replace(tagRegEx, "$1-" + tag).bbbSpaceClean();
9042 else
9043 input.value = (inputValue + " -" + tag).bbbSpaceClean();
9044 break;
9045 case enDash: // Tag currently excluded.
9046 link.innerHTML = "~";
9047
9048 if (tagRegEx.test(inputValue))
9049 input.value = inputValue.replace(tagRegEx, "$1~" + tag).bbbSpaceClean();
9050 else
9051 input.value = (inputValue + " ~" + tag).bbbSpaceClean();
9052 break;
9053 case "~": // Tag currently included with other tags.
9054 link.innerHTML = angleQuotes;
9055
9056 if (tagRegEx.test(inputValue))
9057 input.value = inputValue.replace(tagRegEx, "$1").bbbSpaceClean();
9058 break;
9059 }
9060
9061 event.preventDefault();
9062 }
9063
9064 function localStorageDialog() {
9065 // Open a dialog box for cleaning out local storage for donmai.us.
9066 if (getCookie().bbb_ignore_storage)
9067 return;
9068
9069 var domains = [
9070 {url: "http://danbooru.donmai.us/", untrusted: false},
9071 {url: "https://danbooru.donmai.us/", untrusted: false},
9072 {url: "http://donmai.us/", untrusted: false},
9073 {url: "https://donmai.us/", untrusted: true},
9074 {url: "http://sonohara.donmai.us/", untrusted: false},
9075 {url: "https://sonohara.donmai.us/", untrusted: true},
9076 {url: "http://hijiribe.donmai.us/", untrusted: false},
9077 {url: "https://hijiribe.donmai.us/", untrusted: true},
9078 {url: "http://safebooru.donmai.us/", untrusted: false},
9079 {url: "https://safebooru.donmai.us/", untrusted: false},
9080 {url: "http://testbooru.donmai.us/", untrusted: false}
9081 ];
9082
9083 var content = document.createDocumentFragment();
9084
9085 var header = document.createElement("h2");
9086 header.innerHTML = "Local Storage Error";
9087 header.className = "bbb-header";
9088 content.appendChild(header);
9089
9090 var introText = document.createElement("div");
9091 introText.innerHTML = "While trying to save some settings, BBB has detected that your browser's local storage is full for the donmai.us domain and was unable to automatically fix the problem. In order for BBB to function properly, the storage needs to be cleaned out.<br><br> BBB can cycle through the various donmai locations and clear out Danbooru's autocomplete cache and BBB's thumbnail info cache for each. Please select the domains/subdomains you'd like to clean from below and click OK to continue. If you click cancel, BBB will ignore the storage problems for the rest of this browsing session, but features may not work as expected.<br><br> <b>Notes:</b><ul><li>Three options in the domain list are not selected by default (marked as untrusted) since they require special permission from the user to accept invalid security certificates. However, if BBB detects you're already on one of these untrusted domains, then it will be automatically selected.</li><li>If you encounter this warning again right after storage has been cleaned, you may have to check domains you didn't check before or use the \"delete everything\" option to clear items in local storage besides autocomplete and thumbnail info.</li></ul><br> <b>Donmai.us domains/subdomains:</b><br>";
9092 content.appendChild(introText);
9093
9094 var domainDiv = document.createElement("div");
9095 domainDiv.style.lineHeight = "1.5em";
9096 content.appendChild(domainDiv);
9097
9098 var cbFunc = function(event) {
9099 if (event.button !== 0)
9100 return;
9101
9102 var target = event.target;
9103
9104 target.nextSibling.style.textDecoration = (target.checked ? "none" : "line-through");
9105 };
9106
9107 for (var i = 0, il = domains.length; i < il; i++) {
9108 var domain = domains[i];
9109 var isChecked = (!domain.untrusted || location.href.indexOf(domain.url) > -1);
9110
9111 var listCheckbox = document.createElement("input");
9112 listCheckbox.name = domain.url;
9113 listCheckbox.type = "checkbox";
9114 listCheckbox.checked = isChecked;
9115 listCheckbox.style.marginRight = "5px";
9116 listCheckbox.addEventListener("click", cbFunc, false);
9117 domainDiv.appendChild(listCheckbox);
9118
9119 var listLink = document.createElement("a");
9120 listLink.innerHTML = domain.url + (domain.untrusted ? " (untrusted)" : "");
9121 listLink.href = domain.url;
9122 listLink.target = "_blank";
9123 listLink.style.textDecoration = (isChecked ? "none" : "line-through");
9124 domainDiv.appendChild(listLink);
9125
9126 var br = document.createElement("br");
9127 domainDiv.appendChild(br);
9128 }
9129
9130 var optionsText = document.createElement("div");
9131 optionsText.innerHTML = "<b>Options:</b><br>";
9132 optionsText.style.marginTop = "1em";
9133 content.appendChild(optionsText);
9134
9135 var optionsDiv = document.createElement("div");
9136 optionsDiv.style.lineHeight = "1.5em";
9137 content.appendChild(optionsDiv);
9138
9139 var compCheckbox = document.createElement("input");
9140 compCheckbox.name = "complete-delete";
9141 compCheckbox.type = "checkbox";
9142 compCheckbox.style.marginRight = "5px";
9143 optionsDiv.appendChild(compCheckbox);
9144
9145 var compText = document.createTextNode("Delete everything in local storage for each selection except for my BBB settings.");
9146 optionsDiv.appendChild(compText);
9147
9148 var okFunc = function() {
9149 var options = domainDiv.getElementsByTagName("input");
9150 var mode = (compCheckbox.checked ? "complete" : "normal");
9151 var selectedURLs = [];
9152 var origURL = location.href;
9153 var cleanCur = false;
9154 var session = new Date().getTime();
9155 var nextURL; // Loop variable.
9156
9157 for (var i = 0, il = options.length; i < il; i++) {
9158 var option = options[i];
9159
9160 if (option.checked) {
9161 if (origURL.indexOf(option.name) === 0)
9162 cleanCur = true;
9163 else if (!nextURL)
9164 nextURL = option.name;
9165 else
9166 selectedURLs.push(encodeURIComponent(option.name));
9167 }
9168 }
9169
9170 // Clean the current domain if it was selected.
9171 if (cleanCur)
9172 cleanLocalStorage(mode);
9173
9174 if (!nextURL) {
9175 // Retry saving if only the current domain was selected and do nothing if no domains were selected.
9176 if (cleanCur)
9177 retryLocalStorage();
9178 }
9179 else {
9180 // Start cycling through domains.
9181 bbbDialog("Currently cleaning local storage and loading the next domain. Please wait...", {ok: false, important: true});
9182 sessionStorage.bbbSetItem("bbb_local_storage_queue", JSON.stringify(bbb.local_storage_queue));
9183 location.href = updateURLQuery(nextURL + "posts/1/", {clean_storage: mode, clean_urls: selectedURLs.join(","), clean_origurl: encodeURIComponent(origURL), clean_session: session});
9184 }
9185 };
9186
9187 var cancelFunc = function() {
9188 createCookie("bbb_ignore_storage", 1);
9189 };
9190
9191 bbbDialog(content, {ok: okFunc, cancel: cancelFunc});
9192 }
9193
9194 function cleanLocalStorage(mode) {
9195 // Clean out various values in local storage.
9196 var i, keyName; // Loop variables.
9197
9198 if (mode === "autocomplete") {
9199 for (i = localStorage.length - 1; i >= 0; i--) {
9200 keyName = localStorage.key(i);
9201
9202 if (keyName.indexOf("ac-") === 0)
9203 localStorage.removeItem(keyName);
9204 }
9205 }
9206 else if (mode === "normal") {
9207 for (i = localStorage.length - 1; i >= 0; i--) {
9208 keyName = localStorage.key(i);
9209
9210 if (keyName.indexOf("ac-") === 0 || keyName === "bbb_thumb_cache")
9211 localStorage.removeItem(keyName);
9212 }
9213 }
9214 else if (mode === "complete") {
9215 for (i = localStorage.length - 1; i >= 0; i--) {
9216 keyName = localStorage.key(i);
9217
9218 if (keyName !== "bbb_settings")
9219 localStorage.removeItem(keyName);
9220 }
9221 }
9222 }
9223
9224 function retryLocalStorage() {
9225 // Try to save items to local storage that failed to get saved before.
9226 var sessLocal; // If/else variable.
9227
9228 if (sessionStorage.getItem("bbb_local_storage_queue")) {
9229 // Retrieve the local storage values from session storage after cycling through other domains.
9230 sessLocal = JSON.parse(sessionStorage.getItem("bbb_local_storage_queue"));
9231 sessionStorage.removeItem("bbb_local_storage_queue");
9232 }
9233 else if (bbb.local_storage_queue) {
9234 // If only the BBB storage object exists, assume the user selected to only clean the current domain and reset things.
9235 sessLocal = bbb.local_storage_queue;
9236 delete bbb.local_storage_queue;
9237 delete bbb.flags.local_storage_full;
9238 }
9239 else
9240 return;
9241
9242 for (var i in sessLocal) {
9243 if (sessLocal.hasOwnProperty(i))
9244 localStorage.bbbSetItem(i, sessLocal[i]);
9245 }
9246
9247 bbbNotice("Local storage cleaning has completed.", 6);
9248 }
9249
9250 function localStorageCheck() {
9251 // Check if the script is currently trying to manage local storage.
9252 var cleanMode = getVar("clean_storage");
9253 var cleanSession = Number(getVar("clean_session")) || 0;
9254 var session = new Date().getTime();
9255
9256 // Stop if the script is not currently cleaning storage or if an old URL is detected.
9257 if (!cleanMode || Math.abs(session - cleanSession) > 60000)
9258 return;
9259
9260 if (cleanMode !== "save") {
9261 // Cycle through the domains.
9262 var urls = getVar("clean_urls").split(",");
9263 var nextURL = urls.shift();
9264 var origURL = getVar("clean_origurl");
9265
9266 bbb.flags.local_storage_full = true; // Keep the cycled domains from triggering storage problems
9267 bbbDialog("Currently cleaning local storage and loading the next domain. Please wait...", {ok: false, important: true});
9268 history.replaceState(history.state, "", updateURLQuery(location.href, {clean_storage: undefined, clean_urls: undefined, clean_origurl: undefined, clean_session: undefined}));
9269 cleanLocalStorage(cleanMode);
9270
9271 if (nextURL)
9272 window.setTimeout(function() { location.href = updateURLQuery(decodeURIComponent(nextURL) + "posts/1/", {clean_storage: cleanMode, clean_urls: urls.join(), clean_origurl: origURL, clean_session: session}); }, 2000);
9273 else
9274 window.setTimeout(function() { location.href = updateURLQuery(decodeURIComponent(origURL), {clean_storage: "save", clean_session: session}); }, 2000);
9275 }
9276 else if (cleanMode === "save") {
9277 history.replaceState(history.state, "", updateURLQuery(location.href, {clean_storage: undefined, clean_session: undefined}));
9278 retryLocalStorage();
9279 }
9280 }
9281
9282 function getCookie() {
9283 // Return an associative array with cookie values.
9284 var data = document.cookie;
9285
9286 if(!data)
9287 return false;
9288
9289 data = data.split("; ");
9290 var out = [];
9291
9292 for (var i = 0, il = data.length; i < il; i++) {
9293 var temp = data[i].split("=");
9294 out[temp[0]] = temp[1];
9295 }
9296
9297 return out;
9298 }
9299
9300 function createCookie(cName, cValue, expDays) {
9301 // Generate a cookie with a expiration time in days.
9302 var data = cName + "=" + cValue + "; path=/";
9303
9304 if (expDays !== undefined) {
9305 var expDate = new Date();
9306 expDate.setTime(expDate.getTime() + expDays * 86400000);
9307 expDate.toUTCString();
9308 data += "; expires=" + expDate;
9309 }
9310
9311 document.cookie = data;
9312 }
9313
9314 function scrollbarWidth() {
9315 // Retrieve the scrollbar width by creating an element with scrollbars and finding the difference.
9316 var scroller = document.createElement("div");
9317 scroller.style.width = "150px";
9318 scroller.style.height = "150px";
9319 scroller.style.visibility = "hidden";
9320 scroller.style.overflow = "scroll";
9321 scroller.style.position = "absolute";
9322 scroller.style.top = "0px";
9323 scroller.style.left = "0px";
9324 document.body.appendChild(scroller);
9325 var scrollDiff = scroller.offsetWidth - scroller.clientWidth;
9326 document.body.removeChild(scroller);
9327
9328 return scrollDiff;
9329 }
9330
9331 function bbbIsNum(value) {
9332 // Strictly test for a specific style of number.
9333 return /^-?\d+(\.\d+)?$/.test(value);
9334 }
9335
9336 function isNumMetatag(tag) {
9337 // Check if the tag from a search string is a numeric metatag.
9338 if (tag.indexOf(":") < 0)
9339 return false;
9340 else {
9341 var tagName = tag.split(":", 1)[0];
9342
9343 if (tagName === "score" || tagName === "favcount" || tagName === "id" || tagName === "width" || tagName === "height")
9344 return true;
9345 else
9346 return false;
9347 }
9348 }
9349
9350 function isMetatag(tag) {
9351 // Check if the tag from a search string is a metatag.
9352 if (tag.indexOf(":") < 0)
9353 return false;
9354 else {
9355 var tagName = tag.split(":", 1)[0].bbbSpaceClean();
9356
9357 if (tagName === "pool" || tagName === "user" || tagName === "status" || tagName === "rating" || tagName === "parent" || tagName === "child")
9358 return true;
9359 else
9360 return false;
9361 }
9362 }
9363
9364 function delayMe(func) {
9365 // Run the function after the browser has finished its current stack of tasks.
9366 window.setTimeout(func, 0);
9367 }
9368
9369 function escapeRegEx(regEx) {
9370 // Replace special characters with escaped versions to make them safe for RegEx.
9371 return regEx.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
9372 }
9373
9374 function updateURLQuery(url, newQueries) {
9375 // Update the query portion of a URL. If a param isn't declared, it will be added. If it is, it will be updated.
9376 // Assigning undefined to a param that exists will remove it. Assigning null to a param that exists will completely remove its value. Assigning null to a new param will leave it with no assigned value.
9377 var urlParts = url.split("#", 1)[0].split("?", 2);
9378 var urlQuery = urlParts[1] || "";
9379 var queries = urlQuery.split("&");
9380 var queryObj = {};
9381 var i, il, query, queryName, queryValue; // Loop variables.
9382
9383 for (i = 0, il = queries.length; i < il; i++) {
9384 query = queries[i].split("=");
9385 queryName = query[0];
9386 queryValue = query[1];
9387
9388 if (queryName)
9389 queryObj[queryName] = queryValue;
9390 }
9391
9392 for (i in newQueries) {
9393 if (newQueries.hasOwnProperty(i))
9394 queryObj[i] = newQueries[i];
9395 }
9396
9397 queries.length = 0;
9398
9399 for (i in queryObj) {
9400 if (queryObj.hasOwnProperty(i)) {
9401 queryValue = queryObj[i];
9402
9403 if (queryValue === null) // Declared param with no assigned value.
9404 query = i;
9405 else if (queryValue === undefined) // Undeclared.
9406 query = undefined;
9407 else // Declared param with an assigned value (including empty srings).
9408 query = i + "=" + queryValue;
9409
9410 if (query !== undefined) // Omit undefined params.
9411 queries.push(query);
9412 }
9413 }
9414
9415 urlQuery = queries.join("&");
9416
9417 return (urlParts[0] + (urlQuery ? "?" + urlQuery : ""));
9418 }
9419
9420 function timestamp(format) {
9421 // Returns a simple timestamp based on the format string provided. String placeholders: y = year, m = month, d = day, hh = hours, mm = minutes, ss = seconds
9422 function padDate(number) {
9423 // Adds a leading "0" to single digit values.
9424 var numString = String(number);
9425
9426 if (numString.length === 1)
9427 numString = "0" + numString;
9428
9429 return numString;
9430 }
9431
9432 var stamp = format || "y-m-d hh:mm:ss";
9433 var time = new Date();
9434 var year = time.getFullYear();
9435 var month = padDate(time.getMonth() + 1);
9436 var day = padDate(time.getDate());
9437 var hours = padDate(time.getHours());
9438 var minutes = padDate(time.getMinutes());
9439 var seconds = padDate(time.getSeconds());
9440
9441 stamp = stamp.replace("hh", hours).replace("mm", minutes).replace("ss", seconds).replace("y", year).replace("m", month).replace("d", day);
9442
9443 return stamp;
9444 }
9445
9446 function isOldVersion(ver) {
9447 // Takes the provided version and compares it to the script version. Returns true if the provided version is older than the script version.
9448 var userVer = ver || bbb.user.bbb_version;
9449 var scriptVer = bbb.options.bbb_version;
9450 var userNums = userVer.split(".");
9451 var userLength = userNums.length;
9452 var scriptNums = scriptVer.split(".");
9453 var scriptLength = scriptNums.length;
9454 var loopLength = (userLength > scriptLength ? userLength : scriptLength);
9455
9456 for (var i = 0; i < loopLength; i++) {
9457 var userNum = (userNums[i] ? Number(userNums[i]) : 0);
9458 var scriptNum = (scriptNums[i] ? Number(scriptNums[i]) : 0);
9459
9460 if (userNum < scriptNum)
9461 return true;
9462 else if (scriptNum < userNum)
9463 return false;
9464 }
9465
9466 return false;
9467 }
9468
9469 function uniqueIdNum() {
9470 // Return a unique ID number for an element.
9471 if (!bbb.uId)
9472 bbb.uId = 1;
9473 else
9474 bbb.uId++;
9475
9476 return "bbbuid" + bbb.uId;
9477 }
9478
9479} // End of bbbScript.
9480
9481function runBBBScript() {
9482 // Run the script or prep it to run when Danbooru's JS is ready.
9483 if (document.readyState === "interactive" && typeof(Danbooru) === "undefined") {
9484 var danbScripts = document.getElementsByTagName("script");
9485
9486 for (var i = 0, il = danbScripts.length; i < il; i++) {
9487 var curScript = danbScripts[i];
9488 var curScriptSrc = curScript.src || "";
9489
9490 if (curScriptSrc.indexOf("donmai.us/assets/application-") > -1) {
9491 curScript.addEventListener("load", bbbScript, true);
9492 break;
9493 }
9494 }
9495 }
9496 else
9497 bbbScript();
9498}
9499
9500function testBBBAccess() {
9501 // Check whether the script has access to the page.
9502 if (!document.body)
9503 return;
9504
9505 function testFunc() {
9506 window.bbb_access = true;
9507 }
9508
9509 var testScript = document.createElement('script');
9510 testScript.type = "text/javascript";
9511 testScript.appendChild(document.createTextNode('(' + testFunc + ')();'));
9512 document.body.appendChild(testScript);
9513 window.setTimeout(function() { document.body.removeChild(testScript); }, 0);
9514
9515 if (!window.bbb_access) { // Embed the script since it can't get to Danbooru's JS.
9516 var script = document.createElement('script');
9517 script.type = "text/javascript";
9518 script.appendChild(document.createTextNode(bbbScript));
9519 script.appendChild(document.createTextNode('(' + runBBBScript + ')();'));
9520 document.body.appendChild(script);
9521 }
9522 else // Operate normally.
9523 runBBBScript();
9524}
9525
9526testBBBAccess();