├── .codeclimate.yml
├── .gitignore
├── .travis.yml
├── Gruntfile.js
├── LICENSE
├── README.md
├── package.json
├── src
├── cs.js
└── cs.min.js
└── test
├── cs-test.js
└── index.html
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | languages:
2 | JavaScript: true
3 | exclude_paths:
4 | - "Gruntfile.js"
5 | - "LICENSE"
6 | - "package.json"
7 | - "README.md"
8 | - "src/cs.min.js"
9 | - "test/*"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | build
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4.1"
4 | before_script:
5 | - npm install grunt-cli -g
6 | after_script:
7 | - npm install codeclimate-test-reporter
8 | - codeclimate-test-reporter < ./coverage/lcov.info
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Many thanks to Shadi Abu Hilal for his demonstration repository:
3 | * https://github.com/shadiabuhilal/js-code-coverage-example
4 | * Testing client-side JavaScript is a lot more difficult than
5 | * testing Node.js applications.
6 | */
7 | 'use strict';
8 |
9 | var istanbul = require('browserify-istanbul');
10 |
11 | module.exports = function(grunt) {
12 | grunt.loadNpmTasks('grunt-browserify');
13 | grunt.loadNpmTasks('grunt-contrib-clean');
14 | grunt.loadNpmTasks('grunt-contrib-uglify');
15 | grunt.loadNpmTasks('grunt-mocha-phantom-istanbul');
16 |
17 | grunt.initConfig({
18 | pkg: grunt.file.readJSON('package.json'),
19 |
20 | browserify: {
21 | options: {
22 | browserifyOptions: {
23 | debug: true
24 | },
25 | postBundleCB: function(err, buffer, next) {
26 | var code = grunt.template.process(buffer.toString(), {
27 | data: grunt.file.readJSON('package.json')
28 | });
29 | next(err, code);
30 | }
31 | },
32 | coverage: {
33 | files: {
34 | 'build/cs.js': 'src/cs.js'
35 | },
36 | options: {
37 | transform: [istanbul]
38 | }
39 | }
40 | },
41 |
42 | clean: {
43 | tests: ['build', 'coverage']
44 | },
45 |
46 | mocha: {
47 | options: {
48 | run: true,
49 | reporter: 'Spec',
50 | coverage: {
51 | lcovReport: 'coverage'
52 | }
53 | },
54 | test: {
55 | src: ['test/**/*.html']
56 | }
57 | },
58 |
59 | uglify: {
60 | options: {
61 | preserveComments: /(?:^!|@(?:license|preserve|cc_on))/
62 | },
63 | build: {
64 | src: 'src/cs.js',
65 | dest: 'src/cs.min.js'
66 | }
67 | }
68 | });
69 |
70 | grunt.registerTask('default', ['uglify', 'clean', 'browserify:coverage', 'mocha']);
71 | };
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2012-2016 Connor Wiseman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jcink Custom Structure
2 | A DOM manipulation library specifically designed for the Jcink hosted forum service.
3 |
4 | [](https://travis-ci.org/ConnorWiseman/jcink-custom-structure) [](https://codeclimate.com/github/ConnorWiseman/jcink-custom-structure/coverage) [](https://codeclimate.com/github/ConnorWiseman/jcink-custom-structure) [](https://github.com/ConnorWiseman/jcink-custom-structure/blob/master/LICENSE)
5 |
6 |
7 | ## Table of Contents
8 | 1. [Installation](#installation)
9 | 2. [Configuration](#configuration)
10 | 1. [Execution Timers](#execution-timers)
11 | 3. [Custom Index](#custom-index)
12 | 1. [Custom Index Configuration Reference](#custom-index-configuration-reference)
13 | 2. [Custom Index Key Reference](#custom-index-key-reference)
14 | 3. [Custom Index Output Reference](#custom-index-output-reference)
15 | 4. [Custom Statistics](#custom-statistics)
16 | 1. [Custom Statistics Configuration Reference](#custom-statistics-configuration-reference)
17 | 2. [Custom Statistics Key Reference](#custom-statistics-key-reference)
18 | 3. [Custom Statistics Output Reference](#custom-statistics-output-reference)
19 | 5. [Custom Profile](#custom-profile)
20 | 1. [Custom Profile Configuration Reference](#custom-profile-configuration-reference)
21 | 2. [Custom Profile Key Reference](#custom-profile-key-reference)
22 | 1. [Custom Profile Fields](#custom-profile-fields)
23 | 3. [Custom Profile Output Reference](#custom-profile-output-reference)
24 | 6. [Custom Topics](#custom-topics)
25 | 1. [Custom Topics Configuration Reference](#custom-topics-configuration-reference)
26 | 2. [Custom Topics Key Reference](#custom-topics-key-reference)
27 | 3. [Custom Topics Output Reference](#custom-topics-output-reference)
28 | 7. [Custom Posts](#custom-posts)
29 | 1. [Custom Posts Configuration Reference](#custom-posts-configuration-reference)
30 | 2. [Custom Posts Key Reference](#custom-posts-key-reference)
31 | 3. [Custom Posts Output Reference](#custom-posts-output-reference)
32 | 4. [Quick Edit Addon](#quick-edit-addon)
33 | 5. [Formatted Quote/Code Tags Addon](#formatted-quotecode-tags-addon)
34 | 1. [Formatted Quote/Code Tags Output Reference](#formatted-quotecode-tags-output-reference)
35 | 8. [Advanced Usage](#advanced-usage)
36 | 1. [String and Function Comparison](#string-and-function-comparison)
37 | 2. [hasValue Method](#hasvalue-method)
38 | 3. [getValue Method](#getValue-method)
39 | 4. [Example](#example)
40 |
41 |
42 | ## Installation
43 | Host a copy of [cs.min.js](https://github.com/ConnorWiseman/jcink-custom-structure/blob/master/src/cs.min.js) and place it in the page header inside your Board Wrappers.
44 | ```html
45 |
46 |
47 |
48 | ```
49 | A hosted copy is available for free courtesy of Elegant Expressions:
50 | * [http://elegantexpressions.us/black/cs.min.js](http://elegantexpressions.us/black/cs.min.js)
51 |
52 |
53 | ## Configuration
54 | Configuration is handled on a per-module basis by passing in an optional object, `config`, as a property in each module's `initialize` method argument. For reference, all objects used by the Custom Structure script are [object literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#Creating_objects).
55 |
56 | Here, a brief usage demonstration is provided to point users in the right direction.
57 | ```html
58 | <% BOARD %>
59 |
68 | ```
69 | The accepted configuration properties are detailed for each module in their respective sections below.
70 |
71 | ### Execution Timers
72 | Each module also accepts an optional `time` property that, if set to true, will log the execution time of the module to the browser console.
73 | ```html
74 | <% BOARD %>
75 |
81 | ```
82 |
83 |
84 | ## Custom Index
85 | Reads each category on the forum index, each category inside a subcategory, and the list of subforums inside a forum for important values, then performs replacement and insertion.
86 | ```html
87 | <% BOARD %>
88 |
93 | ```
94 |
95 |
96 | ### Custom Index Configuration Reference
97 | |Property|Description|Default|
98 | |--------|-----------|-------|
99 | |`target`|The `id` attribute of the `<% BOARD %>` wrapper tag's container element. As noted in the changes section, the `target` configuration property is no longer required but including it can provide a minor boost to performance. Consider using it to be an official recommendation.|`board`|
100 | |`keyPrefix`|The default prefix for replacement keys.|`{{`|
101 | |`keySuffix`|The default suffix for replacement keys.|`}}`|
102 | |`insertBefore`|Content to be inserted before a new category. Does nothing if left blank.|Blank.|
103 | |`insertAfter`|Content to be inserted after a new category. Does nothing if left blank.|Blank.|
104 | |`viewingDefault`|The default number of people viewing a given forum([X members are viewing](http://jcink.com/main/wiki/jfb-acp-system-settings#cpu_saving) must be enabled).|`0`|
105 | |`subforumSeparator`|The default subforum separator.|`, `|
106 | |`subforumsNone`|The default indicator for no subforums.|Blank.|
107 | |`moderatorsNone`|The default indicator for no moderators.|Blank.|
108 | |`dateDefault`|The default date for last posts.|`--`|
109 | |`titleDefault`|The default title for last posts.|`----`|
110 | |`urlDefault`|The default URL for last posts.|`#`|
111 | |`authorDefault`|The default author for last posts.|Blank.|
112 | |`passwordTitle`|The default title of topics in password-protected forums.|`Protected Forum`|
113 |
114 |
115 | ### Custom Index Key Reference
116 | **Note:** The keys will be different if the `keyPrefix` or `keySuffix` configuration properties have been overridden with user-defined values.
117 |
118 | |Key|Description|
119 | |---|-----------|
120 | |`{{forumMarker}}`|The forum's marker, including the "Mark this forum as read?" link if available.|
121 | |`{{forumTitle}}`|A link to the forum, containing the forum's title.|
122 | |`{{forumViewing}}`|The number of people viewing the forum.|
123 | |`{{forumId}}`|The forum's numerical id.|
124 | |`{{forumDescription}}`|The forum's description.|
125 | |`{{subforums}}`|The list of subforums the forum contains.|
126 | |`{{moderators}}`|The list of users and user groups assigned to moderate the forum.|
127 | |`{{topicCount}}`|The number of topics in the forum.|
128 | |`{{replyCount}}`|The number of replies in the forum.|
129 | |`{{redirectHits}}`|The number of hits a redirect forum has received.|
130 | |`{{lastPostDate}}`|The date of the last post in the forum.|
131 | |`{{lastPostTitle}}`|A link to the last post in the forum, containing the title of the topic the last post was made in.|
132 | |`{{lastPostURL}}`|The URL pointing to the last post made in the forum.|
133 | |`{{lastPostAuthor}}`|A link to the author of the last post in the forum if available; otherwise, a string containing the name of the guest who made the post.|
134 |
135 |
136 | ### Custom Index Output Reference
137 | ```html
138 |
139 |
140 |
Category Title
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
153 |
154 |
155 |
156 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 | ```
170 |
171 |
172 | ## Custom Statistics
173 | Reads the forum statistics for important values, then performs replacement and insertion.
174 | ```html
175 | <% BOARD %>
176 |
181 | ```
182 |
183 |
184 | ### Custom Statistics Configuration Reference
185 | |Property|Description|Default|
186 | |--------|-----------|-------|
187 | |`keyPrefix`|The default prefix for replacement keys.|`{{`|
188 | |`keySuffix`|The default suffix for replacement keys.|`}}`|
189 |
190 |
191 | ### Custom Statistics Key Reference
192 | **Note:** The keys will be different if the `keyPrefix` or `keySuffix` configuration properties have been overridden with user-defined values.
193 |
194 | |Key|Description|
195 | |---|-----------|
196 | |`{{totalUsers}}`|The total number of users online.|
197 | |`{{totalUsersGuests}}`|The number of guests online.|
198 | |`{{totalUsersRegistered}}`|The number of registered users online.|
199 | |`{{totalUsersAnonymous}}`|The number of anonymous users online.|
200 | |`{{onlineList}}`|A comma-separated list of online users.|
201 | |`{{onlineLegend}}`|The built-in [user group legend](http://jcink.com/main/wiki/member_legend).|
202 | |`{{activityLinkClick}}`|A link to the online list, sorted by last click.|
203 | |`{{activityLinkMemberName}}`|A link to the online list, sorted by member name.|
204 | |`{{birthdays}}`|The number of birthdays being celebrated today.|
205 | |`{{birthdaysList}}`|A list of users celebrating their birthday today, including their age.|
206 | |`{{events}}`|A list of events being observed.|
207 | |`{{totalPosts}}`|The total number of posts on the forum.|
208 | |`{{totalMembers}}`|The total number of members on the forum.|
209 | |`{{newestMember}}`|A link to the newest member.|
210 | |`{{mostOnline}}`|The most users ever online at once.|
211 | |`{{mostOnlineDate}}`|The date when the most users were on.|
212 | |`{{onlineToday}}`|The number of users online today.|
213 | |`{{onlineTodayList}}`|A comma-separated list of the users who have been online today.|
214 | |`{{mostOnlineOneDay}}`|The most users ever online in one day.|
215 | |`{{mostOnlineDateOneDay}}`|The date when the most users ever online in one day occurred.|
216 | |`{{storeProducts}}`|The number of products in the store.|
217 | |`{{storeValue}}`|The total value of all the products in the store.|
218 | |`{{moneyTotal}}`|The total amount of money on the forum.|
219 | |`{{moneyBanked}}`|The amount of money in the forum's bank.|
220 | |`{{moneyCirculating}}`|The amount of money outside the forum's bank.|
221 | |`{{richestMember}}`|A link to the wealthiest member.|
222 | |`{{richestMemberValue}}`|The value of the wealthiest member.|
223 |
224 |
225 | ### Custom Statistics Output Reference
226 | ```html
227 |
228 |
229 |
Board Statistics
230 |
231 |
232 |
236 |
237 |
238 |
239 | ```
240 |
241 |
242 | ## Custom Profile
243 | Reads a user profile page for important values, then performs replacement and insertion. The Custom Profile module reads both the default IPB profile and the personal portal style profiles the same way.
244 | ```html
245 | <% BOARD %>
246 |
251 | ```
252 |
253 |
254 | ### Custom Profile Configuration Reference
255 | |Property|Description|Default|
256 | |--------|-----------|-------|
257 | |`htmlEnabled`|If `true`, Custom Structure will parse the user's interests field for HTML and output it accordingly. Although the Jcink service does not parse the interests field for malicious code on its own, the parsing method used here is [XSS-safe](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet#RULE_.236_-_Populate_the_DOM_using_safe_JavaScript_functions_or_properties). Script and style tags in the interests field will not affect the page.|`false`|
258 | |`keyPrefix`|The default prefix for replacement keys.|`{{`|
259 | |`keySuffix`|The default suffix for replacement keys.|`}}`|
260 | |`emailDefault`|The default text used in the user's email link.|`Click here`|
261 | |`messageDefault`|The default text used in the user's personal message link.|`Click here`|
262 | |`reputationIncrease`|The default text used in the user's reputation increase link.|`+`|
263 | |`reputationDecrease`|The default text used in the user's reputation decrease link.|`-`|
264 | |`reputationDetails`|The default text used in the user's reputation details link.|`[details >>]`|
265 | |`warnIncrease`|The default text used in the user's warning increase link.|`+`|
266 | |`warnDecrease`|The default text used in the user's warning decrease link.|`-`|
267 | |`avatarDefault`|An image URL to use in place of the user's avatar if the user does not have an avatar.|Blank.|
268 | |`userPhotoDefault`|An image URL to use in place of the user's photo if the user does not have a photo.|Blank.|
269 | |`onlineActivityDefault`|The default text used to describe a user's online activity when they are currently offline.|Blank.|
270 | |`customFieldsInnerHTML`|If `true`, derives the values of custom profile fields via `innerHTML` (slower) as opposed to the default `textContent` (faster).|`false`|
271 |
272 |
273 | ### Custom Profile Key Reference
274 | **Note:** The keys will be different if the `keyPrefix` or `keySuffix` configuration properties have been overridden with user-defined values.
275 |
276 | |Key|Description|
277 | |---|-----------|
278 | |`{{userId}}`|The user's numerical id.|
279 | |`{{userPhoto}}`|The URL of the user's photo. Not an image.|
280 | |`{{userName}}`|The user's name.|
281 | |`{{postCount}}`|The user's post count.|
282 | |`{{postsPerDay}}`|The number of posts made per day.|
283 | |`{{joinDate}}`|The date the user joined.|
284 | |`{{localTime}}`|The user's local time.|
285 | |`{{onlineStatus}}`|Online or offline.|
286 | |`{{onlineActivity}}`|If the user is online, what they were doing last.|
287 | |`{{sendEmail}}`|A link to send the user an email.|
288 | |`{{userSkype}}`|The user's Skype.|
289 | |`{{userAIM}}`|The user's AIM.|
290 | |`{{userGtalk}}`|The user's Gtalk.|
291 | |`{{userYahoo}}`|The user's YIM.|
292 | |`{{userMSN}}`|The user's MSN.|
293 | |`{{sendMessage}}`|A link to send the user a personal message.|
294 | |`{{homePage}}`|A link to the user's home page.|
295 | |`{{birthday}}`|The user's birthday.|
296 | |`{{location}}`|The user's location.|
297 | |`{{interests}}`|The user's interests. May contain HTML.|
298 | |`{{lastActivity}}`|The time when the user was last active.|
299 | |`{{userGroup}}`|The user's group name.|
300 | |`{{userTitle}}`|The user's member title.|
301 | |`{{avatar}}`|The URL of the user's avatar. Not an image.|
302 | |`{{reputationTotal}}`|The user's reputation count.|
303 | |`{{reputationIncrease}}`|A link to increase the user's reputation.|
304 | |`{{reputationDecrease}}`|A link to decrease the user's reputation.|
305 | |`{{reputationDetails}}`|A link to the user's reputation details.|
306 | |`{{warnLevel}}`|The user's warn level.|
307 | |`{{warnLevelIncrease}}`|A link to increase the user's warn level.|
308 | |`{{warnLevelDecrease}}`|A link to decrease the user's warn level.|
309 | |`{{signature}}`|The user's signature.|
310 |
311 |
312 | #### Custom Profile Fields
313 | Custom Structure cannot read the database that powers the Jcink service and it is not aware which custom profile fields are assigned which id internally. Instead, it reads them in the order they appear on the user's profile. Therefore, hiding certain custom profile fields from everyone but staff members will have an impact on the appearance of your custom profiles. If you intend to use custom profile fields please keep this in mind.
314 |
315 | |Key|Description|
316 | |---|-----------|
317 | |`{{customFieldn}}`|The nth custom profile field visible on the user's profile.|
318 |
319 | ```html
320 | <% BOARD %>
321 |
326 | ```
327 |
328 |
329 | ### Custom Profile Output Reference
330 | ```html
331 |
332 |
333 |
334 |
335 |
336 |
340 |
341 |
342 |
343 | ```
344 |
345 |
346 | ## Custom Topics
347 | Reads both topic lists inside forums and the active topics search page for important values, then performs replacement and insertion.
348 | ```html
349 | <% BOARD %>
350 |
355 | ```
356 |
357 |
358 | ### Custom Topics Configuration Reference
359 | |Property|Description|Default|
360 | |--------|-----------|-------|
361 | |`keyPrefix`|The default prefix for replacement keys.|`{{`|
362 | |`keySuffix`|The default suffix for replacement keys.|`}}`|
363 | |`announcementsDefault`|The default text used for the announcements row.|`Announcements`|
364 | |`pinnedDefault`|The default text used for the pinned topics row.|`Important Topics`|
365 | |`regularDefault`|The default text used for the regular topics row.|`Forum Topics`|
366 | |`noTopics`|The text displayed when a forum contaions no topics.|`No topics were found. This is either because there are no topics in this forum, or the topics are older than the current age cut-off.`|
367 | |`noActiveTopics`|The text displayed when the active topics list is blank.|`There were no active topics during those date ranges`|
368 | |`paginationDefault`|The text displayed when there are no pagination options.|Blank.|
369 | |`activeTopics`|Whether to apply changes the to the active topics page.|`false`|
370 |
371 |
372 | ### Custom Topics Key Reference
373 | **Note:** The keys will be different if the `keyPrefix` or `keySuffix` configuration properties have been overridden with user-defined values.
374 |
375 | |Key|Description|
376 | |---|-----------|
377 | `{{folder}}`|The topic's folder (new replies, locked, etc).|
378 | `{{marker}}`|The topic's marker (pinned).|
379 | `{{topicId}}`|The topic's numerical id.|
380 | `{{topicTitle}}`|A link to the topic, including the topic's title.|
381 | `{{pagination}}`|The topic's pagination links.|
382 | `{{topicDescription}}`|The topic's description.|
383 | `{{topicAuthor}}`|A link to the topic's author.|
384 | `{{replyCount}}`|The number of replies to this topic.|
385 | `{{viewCount}}`|The number of views this topic has received.|
386 | `{{parentForum}}`|A link to the forum that contains this topic.|
387 | `{{lastReplyDate}}`|The date of the last reply to this topic.|
388 | `{{lastReplyAuthor}}`|A link to the author of the last reply to this topic.|
389 | `{{moderatorCheckbox}}`|The checkbox used for moderating this topic. Invisible to regular members and guests. If you forget to include it somewhere, the topic moderation options form below the topic list will be useless.|
390 |
391 |
392 | ### Custom Topics Output Reference
393 | ```html
394 |
395 |
396 |
Forum Title
397 |
430 | ...
431 |
432 | ```
433 |
434 |
435 | ## Custom Posts
436 | Reads the posts in a topic for important values, then performs replacement and insertion. The Custom Posts module is new in this release.
437 | ```html
438 | <% BOARD %>
439 |
444 | ```
445 |
446 |
447 | ### Custom Posts Configuration Reference
448 | |Property|Description|Default|
449 | |--------|-----------|-------|
450 | |`keyPrefix`|The default prefix for replacement keys.|`{{`|
451 | |`keySuffix`|The default suffix for replacement keys.|`}}`|
452 | |`permaLinkDefault`|The default text used in post permalinks.|`Permalink`|
453 | |`postSignatureDefault`|The default text used for signatures.|Blank.|
454 | |`quickEdit`|Enables or disables the script's built-in quick edit addon.|`false`|
455 | |`formatQuoteCodeTags`|Enables or disables the script's built-in quote/code tag formatting.|`false`|
456 |
457 |
458 | ### Custom Posts Key Reference
459 | **Note:** The keys will be different if the `keyPrefix` or `keySuffix` configuration properties have been overridden with user-defined values.
460 |
461 | |Key|Description|
462 | |---|-----------|
463 | |`{{postId}}`|The post's numerical id.|
464 | |`{{postAuthor}}`|A link to the post's author, or a string with the name of the guest who made the post.|
465 | |`{{permaLink}}`|A permanent link to the post.|
466 | |`{{postDate}}`|The date the post was made.|
467 | |`{{postButtonsTop}}`|The buttons above the post.|
468 | |`{{postCheckbox}}`|The checkbox used for moderating this post. Invisible to regular members and guests. If you forget to include it somewhere, the topic moderation options form below the topic list will be useless.|
469 | |`{{postMiniprofile}}`|The miniprofile to the left of the post.|
470 | |`{{postContent}}`|The main post content.|
471 | |`{{postSignature}}`|The signature below the post.|
472 | |`{{postIp}}`|The IP address associated with the post. Invisible to regular members and guests.|
473 | |`{{postButtonsBottom}}`|The buttons below the post.|
474 |
475 |
476 | ### Custom Posts Output Reference
477 | ```html
478 |
479 |
480 |
Topic Title
481 |
482 |
483 |
484 |
485 |
490 |
491 |
492 |
493 |
# User(s) are reading this topic
494 |
495 |
496 |
497 |
498 | ```
499 |
500 | ### Quick Edit Addon
501 | To enable the quick edit addon packaged with this script, [disable the default quick edit feature](http://forum.jcink.com/index.php?showtopic=24795) and enable the addon through `config` by setting it to `true`.
502 | ```html
503 | <% BOARD %>
504 |
512 | ```
513 |
514 | **Note:** Enabling the quick edit addon will automatically encapsulate the contents of `{{postContent}}` inside a `
` with class name `cs-quick-edit`. The additional element may interfere with the appearance of your forum if you are styling your posts using [CSS combinators](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors#Combinators). Be aware of the potential need to slightly change some CSS rules prior to enabling the quick edit addon.
515 |
516 |
517 | ### Formatted Quote/Code Tags Addon
518 | To enable the formatted quote/code tags addon packaged with this script, enable the addon through `config` by setting it to `true`.
519 | ```html
520 | <% BOARD %>
521 |
529 | ```
530 |
531 | #### Formatted Quote/Code Tags Output Reference
532 | ```html
533 |
534 |
535 | Username
536 | Date and time
537 |
538 |
539 |
Quote goes here
540 |
541 |
542 |
543 |
544 |
Code (Click to highlight)
545 |
546 |
Code goes here
547 |
548 |
549 | ```
550 |
551 | ## Advanced Usage
552 | The `html` property passed to any of the modules can be either a string or a function that returns a string. Although the use of a function will provide additional flexibility, they are somewhat slower. This performance overhead is compounded in the Custom Index, Custom Topics, and Custom Posts modules because they perform multiple reads and inserts. Functions are useful when more fine-tuned behavior is desired, since pure JavaScript can be added to introduce user-defined behavior based on the values provided by the modules.
553 |
554 | ### String and Function Comparison
555 | The following two examples will produce equivalent output.
556 | ```html
557 | <% BOARD %>
558 |
563 | ```
564 |
565 | ```html
566 | <% BOARD %>
567 |
574 | ```
575 |
576 | ### hasValue Method
577 | Each module includes a built-in method for checking whether a specified value exists if users wish to define additional behavior or should the default defaulting options prove restrictive. The argument passed to `hasValue` should not include `keySuffix` or `keyPrefix`.
578 | ```html
579 | <% BOARD %>
580 |
598 | ```
599 |
600 | ### getValue Method
601 | Each module also includes a method for retrieving the value of a specific key for direct manipulation. The argument passed to `getValue` should not include `keySuffix` or `keyPrefix`. The following two examples will produce equivalent output.
602 | ```html
603 | <% BOARD %>
604 |
615 | ```
616 |
617 | ```html
618 | <% BOARD %>
619 |
635 | ```
636 |
637 | ### Example
638 | Because the internal values are all strings, they can be manipulated via [JavaScript's string methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#String_instances) once they are retrieved.
639 | ```html
640 | <% BOARD %>
641 |
653 | ```
654 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jcink-custom-structure",
3 | "version": "2.0.0",
4 | "description": "A DOM manipulation library specifically designed for the Jcink hosted forum service.",
5 | "scripts": {
6 | "test": "grunt"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/ConnorWiseman/jcink-custom-structure.git"
11 | },
12 | "author": "Connor Wiseman",
13 | "license": "MIT",
14 | "bugs": {
15 | "url": "https://github.com/ConnorWiseman/jcink-custom-structure/issues"
16 | },
17 | "homepage": "https://github.com/ConnorWiseman/jcink-custom-structure#readme",
18 | "devDependencies": {
19 | "browserify-istanbul": "^2.0.0",
20 | "chai": "^3.5.0",
21 | "grunt": "^0.4.5",
22 | "grunt-browserify": "^5.0.0",
23 | "grunt-cli": "^0.1.13",
24 | "grunt-contrib-clean": "^1.0.0",
25 | "grunt-contrib-uglify": "^1.0.1",
26 | "grunt-mocha-phantom-istanbul": "^0.2.1",
27 | "istanbul": "^0.4.2",
28 | "mocha": "^2.4.5"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/cs.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | * A DOM manipilation utility library for the version of IPB running on the
4 | * free forum hosting service, Jcink, that reads table information and accepts
5 | * a user-defined template for text replacement. Allows for the structuring
6 | * of forums in nontraditional, table-less layouts. Visible credits are not
7 | * required provided this entire comment block remains intact.
8 | * @author Connor Wiseman
9 | * @copyright 2012-2016 Connor Wiseman
10 | * @version 1.8.2 (September 2016)
11 | * @license
12 | * Copyright (c) 2012-2016 Connor Wiseman
13 | *
14 | * Permission is hereby granted, free of charge, to any person obtaining a copy
15 | * of this software and associated documentation files (the 'Software'), to deal
16 | * in the Software without restriction, including without limitation the rights
17 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 | * copies of the Software, and to permit persons to whom the Software is
19 | * furnished to do so, subject to the following conditions:
20 | *
21 | * The above copyright notice and this permission notice shall be included in
22 | * all copies or substantial portions of the Software.
23 | *
24 | * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30 | * THE SOFTWARE.
31 | */
32 |
33 | // Enforce strict interpretation for compatibility reasons.
34 | 'use strict';
35 |
36 |
37 | /**
38 | * @namespace
39 | */
40 | var $cs = $cs || {
41 | /**
42 | * Extends the prototype of a child object given a parent object to
43 | * inherit from.
44 | * @arg {object} child
45 | * @arg {object} parent
46 | * @readonly
47 | */
48 | extendModule: function(child, parent) {
49 | child.prototype = new parent;
50 | child.prototype.constructor = child.prototype.myParent;
51 | },
52 |
53 |
54 | /**
55 | * @namespace
56 | * @property {object} module
57 | * @property {object} module.Default
58 | * @property {object} module.Index
59 | * @property {object} module.Stats
60 | * @property {object} module.Profile
61 | * @property {object} module.Topics
62 | * @property {object} module.Posts
63 | * @readonly
64 | */
65 | module: {
66 | Default: function() {},
67 | Index: function() {},
68 | Stats: function() {},
69 | Profile: function() {},
70 | Topics: function() {},
71 | Posts: function() {}
72 | }
73 | };
74 |
75 |
76 | /**
77 | * @property {boolean} time - Whether or not to run performance timers on script execution.
78 | */
79 | $cs.module.Default.prototype.time = false;
80 |
81 |
82 | /**
83 | * @property {string} html - User-defined HTML markup for replacement.
84 | */
85 | $cs.module.Default.prototype.html = '';
86 |
87 |
88 | /**
89 | * @property {object} values - Script-defined keys mapped to user-defined values for replacement.
90 | */
91 | $cs.module.Default.prototype.values = {};
92 |
93 |
94 | /**
95 | * Returns whether a given element contains the specified class name.
96 | * @arg {object} el
97 | * @arg {string} class
98 | * @return {boolean}
99 | * @link http://stackoverflow.com/a/5898748/2301088
100 | */
101 |
102 |
103 |
104 | /**
105 | * Retrieves the value of the specified key from the existing values.
106 | * @arg {string} key - The name of the key to retrieve.
107 | * @return {string} - The value associated with the key.
108 | * @readonly
109 | */
110 | $cs.module.Default.prototype.getValue = function(key) {
111 | var key = this.config.keyPrefix + key + this.config.keySuffix;
112 | return this.values[key];
113 | };
114 |
115 |
116 | /**
117 | * Checks the existing values for the presence of a specified key.
118 | * @arg {string} key - The name of the key to check for.
119 | * @return {boolean} - True if value exists, false otherwise.
120 | * @readonly
121 | */
122 | $cs.module.Default.prototype.hasValue = function(key) {
123 | var key = this.config.keyPrefix + key + this.config.keySuffix;
124 | return (this.values[key] && this.values[key] !== '');
125 | };
126 |
127 |
128 | /**
129 | * Initialization function. Reads user-defined settings in for processing and begins script execution.
130 | * @arg {object} settings - An object with user-defined settings as properties.
131 | * @readonly
132 | */
133 | $cs.module.Default.prototype.initialize = function(settings) {
134 | // Make sure we have an object to work with.
135 | settings = settings || {};
136 |
137 | // If we have an empty settings object, display an error message and return false.
138 | if (!Object.keys(settings).length) {
139 | console.error(this.name + ': init method missing required argument "settings"');
140 | return false;
141 | }
142 |
143 | /*
144 | For each key in our settings object, if it's not in the list of reserved names,
145 | overwrite the properties (and methods, I suppose) in the module.
146 | */
147 | for (var key in settings) {
148 | if (this.reserved.indexOf(key) === -1) {
149 | // Go one level deeper if one of the properties is an object.
150 | if (typeof settings[key] == 'object') {
151 | for (var subkey in settings[key]) {
152 | if (key in this) {
153 | this[key][subkey] = settings[key][subkey];
154 | }
155 | }
156 | } else if (key in this) {
157 | this[key] = settings[key];
158 | }
159 | }
160 | }
161 |
162 | /*
163 | If this.html isn't null or empty, execute the script. Otherwise, display an
164 | error message and return false.
165 | */
166 | if (this.html && this.html !== '') {
167 | // Execution timers.
168 | if (this.time) {
169 | console.time(this.name);
170 | }
171 |
172 | // Reinitialize the values object so it's blank for the next pass.
173 | this.values = {};
174 | this.execute();
175 |
176 | // Execution timers.
177 | if (this.time) {
178 | console.timeEnd(this.name);
179 | }
180 | } else {
181 | console.error(this.name + ': required property "html" is undefined');
182 | return false;
183 | }
184 | };
185 |
186 |
187 | /**
188 | * String replacement function.
189 | * @arg {*} string|object - A text string, or function that returns a text string, for replacement.
190 | * @arg {object} object - An object of keys and values to use during replacement.
191 | * @return {string}
192 | * @readonly
193 | */
194 | $cs.module.Default.prototype.replaceValues = function(string, object) {
195 | string = (typeof string == 'function') ? string.call(this) : string;
196 | if (typeof string == 'undefined') {
197 | console.error(this.name + ': function "html" returns null');
198 | return null;
199 | }
200 | // Join the keys with the pipe character for regular expression matching.
201 | var regex = new RegExp(Object.keys(object).join('|'), 'g');
202 | // Find and replace the keys with their associated values, then return the string.
203 | return string.replace(regex, function(matched) {
204 | return object[matched];
205 | });
206 | };
207 |
208 |
209 | /**
210 | * Sets a specified key to a specified value.
211 | * @arg {string} key - The key to set.
212 | * @arg {*} value - The value to be set.
213 | * @readonly
214 | */
215 | $cs.module.Default.prototype.setValue = function(key, value) {
216 | this.values[this.config.keyPrefix + key + this.config.keySuffix] = value;
217 | }
218 |
219 |
220 | // Extend the custom index module with the default properties and methods.
221 | $cs.extendModule($cs.module.Index, $cs.module.Default);
222 |
223 |
224 | /**
225 | * @namespace
226 | * @property {object} config - Default configuration values.
227 | * @property {string} config.target - The default container element.
228 | * @property {string} config.keyPrefix - The default prefix for value keys.
229 | * @property {string} config.keySuffix - The default suffix for value keys.
230 | * @property {string} config.insertBefore - The default content to be inserted before a new category.
231 | * @property {string} config.insertAfter - The default content to be inserted after a new category.
232 | * @property {string} config.subforumSeparator - The default subforum separator.
233 | * @property {string} config.subforumsNone - The default indicator for no subforums.
234 | * @property {string} config.moderatorsNone - The default indicator for no moderators.
235 | * @property {string} config.dateDefault - The default date for last posts.
236 | * @property {string} config.titleDefault - The default title for last posts.
237 | * @property {string} config.urlDefault - The default URL for last posts.
238 | * @property {string} config.authorDefault - The default author for last posts.
239 | * @property {string} config.passwordTitle - The default title of topics in password-protected forums.
240 | */
241 | $cs.module.Index.prototype.config = {
242 | target: 'board',
243 | keyPrefix: '{{',
244 | keySuffix: '}}',
245 | insertBefore: '',
246 | insertAfter: '',
247 | viewingDefault: '0',
248 | subforumSeparator: ', ',
249 | subforumsNone: '',
250 | moderatorsNone: '',
251 | dateDefault: '--',
252 | titleDefault: '----',
253 | urlDefault: '#',
254 | authorDefault: '',
255 | passwordTitle: 'Protected Forum'
256 | };
257 |
258 |
259 | /**
260 | * @property {string} name - The name of this module.
261 | */
262 | $cs.module.Index.prototype.name = '$cs.module.Index';
263 |
264 |
265 | /**
266 | * @property {object} reserved - An array of reserved names.
267 | */
268 | $cs.module.Index.prototype.reserved = [
269 | 'values',
270 | 'makeLink',
271 | 'execute',
272 | 'getValue',
273 | 'hasValue',
274 | 'initialize',
275 | 'readTable',
276 | 'replaceValues',
277 | 'setValue'
278 | ];
279 |
280 |
281 | /**
282 | * Executes the checks and loops needed to complete the script.
283 | * @readonly
284 | */
285 | $cs.module.Index.prototype.execute = function() {
286 | /*
287 | Acquire the container, if one is specified, and then see which kind
288 | of page we're looking at.
289 | */
290 | var container = document.getElementById(this.config.container),
291 | subforumlist = document.getElementById('subforum-list'),
292 | categories = (container || document).getElementsByClassName('category');
293 | if (categories.length) {
294 | /*
295 | If there are categories, we're looking at the forum index.
296 | Loop through each category's table individually.
297 | */
298 | for (var i = 0; i < categories.length; i++) {
299 | var category = categories[i].lastChild.previousSibling,
300 | table = category.firstElementChild;
301 | this.readTable(table, i)
302 | }
303 | } else if (subforumlist) {
304 | /*
305 | If there's a subforumlist, we're inside a forum with only
306 | one category and table to deal with. No looping necessary.
307 | */
308 | var table = subforumlist.firstElementChild;
309 | this.readTable(table, 0);
310 | }
311 | };
312 |
313 |
314 | /**
315 | * Initialization function. Reads user-defined settings in for processing and begins script execution.
316 | * @arg {object} settings - An object with user-defined settings as properties.
317 | * @readonly
318 | */
319 | $cs.module.Index.prototype.initialize = function(settings) {
320 | // Call $cs.module.Default's initialize method instead.
321 | $cs.module.Default.prototype.initialize.call(this, settings);
322 | };
323 |
324 |
325 | /**
326 | * Converts an anchor element to flat HTML markup.
327 | * @arg {object} element
328 | * @return {string}
329 | * @readonly
330 | */
331 | $cs.module.Index.prototype.makeLink = function(element) {
332 | return '' + element.innerHTML + '<\/a>';
333 | };
334 |
335 |
336 | /**
337 | * Reads the rows from a supplied table, generates a new category from the user-defined HTML template, and
338 | * injects the new category into the page.
339 | * @arg {object} table - A reference to an HTML table object.
340 | * @arg {number} index - The numerical index of the current category.
341 | * @readonly
342 | */
343 | $cs.module.Index.prototype.readTable = function(table, index) {
344 | // Acquire all the rows in the table.
345 | var rows = table.getElementsByTagName('tr');
346 |
347 | // Temporarily hide the table. It will be removed altogether later on.
348 | table.style.display = 'none';
349 |
350 | // Create a variable to store the HTML output of the following loop.
351 | var categoryContent = '';
352 |
353 | // Add any content intended to be inserted before every category.
354 | if (this.config.insertBefore && this.config.insertBefore !== '') {
355 | categoryContent += '
';
1249 | break;
1250 | }
1251 | } else {
1252 | if (!viewingActiveTopics) {
1253 | // This forum contains no topics. Display a message and call it good.
1254 | topicsContent += '
' + this.config.noTopics + '
';
1255 | } else if (this.config.activeTopics) {
1256 | // This active topics list is blank. Display a message and call it good.
1257 | topicsContent += '
' + this.config.noActiveTopics + '
';
1258 | }
1259 | }
1260 | }
1261 |
1262 | // Create a new HTML element, set the appropriate attributes, and inject it into the page.
1263 | var newTopics = document.createElement('div');
1264 | newTopics.id = 'new-topics';
1265 | newTopics.innerHTML = topicsContent;
1266 | table.parentNode.insertBefore(newTopics, table);
1267 |
1268 | table.parentNode.removeChild(table);
1269 | // Hide that last, useless search element down below.
1270 | if (this.config.activeTopics && viewingActiveTopics) {
1271 | topicList.removeChild(topicList.lastElementChild);
1272 | }
1273 | }
1274 | }
1275 |
1276 |
1277 | /**
1278 | * Initialization function. Reads user-defined settings in for processing and begins script execution.
1279 | * @arg {object} settings - An object with user-defined settings as properties.
1280 | * @readonly
1281 | */
1282 | $cs.module.Topics.prototype.initialize = function(settings) {
1283 | // Call $cs.module.Default's initialize method instead.
1284 | $cs.module.Default.prototype.initialize.call(this, settings);
1285 | };
1286 |
1287 |
1288 | // Extend the custom posts module with the default properties and methods.
1289 | $cs.extendModule($cs.module.Posts, $cs.module.Default);
1290 |
1291 |
1292 | /**
1293 | * @namespace
1294 | * @property {object} config - Default configuration values.
1295 | * @property {string} config.keyPrefix - The default prefix for value keys.
1296 | * @property {string} config.keySuffix - The default suffix for value keys.
1297 | * @property {string} config.permaLinkDefault - The default text used in permalinks.
1298 | * @property {string} config.postSignatureDefault - The default text used for signatures.
1299 | * @property {boolean} config.quickEdit - Whether or not to use the quick edit feature.
1300 | * @property {boolean} config.formatQuoteCodeTags - Whether or not to use the formatted quote/code tags feature.
1301 | */
1302 | $cs.module.Posts.prototype.config = {
1303 | keyPrefix: '{{',
1304 | keySuffix: '}}',
1305 | permaLinkDefault: 'Permalink',
1306 | postSignatureDefault: '',
1307 | quickEdit: false,
1308 | formatQuoteCodeTags: false
1309 | };
1310 |
1311 |
1312 | /**
1313 | * @property {string} name - The name of this module.
1314 | */
1315 | $cs.module.Posts.prototype.name = '$cs.module.Posts';
1316 |
1317 |
1318 | /**
1319 | * @property {object} reserved - An array of reserved names.
1320 | */
1321 | $cs.module.Posts.prototype.reserved = [
1322 | 'attachCodeEventListeners',
1323 | 'createEditForm',
1324 | 'execute',
1325 | 'Fetch',
1326 | 'formatQuoteCodeTags',
1327 | 'getValue',
1328 | 'hasValue',
1329 | 'initialize',
1330 | 'queryString',
1331 | 'replaceValues',
1332 | 'setValue',
1333 | 'values'
1334 | ];
1335 |
1336 |
1337 | /**
1338 | * Executes the checks and loops needed to complete the script.
1339 | * @readonly
1340 | */
1341 | $cs.module.Posts.prototype.queryString = function(url, query) {
1342 | query = query.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
1343 | var regex = new RegExp('[\\?&]' + query + '=([^]*)'),
1344 | result = regex.exec(url);
1345 | return (result === null) ? '' : decodeURIComponent(result[1].replace(/\+/g, ' '));
1346 | }
1347 |
1348 |
1349 | /**
1350 | * Provides some basic AJAX functionality.
1351 | * @namespace
1352 | * @readonly
1353 | */
1354 | $cs.module.Posts.prototype.Fetch = {
1355 |
1356 |
1357 | /**
1358 | * Creates a new XMLHttpRequest object.
1359 | * @return {Object} a new XMLHttpRequest object.
1360 | */
1361 | request: function() {
1362 | return new (XMLHttpRequest || ActiveXObject)('MSXML2.XMLHTTP.3.0');
1363 | },
1364 |
1365 |
1366 | /**
1367 | * Performs a GET request to the specified URL.
1368 | * @arg {string} url - The URL to GET from.
1369 | * @arg {object} callback - A callback to execute on request success.
1370 | */
1371 | get: function(url, callback) {
1372 | var request = this.request();
1373 | request.open('GET', url);
1374 | request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
1375 | request.onreadystatechange = function() {
1376 | if (request.readyState > 3 && callback && typeof(callback) == 'function') {
1377 | callback(request.responseText);
1378 | }
1379 | };
1380 | request.send();
1381 | },
1382 |
1383 |
1384 | /**
1385 | * Performs a POST request to the specified URL.
1386 | * @arg {string} url - The URL to POST to.
1387 | * @arg {object} data - An associative array of parameters to POST.
1388 | * @arg {object} callback - A callback to execute on request success.
1389 | */
1390 | post: function(url, data, callback) {
1391 | var request = this.request(),
1392 | data = this.formatData(data);
1393 | request.open('POST', url);
1394 | request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
1395 | request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
1396 | request.setRequestHeader('Content-length', data.length);
1397 | request.onreadystatechange = function() {
1398 | if (request.readyState > 3 && callback && typeof(callback) == 'function') {
1399 | callback(request.responseText);
1400 | }
1401 | };
1402 | request.send(data);
1403 | },
1404 |
1405 |
1406 | /**
1407 | * A filter function for outgoing POST requests. Expected by internal
1408 | * IPB1.3.1 form handlers. Partial credit to a user "sk89q" for their
1409 | * work on the original quick edit feature, upon which this function
1410 | * is heavily based.
1411 | * @arg {string} string - The string to be filtered.
1412 | * @readonly
1413 | */
1414 | filter: function(string) {
1415 | var result = '';
1416 | for (var i = 0; i < string.length; i++){
1417 | var currentCharacter = string.charCodeAt(i);
1418 | if(currentCharacter > 127) {
1419 | result += '' + currentCharacter + ';';
1420 | } else {
1421 | result += string.charAt(i);
1422 | }
1423 | }
1424 | return encodeURIComponent ? encodeURIComponent(result) : escape(result);
1425 | },
1426 |
1427 |
1428 | /**
1429 | * A utility function that breaks a parameter object into a POST-friendly
1430 | * query string.
1431 | * @arg {object} parameters - An associative array of parameters.
1432 | * @return {string} - A POST-friendly query string.
1433 | */
1434 | formatData: function(parameters) {
1435 | var temp = [];
1436 | for (var key in parameters) {
1437 | temp.push(this.filter(key) + '=' + this.filter(parameters[key]));
1438 | }
1439 | return temp.join('&');
1440 | }
1441 | };
1442 |
1443 |
1444 | /**
1445 | * An edit form constructor, called dynamically when a user clicks on any post edit link.
1446 | * already.
1447 | * @arg {string} forumId - The forum ID.
1448 | * @arg {string} topicId - The topic ID.
1449 | * @arg {string} postId - The post ID.
1450 | * @arg {string} pageId - The page ID. Varies depending on board settings.
1451 | * @arg {string} response - The HTML response to an AJAX request containing the quick edit form.
1452 | * @arg {object} contentContainer - A reference to the HTML element where the post is contained.
1453 | * @readonly
1454 | */
1455 | $cs.module.Posts.prototype.createEditForm = function(forumId, topicId, postId, pageId, response, contentContainer) {
1456 | // Create all our elements.
1457 | var form = document.createElement('form'),
1458 | textarea = document.createElement('textarea'),
1459 | buttons = document.createElement('div'),
1460 | edit = document.createElement('button'),
1461 | cancel = document.createElement('button'),
1462 | fullEdit = document.createElement('button');
1463 |
1464 | // The form should never just submit, so make sure it can't:
1465 | form.addEventListener('submit', function(event) {
1466 | event.preventDefault();
1467 | });
1468 |
1469 | // Acquire the user's authorization key from the response.
1470 | var responseContainer = document.createElement('div');
1471 | responseContainer.innerHTML = response;
1472 |
1473 | var authKey = responseContainer.firstElementChild.value;
1474 |
1475 | // Acquire the raw contents of the post and place it in our form.
1476 | var rawContent = responseContainer.lastElementChild.value;
1477 |
1478 | // Set the textarea's attributes.
1479 | textarea.innerHTML = rawContent;
1480 | textarea.style.boxSizing = 'border-box';
1481 |
1482 | // Set our button labels.
1483 | edit.innerHTML = 'Edit';
1484 | cancel.innerHTML = 'Cancel';
1485 | fullEdit.innerHTML = 'Full Edit';
1486 |
1487 |
1488 | /**
1489 | * @todo Put this somewhere else. Nested functions like this are ugly.
1490 | */
1491 | var loadEditedPost = function(result) {
1492 | // Put a regular expression together.
1493 | var regex = '(?:\\s*' + // start
1494 | '
)' + // the post container
1495 | '((?:.|\n)*?)' + // the post content - target text
1496 | '(?: <\/div>\n {8}' + // a closing div tag...
1497 | // ... either right before the user's signature, or...
1498 | '(?: ){2}(?:.|\n)*?(?: ' +
1499 | '\n
)|' +
1500 | // ... right before the end of the post.
1501 | '(?:<\/div>\\s*))' //end
1502 |
1503 | // Acquire the edited post.
1504 | var editedPost = new RegExp(regex).exec(result);
1505 |
1506 | /* If we can't find it, though, there was a problem. Show an error,
1507 | then return. */
1508 | if (!editedPost) {
1509 | console.error('Could not GET edited post. Read failed.');
1510 | return;
1511 | }
1512 |
1513 | /* Construct the final post, set its attributes, and replace the edit
1514 | form with it. */
1515 | var finalPost = document.createElement('div');
1516 | // Add the all-important class to the final post.
1517 | finalPost.classList.add('cs-quick-edit');
1518 | finalPost.innerHTML = editedPost[1];
1519 |
1520 | // Replace the edit form with the edited post.
1521 | edit.parentNode.parentNode.parentNode.replaceChild(
1522 | finalPost,
1523 | edit.parentNode.parentNode
1524 | );
1525 |
1526 | // Format quote and code tags, if applicable.
1527 | if (this.config.formatQuoteCodeTags) {
1528 | var tags = finalPost.getElementsByTagName('table');
1529 | this.formatQuoteCodeTags(tags);
1530 | this.attachCodeEventListeners();
1531 | }
1532 | };
1533 |
1534 | /**
1535 | * @todo Put this somewhere else. Nested functions like this are ugly.
1536 | */
1537 | var editPost = function(event) {
1538 | event.preventDefault();
1539 | var editURL = '/?act=Post&quickedit=1&CODE=09&f=' + forumId + '&t=' +
1540 | topicId + '&p=' + postId + '&st=' + pageId + '&auth_key=' +
1541 | authKey;
1542 |
1543 | /**
1544 | * POST parameters to send.
1545 | * @namespace
1546 | */
1547 | var params = {
1548 | enablesig: "yes",
1549 | Post: textarea.value
1550 | };
1551 |
1552 | // POST our edited post to the forum.
1553 | this.Fetch.post(editURL, params, function(response) {
1554 | // Create a container for the response markup.
1555 | var responseContainer = document.createElement('div');
1556 | responseContainer.innerHTML = response;
1557 |
1558 | /*
1559 | If successful, the response will contain a single link to skip
1560 | the redirection. Let's grab it; we'll neet to GET the page
1561 | there to read in the edited post.
1562 | */
1563 | var responseLinks = responseContainer.getElementsByTagName('a');
1564 |
1565 | if (responseLinks.length === 1) {
1566 | /* Add a timestamp to the URL to prevent browsers from caching
1567 | the AJAX call. */
1568 | var redirectUrl = responseLinks[0].href + '&nocache=' +
1569 | new Date().getTime();;
1570 | }
1571 | /* If the redirect URL doesn't exist, we've got a problem. Show an
1572 | error, then return early. */
1573 | else {
1574 | console.error('Couldn\'t POST changes. Edit failed.');
1575 | return;
1576 | }
1577 |
1578 | // GET the edited post from our redirect URL.
1579 | this.Fetch.get(redirectUrl, loadEditedPost.bind(this));
1580 | }.bind(this));
1581 | };
1582 |
1583 | // Attach our event listeners.
1584 | edit.addEventListener('click', editPost.bind(this));
1585 | cancel.addEventListener('click', function(event) {
1586 | event.preventDefault();
1587 | this.parentNode.parentNode.parentNode.replaceChild(
1588 | contentContainer,
1589 | this.parentNode.parentNode
1590 | );
1591 | });
1592 | fullEdit.addEventListener('click', function(event) {
1593 | event.preventDefault();
1594 | window.location.href = '/?act=Post&CODE=08&f=' + forumId + '&t=' +
1595 | topicId + '&p=' + postId + '&st=' + pageId;
1596 | }.bind(this));
1597 |
1598 | // Append the buttons to the button container.
1599 | buttons.appendChild(edit);
1600 | buttons.appendChild(cancel);
1601 | buttons.appendChild(fullEdit);
1602 |
1603 | // Append the textarea and the button container to the form.
1604 | form.appendChild(textarea);
1605 | form.appendChild(buttons);
1606 |
1607 | // Return the form for use elsewhere.
1608 | return form;
1609 | };
1610 |
1611 |
1612 | /**
1613 | * Formats code and quote tags inside a given element, typically series of tables.
1614 | * @arg {object} tags - An array of HTMLObjects- tables.
1615 | * @arg {boolean} includeFirst - Whether or not to include the first
1616 | * @readonly
1617 | */
1618 | $cs.module.Posts.prototype.formatQuoteCodeTags = function(tags) {
1619 | for (var m = tags.length; m > -1; m--) {
1620 | if (typeof tags[m] !== 'undefined' && tags[m].id == 'QUOTE-WRAP') {
1621 | tags[m].style.display = 'none';
1622 | var quoteTitleContents = tags[m].firstElementChild.firstElementChild.firstElementChild.innerHTML.slice(14, -1).split(' @ ');
1623 | var quoteAuthor = quoteTitleContents[0],
1624 | quoteTimestamp = quoteTitleContents[1];
1625 | if (!quoteAuthor) {
1626 | quoteAuthor = '';
1627 | }
1628 | if (!quoteTimestamp) {
1629 | quoteTimestamp = '';
1630 | }
1631 | var originalQuote = tags[m].firstElementChild.lastElementChild.firstElementChild.innerHTML;
1632 | var quoteContainer = document.createElement('div');
1633 | quoteContainer.classList.add('quote-wrapper');
1634 | quoteContainer.innerHTML = '
' + quoteAuthor + '' + quoteTimestamp + '
' + originalQuote + '
';
1635 | tags[m].parentNode.insertBefore(quoteContainer, tags[m].nextSibling);
1636 | tags[m].parentNode.removeChild(tags[m]);
1637 | }
1638 | else if (typeof tags[m] !== 'undefined' && tags[m].id == 'CODE-WRAP') {
1639 | tags[m].style.display = 'none';
1640 | var originalCode = tags[m].firstElementChild.lastElementChild.firstElementChild.innerHTML;
1641 | var codeContainer = document.createElement('div');
1642 | codeContainer.classList.add('code-wrapper');
1643 | var codeTitle = document.createElement('code');
1644 | codeTitle.classList.add('code-title');
1645 | codeTitle.style.cursor = 'pointer';
1646 | codeTitle.appendChild(document.createTextNode('Code (Click to highlight)'));
1647 | codeContainer.appendChild(codeTitle);
1648 | var codeContents = document.createElement('div'),
1649 | codeContentsPre = document.createElement('pre'),
1650 | codeContentsCode = document.createElement('code');
1651 | codeContentsCode.innerHTML = originalCode;
1652 | codeContentsPre.appendChild(codeContentsCode);
1653 | codeContents.appendChild(codeContentsPre);
1654 | codeContents.classList.add('code-contents');
1655 | codeContainer.appendChild(codeContents);
1656 | tags[m].parentNode.insertBefore(codeContainer, tags[m].nextSibling);
1657 | tags[m].parentNode.removeChild(tags[m]);
1658 | }
1659 | }
1660 | };
1661 |
1662 |
1663 | /**
1664 | * Attaches click-to-highlight events to newly created code containers.
1665 | * @readonly
1666 | */
1667 | $cs.module.Posts.prototype.attachCodeEventListeners = function() {
1668 | var newCode = document.getElementsByClassName('code-wrapper');
1669 | for (var n = 0, newCodeCount = newCode.length; n < newCodeCount; n++) {
1670 | newCode[n].firstElementChild.addEventListener('click', function(event) {
1671 | event.preventDefault();
1672 | var range, selection;
1673 | if (document.body.createTextRange) {
1674 | range = document.body.createTextRange();
1675 | range.moveToElementText(this.nextElementSibling);
1676 | range.select();
1677 | } else if (window.getSelection) {
1678 | selection = window.getSelection();
1679 | range = document.createRange();
1680 | range.selectNodeContents(this.nextElementSibling);
1681 | selection.removeAllRanges();
1682 | selection.addRange(range);
1683 | }
1684 | });
1685 | };
1686 | };
1687 |
1688 |
1689 | /**
1690 | * Executes the checks and loops needed to complete the script.
1691 | * @readonly
1692 | */
1693 | $cs.module.Posts.prototype.execute = function() {
1694 | // Make sure we're viewing a topic before executing.
1695 | if (window.location.href.indexOf('showtopic') !== -1 || window.location.href.indexOf('ST') !== -1) {
1696 | var posts = document.getElementsByClassName('post-normal');
1697 |
1698 | // Create a new HTML element and set the appropriate attributes.
1699 | var newPosts = document.createElement('div');
1700 | newPosts.id = 'new-posts';
1701 |
1702 | // Loop through each post being displayed.
1703 | for (var i = 0, numPosts = posts.length; i < numPosts; i++) {
1704 | // Hide each post.
1705 | posts[i].style.display = 'none';
1706 |
1707 | // Acquire the elements necessary to read in the values.
1708 | var table = posts[i].firstElementChild,
1709 | rows = table.getElementsByTagName('tr'),
1710 | cells = [];
1711 |
1712 | /*
1713 | To avoid collisions with doHTML and custom miniprofiles, we need to
1714 | check the direct children of each row. This takes some extra work.
1715 | */
1716 | for (var j = 0, numRows = rows.length; j < numRows; j++) {
1717 | var directChildrenOfRow = rows[j].childNodes;
1718 | for (var k = 0, numCells = directChildrenOfRow.length; k < numCells; k++) {
1719 | var child = directChildrenOfRow[k];
1720 | if (child.nodeType === 1 && child.tagName === 'TD') {
1721 | if (child.parentNode.parentNode.parentNode === table) {
1722 | cells.push(child);
1723 | }
1724 | }
1725 | }
1726 | }
1727 |
1728 | // Read the values in.
1729 | var postLinks = cells[0].getElementsByTagName('a'),
1730 | postId = postLinks[0].name.split('entry')[1],
1731 | topicId;
1732 |
1733 | /*
1734 | IPB 1.3.1 has two different ways of displaying topics using URL query strings.
1735 | If we don't have a match for the usual one, check the other possible URL query.
1736 | Internally consistent, IPB 1.3.1 ain't.
1737 | */
1738 | if (window.location.search.indexOf('showtopic') !== -1) {
1739 | topicId = this.queryString(window.location.search, 'showtopic');
1740 | } else {
1741 | topicId = this.queryString(window.location.search, 't');
1742 | }
1743 |
1744 | this.setValue('postId', postId);
1745 |
1746 | // The author names for guests and users have to be read differently.
1747 | if (postLinks.length > 1) {
1748 | this.setValue('postAuthor', cells[0].innerHTML.split('normalname">')[1].slice(0, -7));
1749 | } else {
1750 | this.setValue('postAuthor', cells[0].innerHTML.split('unreg">')[1].slice(0, -7));
1751 | }
1752 | this.setValue('permaLink', '' + this.config.permaLinkDefault + '');
1753 | this.setValue('postDate', cells[1].firstElementChild.textContent.split('Posted: ')[1]);
1754 | this.setValue('postButtonsTop', cells[1].lastElementChild.innerHTML);
1755 |
1756 | /*
1757 | The topic starter will always be missing the checkbox, so use an offset to
1758 | properly count the cells from this point onward.
1759 | */
1760 | var cellOffset = 0;
1761 | if (cells[2].innerHTML.indexOf('input') !== -1) {
1762 | this.setValue('postCheckbox', cells[2].innerHTML);
1763 | cellOffset = 1;
1764 | } else {
1765 | this.setValue('postCheckbox', '');
1766 | }
1767 | this.setValue('postMiniprofile', cells[2 + cellOffset].firstElementChild.innerHTML);
1768 |
1769 | // If the quick edit feature is enabled, make sure to wrap the post contents here.
1770 | if (this.config.quickEdit) {
1771 | this.setValue('postContent', '
');
1772 | } else {
1773 | this.setValue('postContent', cells[3 + cellOffset].firstElementChild.innerHTML);
1774 | }
1775 |
1776 | var postSignature = cells[3 + cellOffset].lastElementChild;
1777 | if (postSignature.previousElementSibling) {
1778 | this.setValue('postSignature', postSignature.innerHTML);
1779 | } else {
1780 | this.setValue('postSignature', this.config.postSignatureDefault);
1781 | }
1782 | this.setValue('postIp', cells[4 + cellOffset].textContent);
1783 | this.setValue('postButtonsBottom', cells[5 + cellOffset].firstElementChild.innerHTML);
1784 |
1785 | // Create a new element for this post and append it to the new posts container.
1786 | var newPost = document.createElement('div');
1787 | newPost.id = 'entry' + postId;
1788 | newPost.classList.add('new-post', posts[i].classList[1]);
1789 | newPost.innerHTML = this.replaceValues(this.html, this.values);
1790 | newPosts.appendChild(newPost);
1791 |
1792 | // Handle the quick edit feature, if applicable.
1793 | if (this.config.quickEdit) {
1794 | // Get all the links in the new post.
1795 | var postLinks = newPost.getElementsByTagName('a');
1796 |
1797 | // Iterate over each of the links.
1798 | for (var l = 0; l < postLinks.length; l++) {
1799 |
1800 | // If it's an edit link we're looking at...
1801 | if (postLinks[l].href.indexOf('act=Post&CODE=08') !== -1) {
1802 |
1803 | // ... attach an event listener.
1804 | postLinks[l].addEventListener('click', function(event) {
1805 | event.preventDefault();
1806 |
1807 | var forumId = this.queryString(event.currentTarget.href, 'f'),
1808 | topicId = this.queryString(event.currentTarget.href, 't'),
1809 | postId = this.queryString(event.currentTarget.href, 'p'),
1810 | pageId = this.queryString(event.currentTarget.href, 'st') || '0';
1811 |
1812 | var quickEditURL = '/?&act=Post&CODE=08&f=' + forumId + '&t=' + topicId + '&p=' + postId + '&st=' + pageId + '&quickedit=1';
1813 |
1814 | var editableContent = document.getElementById('entry' + postId).getElementsByClassName('cs-quick-edit')[0];
1815 |
1816 | this.Fetch.get(quickEditURL, function(response) {
1817 | var editForm = this.createEditForm(forumId, topicId, postId, pageId, response, editableContent);
1818 | editableContent.parentNode.replaceChild(editForm, editableContent);
1819 | }.bind(this));
1820 | }.bind(this));
1821 | }
1822 | }
1823 | }
1824 |
1825 | // Formatted code/quote tags
1826 | if (this.config.formatQuoteCodeTags) {
1827 | var tags = newPost.getElementsByTagName('table');
1828 | this.formatQuoteCodeTags(tags);
1829 | }
1830 | }
1831 |
1832 | // Inject the new posts container into the page.
1833 | posts[0].parentNode.insertBefore(newPosts, posts[0]);
1834 |
1835 | // Hide the original posts container.
1836 | table.parentNode.removeChild(table);
1837 | }
1838 |
1839 | if (this.config.formatQuoteCodeTags) {
1840 | this.attachCodeEventListeners();
1841 | }
1842 | };
1843 |
1844 |
1845 | /**
1846 | * Initialization function. Reads user-defined settings in for processing and begins script execution.
1847 | * @arg {object} settings - An object with user-defined settings as properties.
1848 | * @readonly
1849 | */
1850 | $cs.module.Posts.prototype.initialize = function(settings) {
1851 | // Call $cs.module.Default's initialize method instead.
1852 | $cs.module.Default.prototype.initialize.call(this, settings);
1853 | };
1854 |
1855 |
1856 | // Expose some familiar, user-friendly objects for public use.
1857 | window.customIndex = new $cs.module.Index(),
1858 | window.customStats = new $cs.module.Stats(),
1859 | window.customProfile = new $cs.module.Profile(),
1860 | window.customTopics = new $cs.module.Topics(),
1861 | window.customPosts = new $cs.module.Posts();
1862 |
--------------------------------------------------------------------------------
/src/cs.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file
3 | * A DOM manipilation utility library for the version of IPB running on the
4 | * free forum hosting service, Jcink, that reads table information and accepts
5 | * a user-defined template for text replacement. Allows for the structuring
6 | * of forums in nontraditional, table-less layouts. Visible credits are not
7 | * required provided this entire comment block remains intact.
8 | * @author Connor Wiseman
9 | * @copyright 2012-2016 Connor Wiseman
10 | * @version 1.8.2 (September 2016)
11 | * @license
12 | * Copyright (c) 2012-2016 Connor Wiseman
13 | *
14 | * Permission is hereby granted, free of charge, to any person obtaining a copy
15 | * of this software and associated documentation files (the 'Software'), to deal
16 | * in the Software without restriction, including without limitation the rights
17 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 | * copies of the Software, and to permit persons to whom the Software is
19 | * furnished to do so, subject to the following conditions:
20 | *
21 | * The above copyright notice and this permission notice shall be included in
22 | * all copies or substantial portions of the Software.
23 | *
24 | * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30 | * THE SOFTWARE.
31 | */
32 | "use strict";var $cs=$cs||{extendModule:function(e,t){e.prototype=new t,e.prototype.constructor=e.prototype.myParent},module:{Default:function(){},Index:function(){},Stats:function(){},Profile:function(){},Topics:function(){},Posts:function(){}}};$cs.module.Default.prototype.time=!1,$cs.module.Default.prototype.html="",$cs.module.Default.prototype.values={},$cs.module.Default.prototype.getValue=function(e){var e=this.config.keyPrefix+e+this.config.keySuffix;return this.values[e]},$cs.module.Default.prototype.hasValue=function(e){var e=this.config.keyPrefix+e+this.config.keySuffix;return this.values[e]&&""!==this.values[e]},$cs.module.Default.prototype.initialize=function(e){if(e=e||{},!Object.keys(e).length)return console.error(this.name+': init method missing required argument "settings"'),!1;for(var t in e)if(-1===this.reserved.indexOf(t))if("object"==typeof e[t])for(var s in e[t])t in this&&(this[t][s]=e[t][s]);else t in this&&(this[t]=e[t]);return this.html&&""!==this.html?(this.time&&console.time(this.name),this.values={},this.execute(),this.time&&console.timeEnd(this.name),void 0):(console.error(this.name+': required property "html" is undefined'),!1)},$cs.module.Default.prototype.replaceValues=function(e,t){if(e="function"==typeof e?e.call(this):e,void 0===e)return console.error(this.name+': function "html" returns null'),null;var s=RegExp(Object.keys(t).join("|"),"g");return e.replace(s,function(e){return t[e]})},$cs.module.Default.prototype.setValue=function(e,t){this.values[this.config.keyPrefix+e+this.config.keySuffix]=t},$cs.extendModule($cs.module.Index,$cs.module.Default),$cs.module.Index.prototype.config={target:"board",keyPrefix:"{{",keySuffix:"}}",insertBefore:"",insertAfter:"",viewingDefault:"0",subforumSeparator:", ",subforumsNone:"",moderatorsNone:"",dateDefault:"--",titleDefault:"----",urlDefault:"#",authorDefault:"",passwordTitle:"Protected Forum"},$cs.module.Index.prototype.name="$cs.module.Index",$cs.module.Index.prototype.reserved=["values","makeLink","execute","getValue","hasValue","initialize","readTable","replaceValues","setValue"],$cs.module.Index.prototype.execute=function(){var e=document.getElementById(this.config.container),t=document.getElementById("subforum-list"),s=(e||document).getElementsByClassName("category");if(s.length)for(var i=0;i"+e.innerHTML+""},$cs.module.Index.prototype.readTable=function(e,t){var s=e.getElementsByTagName("tr");e.style.display="none";var i="";this.config.insertBefore&&""!==this.config.insertBefore&&(i+='
")[1]),this.setValue("activityLinkClick",'Last Click'),this.setValue("activityLinkMemberName",'Member Name');break;case"Today's"===s[i].textContent.slice(0,7):var l=s[i+2].getElementsByTagName("b");l.length>1?(this.setValue("birthdays",l[0].textContent),this.setValue("birthdaysList",s[i+2].innerHTML.split(" ")[1])):(this.setValue("birthdays","0"),this.setValue("birthdaysList",s[i+2].textContent));break;case"Forthcoming"===s[i].textContent.slice(0,11):this.setValue("events",s[i+2].innerHTML);break;case"Board"===s[i].textContent.slice(0,5):var r=s[i+2].getElementsByTagName("b");this.setValue("totalPosts",r[0].textContent),this.setValue("totalMembers",r[1].textContent),this.setValue("newestMember",r[2].innerHTML),this.setValue("mostOnline",r[3].textContent),this.setValue("mostOnlineDate",r[4].textContent);break;case"Members"===s[i].textContent.slice(0,7):this.setValue("onlineToday",s[i].textContent.split(": ")[1].split(" [")[0]),this.setValue("onlineTodayList",s[i+2].innerHTML.split(": ")[1]);var u=s[i+2].getElementsByTagName("b");this.setValue("mostOnlineOneDay",u[0].textContent),this.setValue("mostOnlineDateOneDay",u[1].textContent);break;case"IBStore"===s[i].textContent.slice(0,7):var c=s[i+2].getElementsByTagName("b");this.setValue("storeProducts",c[0].textContent),this.setValue("storeValue",c[1].textContent),this.setValue("moneyTotal",c[2].textContent),this.setValue("moneyBanked",c[3].textContent),this.setValue("moneyCirculating",c[4].textContent),this.setValue("richestMember",c[5].innerHTML),this.setValue("richestMemberValue",c[6].textContent)}var d=document.createElement("div");d.innerHTML=this.replaceValues(this.html,this.values),d.id="new-statistics",t.parentNode.appendChild(d),t.parentNode.removeChild(t)}},$cs.module.Stats.prototype.initialize=function(e){$cs.module.Default.prototype.initialize.call(this,e)},$cs.extendModule($cs.module.Profile,$cs.module.Default),$cs.module.Profile.prototype.config={htmlEnabled:!1,keyPrefix:"{{",keySuffix:"}}",emailDefault:"Click here",messageDefault:"Click here",reputationIncrease:"+",reputationDecrease:"-",warnIncrease:"+",warnDecrease:"-",reputationDetails:"[details >>]",avatarDefault:"",userPhotoDefault:"",onlineActivityDefault:"",customFieldsInnerHTML:!1},$cs.module.Profile.prototype.name="$cs.module.Profile",$cs.module.Profile.prototype.reserved=["values","execute","getValue","hasValue","initialize","replaceValues","setValue","stringToMarkup"],$cs.module.Profile.prototype.execute=function(){var e=document.getElementById("profile-heading"),t=document.getElementById("profilename");if(e){var s=e.parentNode.parentNode.parentNode.parentNode,i=document.getElementById("profile-personalinfo"),n=document.getElementById("profile-customfields"),a=document.getElementById("profile-statistics"),o=document.getElementById("profile-contactinfo"),l=document.getElementById("sig_popup"),r=document.getElementById("profile-header");s.style.display="none";var u=location.href.split("?showuser=")[1];this.setValue("userId",u);var c=i.getElementsByTagName("div"),d=c[2].getElementsByTagName("img")[0];d?this.setValue("userPhoto",d.src):this.setValue("userPhoto",this.config.userPhotoDefault);var h=c[4].textContent.split("%")[0];""!==h&&(this.setValue("warnLevel",h),this.setValue("warnLevelIncrease",''+this.config.warnIncrease+""),this.setValue("warnLevelDecrease",''+this.config.warnDecrease+""));var m=0;if("Options"!==c[5].textContent){m=1;var p=c[5].textContent.split("] ")[1].split(" pts")[0];""!==p&&(this.setValue("reputationTotal",p),this.setValue("reputationIncrease",''+this.config.reputationIncrease+""),this.setValue("reputationDecrease",''+this.config.reputationDecrease+""),this.setValue("reputationDetails",''+this.config.reputationDetails+""))}this.setValue("userTitle",c[10+m].textContent),this.setValue("location",c[12+m].textContent.split("Location: ")[1]),this.setValue("birthday",c[13+m].textContent.split("Born: ")[1]),this.setValue("homePage",c[14+m].innerHTML.split("Website: ")[1]),this.config.htmlEnabled?this.setValue("interests",this.stringToMarkup(c[16+m].innerHTML)):this.setValue("interests",c[16+m].innerHTML);for(var f=n.getElementsByTagName("div"),g=1,v=f.length;v>g;g++){if(this.config.customFieldsInnerHTML)var y=f[g].innerHTML.split(": ")[1];else var y=f[g].textContent.split(": ")[1];this.setValue("customField"+g,y)}var C=a.getElementsByTagName("div");this.setValue("joinDate",C[1].textContent.split("Joined: ")[1]),this.setValue("onlineStatus",C[2].textContent.split("(")[1].split(")")[0]),-1===C[2].textContent.split("(")[1].split(")")[0].indexOf("Offline")?this.setValue("onlineActivity",C[2].textContent.split(") (")[1].split(")")[0]):this.setValue("onlineActivity",this.config.onlineActivityDefault),this.setValue("lastActivity",C[3].textContent.split(": ")[1]),this.setValue("localTime",C[4].textContent.split(": ")[1]),this.setValue("postCount",C[5].textContent.split("posts")[0]),this.setValue("postsPerDay",C[5].textContent.split("(")[1].split(" per")[0]);var V=o.getElementsByTagName("div");this.setValue("userAIM",V[1].textContent),this.setValue("userYahoo",V[2].textContent),this.setValue("userGtalk",V[3].textContent),this.setValue("userMSN",V[4].textContent),this.setValue("userSkype",V[5].textContent),-1!==V[6].textContent.indexOf("Click")?this.setValue("sendMessage",''+this.config.messageDefault+""):this.setValue("sendMessage","Private"),-1!==V[7].textContent.indexOf("Click")?this.setValue("sendEmail",''+this.config.emailDefault+""):this.setValue("sendEmail","Private");var T=l.getElementsByTagName("td");this.setValue("signature",T[2].innerHTML);var x=r.getElementsByTagName("h3");this.setValue("userName",x[0].textContent);var E=r.getElementsByTagName("strong");this.setValue("userGroup",E[0].textContent);var D=r.previousElementSibling.getElementsByTagName("img")[0];D?this.setValue("avatar",D.src):this.setValue("avatar",this.config.avatarDefault);var N=document.createElement("div");N.innerHTML=this.replaceValues(this.html,this.values),N.id="new-profile",s.parentNode.appendChild(N),s.parentNode.removeChild(s)}else if(t){var L=t.parentNode.parentNode.parentNode.parentNode,M=L.nextElementSibling,w=M.nextElementSibling,b=w.nextElementSibling,k=b.nextElementSibling;L.style.display="none",M.style.display="none",w.style.display="none",b.style.display="none",k.style.display="none";var u=location.href.split("?showuser=")[1];this.setValue("userId",u);var B=L.getElementsByTagName("td"),d=B[0].getElementsByTagName("img")[0],H=B[1].getElementsByTagName("div");d?this.setValue("userPhoto",d.src):this.setValue("userPhoto",this.config.userPhotoDefault),this.setValue("userName",H[0].textContent);var P=w.getElementsByTagName("table"),$=P[0],S=P[1],O=P[2],I=P[3],A=$.getElementsByTagName("td");this.setValue("postCount",A[2].textContent.split("(")[0]),this.setValue("postsPerDay",A[4].textContent),this.setValue("joinDate",A[6].textContent),this.setValue("localTime",A[8].textContent);var q=A[10].textContent;this.setValue("onlineStatus",q.split("(")[1].split(")")[0]),-1===q.split("(")[1].split(")")[0].indexOf("Offline")?this.setValue("onlineActivity",q.split(") (")[1].split(")")[0]):this.setValue("onlineActivity",this.config.onlineActivityDefault);var R=S.getElementsByTagName("td");-1!==R[2].textContent.indexOf("Click")?this.setValue("sendEmail",'Click here'):this.setValue("sendEmail","Private"),this.setValue("userSkype",R[4].textContent),this.setValue("userAIM",R[6].textContent),this.setValue("userGtalk",R[8].textContent),this.setValue("userYahoo",R[10].textContent),this.setValue("userMSN",R[12].textContent),-1!==R[14].textContent.indexOf("Click")?this.setValue("sendMessage",'Click here'):this.setValue("sendMessage","Private");var F=O.getElementsByTagName("td");this.setValue("homePage",F[2].innerHTML),this.setValue("birthday",F[4].textContent),this.setValue("location",F[6].textContent),this.config.htmlEnabled?this.setValue("interests",this.stringToMarkup(F[8].innerHTML)):this.setValue("interests",F[8].innerHTML);var z=0;-1!==F[9].textContent.indexOf("Awards")&&(z=2,this.setValue("awards",F[10].innerHTML)),this.setValue("lastActivity",F[12+z].textContent);for(var g=14+z,U=1;g'+this.config.reputationIncrease+""),this.setValue("reputationDecrease",''+this.config.reputationDecrease+""),this.setValue("reputationDetails",''+this.config.reputationDetails+"");break;case"Warn Level"===Q[g].textContent:this.setValue("warnLevel",Q[g+1].textContent.split("%")[0]),this.setValue("warnLevelIncrease",''+this.config.warnIncrease+""),this.setValue("warnLevelDecrease",''+this.config.warnDecrease+"")}var X=b.getElementsByTagName("td");this.setValue("signature",X[2].innerHTML);var N=document.createElement("div");N.innerHTML=this.replaceValues(this.html,this.values),N.id="new-profile",L.parentNode.appendChild(N),L.parentNode.removeChild(L),M.parentNode.removeChild(M),w.parentNode.removeChild(w),b.parentNode.removeChild(b),k.parentNode.removeChild(k)}},$cs.module.Profile.prototype.initialize=function(e){$cs.module.Default.prototype.initialize.call(this,e)},$cs.module.Profile.prototype.stringToMarkup=function(e){var t=document.createElement("div");t.innerHTML=e;for(var s="",i=0,n=t.childNodes.length;n>i;i++)t.childNodes[i].nodeValue&&(s+=t.childNodes[i].nodeValue);return t="",s},$cs.extendModule($cs.module.Topics,$cs.module.Default),$cs.module.Topics.prototype.config={keyPrefix:"{{",keySuffix:"}}",announcementsDefault:"Announcements",pinnedDefault:"Important Topics",regularDefault:"Forum Topics",noTopics:"No topics were found. This is either because there are no topics in this forum, or the topics are older than the current age cut-off.",noActiveTopics:"There were no active topics during those date ranges",paginationDefault:"",activeTopics:!1},$cs.module.Topics.prototype.name="$cs.module.Topics",$cs.module.Topics.prototype.reserved=["values","execute","getValue","hasValue","initialize","replaceValues","setValue"],$cs.module.Topics.prototype.execute=function(){var e=document.getElementById("topic-list");if(!e&&this.config.activeTopics&&window.location.href.indexOf("act=Search&CODE=getactive")>-1){for(var t=document.getElementsByTagName("form"),s=0;s-1&&(e=t[s].nextElementSibling.nextElementSibling);var i=!0}if(e){for(var n=e.getElementsByTagName("table")[0],a=[],o="",l=" regular-topic",r=0;rs;s++){var c=a[s].getElementsByTagName("td");if(c[3]){if(i){if(this.config.activeTopics){this.setValue("folder",c[0].innerHTML),this.setValue("marker",c[1].innerHTML);var d=c[2].getElementsByTagName("a");if(-1===d[0].href.indexOf("view=getnewpost"))var h=d[0];else var h=d[1];this.setValue("topicId",h.getAttribute("href").split("showtopic=")[1].split("&")[0]),this.setValue("topicTitle",''+h.textContent+"");var m=c[2].getElementsByTagName("span");-1!==m[0].textContent.indexOf("(Pages ")?(this.setValue("pagination",m[0].innerHTML),this.setValue("topicDescription",m[1].textContent)):(this.setValue("pagination",this.config.paginationDefault),this.setValue("topicDescription",m[0].textContent)),this.setValue("parentForum",c[5].innerHTML),this.setValue("topicAuthor",c[6].innerHTML),this.setValue("replyCount",c[7].textContent),this.setValue("viewCount",c[8].textContent),this.setValue("lastReplyDate",c[9].firstChild.nodeValue),this.setValue("lastReplyDate",c[9].firstChild.nodeValue);var p=c[9].getElementsByTagName("b");p[1]?this.setValue("lastReplyAuthor",p[1].innerHTML):this.setValue("lastReplyAuthor",p[0].innerHTML),this.setValue("moderatorCheckbox","")}}else{this.setValue("folder",c[0].innerHTML),this.setValue("marker",c[1].innerHTML);var h=c[2].getElementsByTagName("a")[0];h.getAttribute("title")||(h=c[2].getElementsByTagName("a")[1]),this.setValue("topicId",h.getAttribute("href").split("showtopic=")[1]),this.setValue("topicTitle",''+h.textContent+"");var m=c[2].getElementsByTagName("span");-1!==m[0].textContent.indexOf("(Pages ")?(this.setValue("pagination",m[0].innerHTML),this.setValue("topicDescription",m[1].textContent)):(this.setValue("pagination",this.config.paginationDefault),this.setValue("topicDescription",m[0].textContent));var f=e.firstElementChild.textContent.trim(),g=''+f+"";this.setValue("parentForum",g),this.setValue("topicAuthor",c[3].innerHTML),this.setValue("replyCount",c[4].textContent),this.setValue("viewCount",c[5].textContent),this.setValue("lastReplyDate",c[6].getElementsByTagName("span")[0].firstChild.nodeValue),this.setValue("lastReplyAuthor",c[6].getElementsByTagName("b")[0].innerHTML),this.setValue("moderatorCheckbox",c[7].innerHTML)}o+='