├── .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 | [![Build Status](https://travis-ci.org/ConnorWiseman/jcink-custom-structure.svg?branch=master)](https://travis-ci.org/ConnorWiseman/jcink-custom-structure) [![Test Coverage](https://codeclimate.com/github/ConnorWiseman/jcink-custom-structure/badges/coverage.svg)](https://codeclimate.com/github/ConnorWiseman/jcink-custom-structure/coverage) [![Code Climate](https://codeclimate.com/github/ConnorWiseman/jcink-custom-structure/badges/gpa.svg)](https://codeclimate.com/github/ConnorWiseman/jcink-custom-structure) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](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 |
398 | 399 | 400 |
401 |
Announcements
402 |
403 | 408 |
409 |
Important Topics
410 |
411 | 416 |
417 |
Forum Topics
418 |
419 | 424 |
425 |
426 | 427 |
# User(s) are browsing this forum
428 |
429 |
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 += '
' + this.config.insertBefore + '
'; 356 | } 357 | 358 | /* 359 | Loop through each row in the table except the first and the last, 360 | which are only used for layout and are useless for this script. 361 | */ 362 | for (var j = 1, numRows = rows.length - 1; j < numRows; j++) { 363 | // Acquire all the cells in the row, then begin reading in the necessary values. 364 | var cells = rows[j].getElementsByTagName('td'); 365 | this.setValue('forumMarker', cells[0].innerHTML); 366 | this.setValue('forumTitle', cells[1].firstElementChild.innerHTML); 367 | 368 | // If "(X Viewing)" is enabled, make sure to account for it. 369 | var viewing = cells[1].getElementsByClassName('x-viewing-forum')[0]; 370 | if (viewing) { 371 | this.setValue('forumViewing', viewing.innerHTML.split('(')[1].split(' Vi')[0]); 372 | } else { 373 | this.setValue('forumViewing', this.config.viewingDefault); 374 | } 375 | 376 | this.setValue('forumId', this.values['{{forumTitle}}'].split('showforum=')[1].split('" alt="')[0]); 377 | this.setValue('forumDescription', cells[1].getElementsByClassName('forum-desc')[0].innerHTML); 378 | 379 | // Subforums need a bit of extra processing. 380 | var subforums = cells[1].getElementsByClassName('subforums')[0], 381 | subforumList = ''; 382 | if (subforums) { 383 | /* 384 | If this row contains subforums acquire all the anchors in the subforum 385 | element and loop over them to build a list. For some reason Jcink tosses 386 | in empty links here, so skip every other link. 387 | */ 388 | var subforumLinks = subforums.getElementsByTagName('a'); 389 | for (var k = 0, numLinks = subforumLinks.length; k < numLinks; k++) { 390 | if(subforumLinks[k].classList[0] == 'subforums-macro') { 391 | continue; 392 | } 393 | 394 | //if (!this.elemHasClass(link, ' 395 | // Build an HTML string out of the anchor object. 396 | var link = this.makeLink(subforumLinks[k]); 397 | 398 | /* 399 | If this is the last link in the list (not counting the extra empty link), 400 | then don't add the separator. Otherwise, the separator is necessary. 401 | */ 402 | if(k === numLinks - 1) { 403 | subforumList += link; 404 | } else { 405 | subforumList += link + this.config.subforumSeparator; 406 | } 407 | } 408 | this.setValue('subforums', subforumList); 409 | } else { 410 | // If this row does not contain any subforums, use the default instead. 411 | this.setValue('subforums', this.config.subforumsNone); 412 | } 413 | 414 | // Moderators also get processed a little differently. 415 | var moderators = cells[1].getElementsByClassName('forum-led-by')[0]; 416 | if (moderators && moderators.textContent) { 417 | // If this row contains moderators, acquire everything after the useless intro string. 418 | this.setValue('moderators', moderators.innerHTML.split('Forum Led by: ')[1]); 419 | this.setValue('redirectHits', 0); 420 | } else if (moderators) { 421 | // If it doesn't, but it could, set the number of redirects to zero. 422 | this.setValue('redirectHits', 0); 423 | } else { 424 | // If it doesn't, use the default instead. 425 | this.setValue('moderators', this.config.moderatorsNone); 426 | // Moderators are never shown on redirect forms, so it's convenient to do this: 427 | this.setValue('redirectHits', cells[4].textContent.split('Redirected Hits: ')[1]); 428 | } 429 | 430 | this.setValue('topicCount', cells[2].textContent); 431 | this.setValue('replyCount', cells[3].textContent); 432 | 433 | /* 434 | Last post content is processed together in a batch. If the cell contains a link, 435 | there's last post information. If not, use the default values. 436 | */ 437 | var lastPost = cells[4], 438 | lastPostLinks = lastPost.getElementsByTagName('a'); 439 | if (lastPostLinks.length > 0) { 440 | // If there are links, read the values on the page. 441 | this.setValue('lastPostDate', lastPost.childNodes[0].nodeValue); 442 | if (lastPost.textContent.indexOf('Protected Forum') === -1) { 443 | // If there are no italics, this forum is not password-protected. 444 | this.setValue('lastPostTitle', this.makeLink(lastPostLinks[1])); 445 | this.setValue('lastPostURL', lastPostLinks[1].href.slice(0, -16)); 446 | if (lastPostLinks[2]) { 447 | this.setValue('lastPostAuthor', this.makeLink(lastPostLinks[2])); 448 | } else { 449 | this.setValue('lastPostAuthor', lastPost.textContent.split('By: ')[1]); 450 | } 451 | } else { 452 | // If there are italics, this forum is password-protected. 453 | this.setValue('lastPostTitle', this.config.passwordTitle); 454 | this.setValue('lastPostURL', this.config.urlDefault); 455 | this.setValue('lastPostAuthor', this.makeLink(lastPostLinks[0])); 456 | } 457 | } else { 458 | // If there are no links, use the default values. 459 | this.setValue('lastPostDate', this.config.dateDefault); 460 | this.setValue('lastPostTitle', this.config.titleDefault); 461 | this.setValue('lastPostURL', this.config.urlDefault); 462 | this.setValue('lastPostAuthor', this.config.authorDefault); 463 | } 464 | 465 | // If the forum marker contains a link, prepare to append a useful utility class to the output. 466 | var newPosts = ''; 467 | if(this.values['{{forumMarker}}'].indexOf('
' + 473 | this.replaceValues(this.html, this.values) + '<\/div>'; 474 | } 475 | 476 | // Add any content intended to be inserted after every category. 477 | if (this.config.insertAfter && this.config.insertAfter !== '') { 478 | categoryContent += '
' + this.config.insertAfter + '
'; 479 | } 480 | 481 | // Create a new HTML element, set the appropriate attributes, and inject it into the page. 482 | var newCategory = document.createElement('div'); 483 | newCategory.innerHTML = categoryContent; 484 | newCategory.id = 'category-' + (index + 1); 485 | newCategory.className = 'new-category'; 486 | table.parentNode.appendChild(newCategory); 487 | 488 | // Remove the original table. 489 | table.parentNode.removeChild(table); 490 | }; 491 | 492 | 493 | // Extend the custom stats module with the default properties and methods. 494 | $cs.extendModule($cs.module.Stats, $cs.module.Default); 495 | 496 | 497 | /** 498 | * @namespace 499 | * @property {object} config - Default configuration values. 500 | * @property {string} config.keyPrefix - The default prefix for value keys. 501 | * @property {string} config.keySuffix - The default suffix for value keys. 502 | */ 503 | $cs.module.Stats.prototype.config = { 504 | keyPrefix: '{{', 505 | keySuffix: '}}' 506 | }; 507 | 508 | 509 | /** 510 | * @property {string} name - The name of this module. 511 | */ 512 | $cs.module.Stats.prototype.name = '$cs.module.Stats'; 513 | 514 | 515 | /** 516 | * @property {object} reserved - An array of reserved names. 517 | */ 518 | $cs.module.Stats.prototype.reserved = [ 519 | 'values', 520 | 'execute', 521 | 'getValue', 522 | 'hasValue', 523 | 'initialize', 524 | 'replaceValues', 525 | 'setValue' 526 | ]; 527 | 528 | 529 | /** 530 | * Executes the checks and loops needed to complete the script. 531 | * @readonly 532 | */ 533 | $cs.module.Stats.prototype.execute = function() { 534 | var boardStats = document.getElementById('boardstats'); 535 | if (boardStats) { 536 | var table = boardStats.lastElementChild; 537 | // Hide the original table. 538 | table.style.display = 'none'; 539 | 540 | // Acquire all the cells in the table. 541 | var cells = table.getElementsByTagName('td'); 542 | 543 | /* 544 | Loop through all of the cells in the table in groups of three and read 545 | in the values. The order of the statistics can be arbitrary thanks to 546 | feature bloat, so a switch statement is used inside the loop to check 547 | which set of cells we're working with before proceeding. This does mean, 548 | unfortunately, that some of these case expressions are checked more than 549 | once. The redundant execution overhead is a small price to pay for 550 | accuracy. 551 | */ 552 | for (var i = 0, numCells = cells.length; i < numCells; i += 3) { 553 | switch (true) { 554 | case (cells[i].textContent.indexOf('active in the past') !== -1): 555 | // Total number of users online. 556 | this.setValue('totalUsers', cells[i].textContent.split(' user')[0]); 557 | 558 | // Individual online totals are divided into three parts. 559 | var individualTotals = cells[i + 2].getElementsByTagName('b'); 560 | this.setValue('totalUsersGuests', individualTotals[0].textContent); 561 | this.setValue('totalUsersRegistered', individualTotals[1].textContent); 562 | this.setValue('totalUsersAnonymous', individualTotals[2].textContent); 563 | 564 | // List of online users. 565 | var onlineList = cells[i + 2].getElementsByClassName('thin')[0]; 566 | this.setValue('onlineList', onlineList.innerHTML.split('

')[0]); 567 | 568 | // Member legend. 569 | this.setValue('onlineLegend', onlineList.innerHTML.split('

')[1]); 570 | 571 | // Useful links. 572 | this.setValue('activityLinkClick', '
Last Click'); 573 | this.setValue('activityLinkMemberName', 'Member Name'); 574 | break; 575 | 576 | case (cells[i].textContent.slice(0, 7) === 'Today\'s'): 577 | // Today's birthdays. 578 | var numBirthdays = cells[i + 2].getElementsByTagName('b'); 579 | if (numBirthdays.length > 1) { 580 | this.setValue('birthdays', numBirthdays[0].textContent); 581 | this.setValue('birthdaysList', cells[i + 2].innerHTML.split('
')[1]); 582 | } else { 583 | this.setValue('birthdays', '0'); 584 | this.setValue('birthdaysList', cells[i + 2].textContent); 585 | } 586 | break; 587 | 588 | case (cells[i].textContent.slice(0, 11) === 'Forthcoming'): 589 | // Forthcoming events. 590 | this.setValue('events', cells[i + 2].innerHTML); 591 | break; 592 | 593 | case (cells[i].textContent.slice(0, 5) === 'Board'): 594 | // The post and member statistics. 595 | var statisticsItems = cells[i + 2].getElementsByTagName('b'); 596 | this.setValue('totalPosts', statisticsItems[0].textContent); 597 | this.setValue('totalMembers', statisticsItems[1].textContent); 598 | this.setValue('newestMember', statisticsItems[2].innerHTML); 599 | this.setValue('mostOnline', statisticsItems[3].textContent); 600 | this.setValue('mostOnlineDate', statisticsItems[4].textContent); 601 | break; 602 | 603 | case (cells[i].textContent.slice(0, 7) === 'Members'): 604 | // Online today total. 605 | this.setValue('onlineToday', cells[i].textContent.split(': ')[1].split(' [')[0]); 606 | 607 | // Members online today list. 608 | this.setValue('onlineTodayList', cells[i + 2].innerHTML.split(':
')[1]); 609 | 610 | // Members online today statistics. 611 | var membersOnlineItems = cells[i + 2].getElementsByTagName('b'); 612 | this.setValue('mostOnlineOneDay', membersOnlineItems[0].textContent); 613 | this.setValue('mostOnlineDateOneDay', membersOnlineItems[1].textContent); 614 | break; 615 | 616 | case (cells[i].textContent.slice(0, 7) === 'IBStore'): 617 | // IBStore totals. 618 | var ibstoreItems = cells[i + 2].getElementsByTagName('b'); 619 | this.setValue('storeProducts', ibstoreItems[0].textContent); 620 | this.setValue('storeValue', ibstoreItems[1].textContent); 621 | this.setValue('moneyTotal', ibstoreItems[2].textContent); 622 | this.setValue('moneyBanked', ibstoreItems[3].textContent); 623 | this.setValue('moneyCirculating', ibstoreItems[4].textContent); 624 | this.setValue('richestMember', ibstoreItems[5].innerHTML); 625 | this.setValue('richestMemberValue', ibstoreItems[6].textContent); 626 | break; 627 | } 628 | } 629 | 630 | // Create a new HTML element, set the appropriate attributes, and inject it into the page. 631 | var newStats = document.createElement('div'); 632 | newStats.innerHTML = this.replaceValues(this.html, this.values); 633 | newStats.id = 'new-statistics'; 634 | table.parentNode.appendChild(newStats); 635 | 636 | // Remove the original table. 637 | table.parentNode.removeChild(table); 638 | } 639 | }; 640 | 641 | 642 | /** 643 | * Initialization function. Reads user-defined settings in for processing and begins script execution. 644 | * @arg {object} settings - An object with user-defined settings as properties. 645 | * @readonly 646 | */ 647 | $cs.module.Stats.prototype.initialize = function(settings) { 648 | // Call $cs.module.Default's initialize method instead. 649 | $cs.module.Default.prototype.initialize.call(this, settings); 650 | }; 651 | 652 | 653 | // Extend the custom profile module with the default properties and methods. 654 | $cs.extendModule($cs.module.Profile, $cs.module.Default); 655 | 656 | 657 | /** 658 | * @namespace 659 | * @property {object} config - Default configuration values. 660 | * @property {boolean} config.htmlEnabled - Whether or not HTML is enabled in the interests field. 661 | * @property {string} config.keyPrefix - The default prefix for value keys. 662 | * @property {string} config.keySuffix - The default suffix for value keys. 663 | * @property {string} config.emailDefault - The default email link content. 664 | * @property {string} config.messageDefault - The default personal message link content. 665 | * @property {string} config.reputationIncrease - The default increase reputation link content. 666 | * @property {string} config.reputationDecrease - The default decrease reputation link content. 667 | * @property {string} config.warnIncrease - The default increase warning link content. 668 | * @property {string} config.warnDecrease - The default decrease warning link content. 669 | * @property {string} config.reputationDetails - The default reputation details link content. 670 | * @property {string} config.avatarDefault - The URL of a default avatar. 671 | * @property {string} config.userPhotoDefault - The URL of a default user photo. 672 | * @property {string} config.onlineActivityDefault - The default online activity text. 673 | */ 674 | $cs.module.Profile.prototype.config = { 675 | htmlEnabled: false, 676 | keyPrefix: '{{', 677 | keySuffix: '}}', 678 | emailDefault: 'Click here', 679 | messageDefault: 'Click here', 680 | reputationIncrease: '+', 681 | reputationDecrease: '-', 682 | warnIncrease: '+', 683 | warnDecrease: '-', 684 | reputationDetails: '[details >>]', 685 | avatarDefault: '', 686 | userPhotoDefault: '', 687 | onlineActivityDefault: '', 688 | customFieldsInnerHTML: false 689 | }; 690 | 691 | 692 | /** 693 | * @property {string} name - The name of this module. 694 | */ 695 | $cs.module.Profile.prototype.name = '$cs.module.Profile'; 696 | 697 | 698 | /** 699 | * @property {object} reserved - An array of reserved names. 700 | */ 701 | $cs.module.Profile.prototype.reserved = [ 702 | 'values', 703 | 'execute', 704 | 'getValue', 705 | 'hasValue', 706 | 'initialize', 707 | 'replaceValues', 708 | 'setValue', 709 | 'stringToMarkup' 710 | ]; 711 | 712 | 713 | /** 714 | * Executes the checks and loops needed to complete the script. 715 | * @readonly 716 | */ 717 | $cs.module.Profile.prototype.execute = function() { 718 | var portalStyle = document.getElementById('profile-heading'), 719 | defaultStyle = document.getElementById('profilename'); 720 | // Check for personal portal style profiles first, since those are the default. 721 | if (portalStyle) { 722 | var table = portalStyle.parentNode.parentNode.parentNode.parentNode; 723 | 724 | // Acquire the elements needed to read the values in. 725 | var personalInfo = document.getElementById('profile-personalinfo'), 726 | customFields = document.getElementById('profile-customfields'), 727 | statistics = document.getElementById('profile-statistics'), 728 | contactInfo = document.getElementById('profile-contactinfo'), 729 | signature = document.getElementById('sig_popup'), 730 | profileTop = document.getElementById('profile-header'); 731 | 732 | // Hide the original profile. 733 | table.style.display = 'none'; 734 | 735 | // Get the user's id. 736 | var userId = location.href.split("?showuser=")[1]; 737 | this.setValue('userId', userId); 738 | 739 | // personalInfo 740 | var personalInfoDivs = personalInfo.getElementsByTagName('div'), 741 | userPhoto = personalInfoDivs[2].getElementsByTagName('img')[0]; 742 | if (userPhoto) { 743 | this.setValue('userPhoto', userPhoto.src); 744 | } else { 745 | this.setValue('userPhoto', this.config.userPhotoDefault); 746 | } 747 | var warnLevel = personalInfoDivs[4].textContent.split('%')[0]; 748 | if (warnLevel !== '') { 749 | this.setValue('warnLevel', warnLevel); 750 | this.setValue('warnLevelIncrease', '' + this.config.warnIncrease + ''); 751 | this.setValue('warnLevelDecrease', '' + this.config.warnDecrease + ''); 752 | } 753 | 754 | /* 755 | Reputation was wrapped in a div of its own for some reason between 756 | versions 1.5.3 and 1.5.4, so another offset is required here. 757 | */ 758 | var reputationOffset = 0; 759 | if (personalInfoDivs[5].textContent !== 'Options') { 760 | reputationOffset = 1; 761 | var reputationTotal = personalInfoDivs[5].textContent.split('] ')[1].split(' pts')[0]; 762 | if (reputationTotal !== '') { 763 | this.setValue('reputationTotal', reputationTotal); 764 | this.setValue('reputationIncrease', '' + this.config.reputationIncrease + ''); 765 | this.setValue('reputationDecrease', '' + this.config.reputationDecrease + ''); 766 | this.setValue('reputationDetails', '' + this.config.reputationDetails + ''); 767 | } 768 | } 769 | this.setValue('userTitle', personalInfoDivs[10 + reputationOffset].textContent); 770 | this.setValue('location', personalInfoDivs[12 + reputationOffset].textContent.split('Location: ')[1]); 771 | this.setValue('birthday', personalInfoDivs[13 + reputationOffset].textContent.split('Born: ')[1]); 772 | this.setValue('homePage', personalInfoDivs[14 + reputationOffset].innerHTML.split('Website: ')[1]); 773 | if (this.config.htmlEnabled) { 774 | this.setValue('interests', this.stringToMarkup(personalInfoDivs[16 + reputationOffset].innerHTML)); 775 | } else { 776 | this.setValue('interests', personalInfoDivs[16 + reputationOffset].innerHTML); 777 | } 778 | 779 | // customFields 780 | var customFieldsDivs = customFields.getElementsByTagName('div'); 781 | for (var i = 1, numCustomFieldsDivs = customFieldsDivs.length; i < numCustomFieldsDivs; i++) { 782 | if (this.config.customFieldsInnerHTML) { 783 | var customFieldContent = customFieldsDivs[i].innerHTML.split(': ')[1]; 784 | } else { 785 | var customFieldContent = customFieldsDivs[i].textContent.split(': ')[1]; 786 | } 787 | this.setValue('customField' + i, customFieldContent); 788 | } 789 | 790 | // statistics 791 | var statisticsDivs = statistics.getElementsByTagName('div'); 792 | this.setValue('joinDate', statisticsDivs[1].textContent.split('Joined: ')[1]); 793 | this.setValue('onlineStatus', statisticsDivs[2].textContent.split('(')[1].split(')')[0]); 794 | if (statisticsDivs[2].textContent.split('(')[1].split(')')[0].indexOf('Offline') === -1) { 795 | this.setValue('onlineActivity', statisticsDivs[2].textContent.split(') (')[1].split(')')[0]); 796 | } else { 797 | this.setValue('onlineActivity', this.config.onlineActivityDefault); 798 | } 799 | this.setValue('lastActivity', statisticsDivs[3].textContent.split(': ')[1]); 800 | this.setValue('localTime', statisticsDivs[4].textContent.split(': ')[1]); 801 | this.setValue('postCount', statisticsDivs[5].textContent.split('posts')[0]); 802 | this.setValue('postsPerDay', statisticsDivs[5].textContent.split('(')[1].split(' per')[0]); 803 | 804 | // Create a new HTML element, set the appropriate attributes, and inject it into the page. 805 | 806 | // contactInfo 807 | var contactInfoDivs = contactInfo.getElementsByTagName('div'); 808 | this.setValue('userAIM', contactInfoDivs[1].textContent); 809 | this.setValue('userYahoo', contactInfoDivs[2].textContent); 810 | this.setValue('userGtalk', contactInfoDivs[3].textContent); 811 | this.setValue('userMSN', contactInfoDivs[4].textContent); 812 | this.setValue('userSkype', contactInfoDivs[5].textContent); 813 | if (contactInfoDivs[6].textContent.indexOf('Click') !== -1) { 814 | this.setValue('sendMessage', '' + this.config.messageDefault + ''); 815 | } else { 816 | this.setValue('sendMessage', 'Private'); 817 | } 818 | if (contactInfoDivs[7].textContent.indexOf('Click') !== -1) { 819 | this.setValue('sendEmail', '' + this.config.emailDefault + ''); 820 | } else { 821 | this.setValue('sendEmail', 'Private'); 822 | } 823 | 824 | // signature 825 | var signatureContainer = signature.getElementsByTagName('td'); 826 | this.setValue('signature', signatureContainer[2].innerHTML); 827 | 828 | // profileTop 829 | var usernameContainer = profileTop.getElementsByTagName('h3'); 830 | this.setValue('userName', usernameContainer[0].textContent); 831 | var groupContainer = profileTop.getElementsByTagName('strong'); 832 | this.setValue('userGroup', groupContainer[0].textContent); 833 | var avatar = profileTop.previousElementSibling.getElementsByTagName('img')[0]; 834 | if (avatar) { 835 | this.setValue('avatar', avatar.src); 836 | } else { 837 | this.setValue('avatar', this.config.avatarDefault); 838 | } 839 | 840 | // Create a new HTML element, set the appropriate attributes, and inject it into the page. 841 | var newProfile = document.createElement('div'); 842 | newProfile.innerHTML = this.replaceValues(this.html, this.values); 843 | newProfile.id = 'new-profile'; 844 | table.parentNode.appendChild(newProfile); 845 | 846 | // Remove the original profile. 847 | table.parentNode.removeChild(table); 848 | } 849 | else if (defaultStyle) { 850 | // Acquire the elements needed to read the values in. 851 | var topTable = defaultStyle.parentNode.parentNode.parentNode.parentNode, 852 | lineBreak = topTable.nextElementSibling, 853 | middleTable = lineBreak.nextElementSibling, 854 | bottomTable = middleTable.nextElementSibling, 855 | finalDiv = bottomTable.nextElementSibling; 856 | 857 | // Hide the original profile. 858 | topTable.style.display = 'none'; 859 | lineBreak.style.display = 'none'; 860 | middleTable.style.display = 'none'; 861 | bottomTable.style.display = 'none'; 862 | finalDiv.style.display = 'none'; 863 | 864 | // Get the user's id. 865 | var userId = location.href.split("?showuser=")[1]; 866 | this.setValue('userId', userId); 867 | 868 | // Read the values from the top table. 869 | var topTableCells = topTable.getElementsByTagName('td'), 870 | userPhoto = topTableCells[0].getElementsByTagName('img')[0], 871 | topTableDivs = topTableCells[1].getElementsByTagName('div'); 872 | if (userPhoto) { 873 | this.setValue('userPhoto', userPhoto.src); 874 | } else { 875 | this.setValue('userPhoto', this.config.userPhotoDefault); 876 | } 877 | this.setValue('userName', topTableDivs[0].textContent); 878 | /* 879 | Profile links don't appear on the personal portal style profiles, 880 | so it's been commented out for compatibility between the two halves 881 | of this module. 882 | 883 | this.setValue('profileLinks', topTableDivs[1].innerHTML); 884 | */ 885 | 886 | // Read the values from the middle table. 887 | var middleTables = middleTable.getElementsByTagName('table'), 888 | topLeftTable = middleTables[0], 889 | topRightTable = middleTables[1], 890 | bottomLeftTable = middleTables[2], 891 | bottomRightTable = middleTables[3]; 892 | 893 | // Read the values in the top left table. 894 | var topLeftCells = topLeftTable.getElementsByTagName('td'); 895 | this.setValue('postCount', topLeftCells[2].textContent.split('(')[0]); 896 | this.setValue('postsPerDay', topLeftCells[4].textContent); 897 | this.setValue('joinDate', topLeftCells[6].textContent); 898 | this.setValue('localTime', topLeftCells[8].textContent); 899 | var onlineStatus = topLeftCells[10].textContent; 900 | this.setValue('onlineStatus', onlineStatus.split('(')[1].split(')')[0]); 901 | if (onlineStatus.split('(')[1].split(')')[0].indexOf('Offline') === -1) { 902 | this.setValue('onlineActivity', onlineStatus.split(') (')[1].split(')')[0]); 903 | } else { 904 | this.setValue('onlineActivity', this.config.onlineActivityDefault); 905 | } 906 | 907 | // Read the values in the top right table. 908 | var topRightCells = topRightTable.getElementsByTagName('td'); 909 | if (topRightCells[2].textContent.indexOf('Click') !== -1) { 910 | this.setValue('sendEmail', 'Click here'); 911 | } else { 912 | this.setValue('sendEmail', 'Private'); 913 | } 914 | this.setValue('userSkype', topRightCells[4].textContent); 915 | this.setValue('userAIM', topRightCells[6].textContent); 916 | this.setValue('userGtalk', topRightCells[8].textContent); 917 | this.setValue('userYahoo', topRightCells[10].textContent); 918 | this.setValue('userMSN', topRightCells[12].textContent); 919 | if (topRightCells[14].textContent.indexOf('Click') !== -1) { 920 | this.setValue('sendMessage', 'Click here'); 921 | } else { 922 | this.setValue('sendMessage', 'Private'); 923 | } 924 | 925 | // Read the values in the bottom left table. 926 | var bottomLeftCells = bottomLeftTable.getElementsByTagName('td'); 927 | this.setValue('homePage', bottomLeftCells[2].innerHTML); 928 | this.setValue('birthday', bottomLeftCells[4].textContent); 929 | this.setValue('location', bottomLeftCells[6].textContent); 930 | 931 | // If HTML in the interests field is enabled, make sure to parse it correctly. 932 | if (this.config.htmlEnabled) { 933 | this.setValue('interests', this.stringToMarkup(bottomLeftCells[8].innerHTML)); 934 | } else { 935 | this.setValue('interests', bottomLeftCells[8].innerHTML); 936 | } 937 | 938 | /* 939 | More trouble caused by feature bloat. Awards get inserted at this point, 940 | but instead of using a switch statement here it's more efficient to 941 | declare an offset and watch it carefully since the rows following this 942 | one are not presented in an arbitrary order. 943 | */ 944 | var awardOffset = 0; 945 | if (bottomLeftCells[9].textContent.indexOf('Awards') !== -1) { 946 | awardOffset = 2; 947 | this.setValue('awards', bottomLeftCells[10].innerHTML); 948 | } 949 | 950 | // Wrap up the default profile fields. 951 | /* 952 | Last post doesn't appear on the personal portal style profiles, 953 | so it's been commented out for compatibility between the two halves 954 | of this module. 955 | 956 | this.setValue('lastPost', bottomLeftCells[10 + awardOffset].textContent); 957 | */ 958 | this.setValue('lastActivity', bottomLeftCells[12 + awardOffset].textContent); 959 | 960 | // Custom profile fields are simple- just iterate over the remainder in a loop. 961 | for (var i = 14 + awardOffset, fieldNum = 1; i < bottomLeftCells.length; i += 2, fieldNum++) { 962 | if (this.config.customFieldsInnerHTML) { 963 | var customFieldContent = bottomLeftCells[i].innerHTML; 964 | } else { 965 | var customFieldContent = bottomLeftCells[i].textContent; 966 | } 967 | this.setValue('customField' + fieldNum, customFieldContent); 968 | } 969 | 970 | // Read the values in the bottom right table. 971 | var bottomRightCells = bottomRightTable.getElementsByTagName('td'); 972 | this.setValue('userGroup', bottomRightCells[2].textContent); 973 | this.setValue('userTitle', bottomRightCells[4].textContent); 974 | var avatar = bottomRightCells[6].getElementsByTagName('img')[0]; 975 | if (avatar) { 976 | this.setValue('avatar', avatar.src); 977 | } else { 978 | this.setValue('avatar', this.config.avatarDefault); 979 | } 980 | 981 | /* 982 | Another switch statement is used for the final potential three rows. 983 | Their order may be arbitrary, so it's required. 984 | */ 985 | for (var i = 7; i < bottomRightCells.length; i++) { 986 | switch (true) { 987 | case (bottomRightCells[i].textContent === 'Rep:'): 988 | var reputation = bottomRightCells[i + 1].textContent; 989 | if (reputation.indexOf('pts') !== -1) { 990 | this.setValue('reputationTotal', reputation.split('pts [')[0]); 991 | } else { 992 | this.setValue('reputationTotal', '0'); 993 | } 994 | this.setValue('reputationIncrease', '' + this.config.reputationIncrease + ''); 995 | this.setValue('reputationDecrease', '' + this.config.reputationDecrease + ''); 996 | this.setValue('reputationDetails', '' + this.config.reputationDetails + ''); 997 | break; 998 | case (bottomRightCells[i].textContent === 'Warn Level'): 999 | this.setValue('warnLevel', bottomRightCells[i + 1].textContent.split('%')[0]); 1000 | this.setValue('warnLevelIncrease', '' + this.config.warnIncrease + ''); 1001 | this.setValue('warnLevelDecrease', '' + this.config.warnDecrease + ''); 1002 | break; 1003 | /* 1004 | Moderator notes don't appear on the personal portal style profiles, 1005 | so they've been commented out for compatibility between the two halves 1006 | of this module. 1007 | 1008 | case (bottomRightCells[i].textContent === 'Moderator Notes:'): 1009 | this.setValue('moderatorNotes', bottomRightCells[i + 1].getElementsByTagName('textarea')[0].value); 1010 | break; 1011 | */ 1012 | } 1013 | } 1014 | 1015 | // Get the user's signature. 1016 | var bottomTableCells = bottomTable.getElementsByTagName('td'); 1017 | this.setValue('signature', bottomTableCells[2].innerHTML); 1018 | 1019 | // Create a new HTML element, set the appropriate attributes, and inject it into the page. 1020 | var newProfile = document.createElement('div'); 1021 | newProfile.innerHTML = this.replaceValues(this.html, this.values); 1022 | newProfile.id = 'new-profile'; 1023 | topTable.parentNode.appendChild(newProfile); 1024 | 1025 | // Remove the original profile. 1026 | topTable.parentNode.removeChild(topTable); 1027 | lineBreak.parentNode.removeChild(lineBreak); 1028 | middleTable.parentNode.removeChild(middleTable); 1029 | bottomTable.parentNode.removeChild(bottomTable); 1030 | finalDiv.parentNode.removeChild(finalDiv); 1031 | } 1032 | }; 1033 | 1034 | 1035 | /** 1036 | * Initialization function. Reads user-defined settings in for processing and begins script execution. 1037 | * @arg {object} settings - An object with user-defined settings as properties. 1038 | * @readonly 1039 | */ 1040 | $cs.module.Profile.prototype.initialize = function(settings) { 1041 | // Call $cs.module.Default's initialize method instead. 1042 | $cs.module.Default.prototype.initialize.call(this, settings); 1043 | }; 1044 | 1045 | 1046 | /** 1047 | * Converts a string containing encoded HTML markup into actual HTML markup. 1048 | * @arg {string} string - A string containing encoded HTML markup. 1049 | * @return {string} 1050 | * @readonly 1051 | */ 1052 | $cs.module.Profile.prototype.stringToMarkup = function(string) { 1053 | // Create a throwaway element and set its innerHTML. 1054 | var temp = document.createElement('div'); 1055 | temp.innerHTML = string; 1056 | var result = ''; 1057 | for(var i = 0, length = temp.childNodes.length; i < length; i++) { 1058 | if(temp.childNodes[i].nodeValue) { 1059 | result += temp.childNodes[i].nodeValue; 1060 | } 1061 | } 1062 | temp = ''; 1063 | return result; 1064 | } 1065 | 1066 | 1067 | // Extend the custom topics module with the default properties and methods. 1068 | $cs.extendModule($cs.module.Topics, $cs.module.Default); 1069 | 1070 | 1071 | /** 1072 | * @namespace 1073 | * @property {object} config - Default configuration values. 1074 | * @property {string} config.keyPrefix - The default prefix for value keys. 1075 | * @property {string} config.keySuffix - The default suffix for value keys. 1076 | * @property {string} config.announcementsDefault - The default title row text for announcements. 1077 | * @property {string} config.pinnedDefault - The default title row text for pinned topics. 1078 | * @property {string} config.regularDefault - The default title row text for regular topics. 1079 | * @property {string} config.noTopics - The default message displayed when a forum contains no topics. 1080 | * @property {string} config.noActiveTopics - The default message displayed when the active topics page is blank. 1081 | * @property {string} config.paginationDefault - The default text displayed when pagination is blank. 1082 | * @property {boolean} config.activeTopics - Whether to apply changes the to the active topics page. 1083 | */ 1084 | $cs.module.Topics.prototype.config = { 1085 | keyPrefix: '{{', 1086 | keySuffix: '}}', 1087 | announcementsDefault: 'Announcements', 1088 | pinnedDefault: 'Important Topics', 1089 | regularDefault: 'Forum Topics', 1090 | 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.', 1091 | noActiveTopics: 'There were no active topics during those date ranges', 1092 | paginationDefault: '', 1093 | activeTopics: false 1094 | }; 1095 | 1096 | 1097 | /** 1098 | * @property {string} name - The name of this module. 1099 | */ 1100 | $cs.module.Topics.prototype.name = '$cs.module.Topics'; 1101 | 1102 | 1103 | /** 1104 | * @property {object} reserved - An array of reserved names. 1105 | */ 1106 | $cs.module.Topics.prototype.reserved = [ 1107 | 'values', 1108 | 'execute', 1109 | 'getValue', 1110 | 'hasValue', 1111 | 'initialize', 1112 | 'replaceValues', 1113 | 'setValue', 1114 | ]; 1115 | 1116 | 1117 | /** 1118 | * Executes the checks and loops needed to complete the script. 1119 | * @readonly 1120 | */ 1121 | $cs.module.Topics.prototype.execute = function() { 1122 | var topicList = document.getElementById('topic-list'); 1123 | // If we couldn't find the default topic list, check what page we're on. 1124 | if (!topicList) { 1125 | if (this.config.activeTopics && window.location.href.indexOf('act=Search&CODE=getactive') > -1) { 1126 | var forms = document.getElementsByTagName('form'); 1127 | for (var i = 0; i < forms.length; i++) { 1128 | if (forms[i].action.indexOf('act=Search&CODE=getactive') > -1) { 1129 | topicList = forms[i].nextElementSibling.nextElementSibling; 1130 | } 1131 | } 1132 | 1133 | // I don't like flags, but this is the best way to handle this here. 1134 | var viewingActiveTopics = true; 1135 | } 1136 | } 1137 | if (topicList) { 1138 | /* 1139 | Acquire the elements needed to read the values in and initialize some variables 1140 | for formatting the script output. 1141 | */ 1142 | var table = topicList.getElementsByTagName('table')[0], 1143 | rows = [], 1144 | topicsContent = '', 1145 | rowClass = ' regular-topic'; 1146 | 1147 | /* 1148 | Loop through the direct children of the table to get the correct row count. 1149 | This is necessary due to extraneous tables on the active topics view. 1150 | */ 1151 | for (var t = 0; t < table.firstElementChild.childNodes.length; t++) { 1152 | if (table.firstElementChild.childNodes[t].nodeType === 1 && table.firstElementChild.childNodes[t].tagName === 'TR') { 1153 | rows.push(table.firstElementChild.childNodes[t]); 1154 | } 1155 | } 1156 | 1157 | // Hide the original table. 1158 | table.style.display = 'none'; 1159 | 1160 | // Loop through each row and either read the values or output a title row. 1161 | for (var i = 1, numRows = rows.length; i < numRows; i++) { 1162 | // Get all the cells in each row. If a fourth cell exists, read the values in. 1163 | var cells = rows[i].getElementsByTagName('td'); 1164 | if (cells[3]) { 1165 | // Regular topic listing. 1166 | if (!viewingActiveTopics) { 1167 | this.setValue('folder', cells[0].innerHTML); 1168 | this.setValue('marker', cells[1].innerHTML); 1169 | var topicTitle = cells[2].getElementsByTagName('a')[0]; 1170 | if (!topicTitle.getAttribute('title')) { 1171 | topicTitle = cells[2].getElementsByTagName('a')[1]; 1172 | } 1173 | this.setValue('topicId', topicTitle.getAttribute('href').split('showtopic=')[1]); 1174 | this.setValue('topicTitle', '' + topicTitle.textContent + ''); 1175 | var topicSpans = cells[2].getElementsByTagName('span'); 1176 | if (topicSpans[0].textContent.indexOf('(Pages ') !== -1) { 1177 | this.setValue('pagination', topicSpans[0].innerHTML); 1178 | this.setValue('topicDescription', topicSpans[1].textContent); 1179 | } else { 1180 | this.setValue('pagination', this.config.paginationDefault); 1181 | this.setValue('topicDescription', topicSpans[0].textContent); 1182 | } 1183 | var forumName = topicList.firstElementChild.textContent.trim(), 1184 | parentForum = '' + forumName + ''; 1185 | this.setValue('parentForum', parentForum); 1186 | this.setValue('topicAuthor', cells[3].innerHTML); 1187 | this.setValue('replyCount', cells[4].textContent); 1188 | this.setValue('viewCount', cells[5].textContent); 1189 | 1190 | this.setValue('lastReplyDate', cells[6].getElementsByTagName('span')[0].firstChild.nodeValue); 1191 | this.setValue('lastReplyAuthor', cells[6].getElementsByTagName('b')[0].innerHTML); 1192 | this.setValue('moderatorCheckbox', cells[7].innerHTML); 1193 | } 1194 | // Active topics search page listing. 1195 | else if (this.config.activeTopics) { 1196 | this.setValue('folder', cells[0].innerHTML); 1197 | this.setValue('marker', cells[1].innerHTML); 1198 | var topicTitleLinks = cells[2].getElementsByTagName('a'); 1199 | if (topicTitleLinks[0].href.indexOf('view=getnewpost') === -1) { 1200 | var topicTitle = topicTitleLinks[0]; 1201 | } else { 1202 | var topicTitle = topicTitleLinks[1]; 1203 | } 1204 | this.setValue('topicId', topicTitle.getAttribute('href').split('showtopic=')[1].split('&')[0]); 1205 | this.setValue('topicTitle', '' + topicTitle.textContent + ''); 1206 | var topicSpans = cells[2].getElementsByTagName('span'); 1207 | if (topicSpans[0].textContent.indexOf('(Pages ') !== -1) { 1208 | this.setValue('pagination', topicSpans[0].innerHTML); 1209 | this.setValue('topicDescription', topicSpans[1].textContent); 1210 | } else { 1211 | this.setValue('pagination', this.config.paginationDefault); 1212 | this.setValue('topicDescription', topicSpans[0].textContent); 1213 | } 1214 | this.setValue('parentForum', cells[5].innerHTML); 1215 | this.setValue('topicAuthor', cells[6].innerHTML); 1216 | this.setValue('replyCount', cells[7].textContent); 1217 | this.setValue('viewCount', cells[8].textContent); 1218 | 1219 | this.setValue('lastReplyDate', cells[9].firstChild.nodeValue); 1220 | this.setValue('lastReplyDate', cells[9].firstChild.nodeValue); 1221 | var author = cells[9].getElementsByTagName('b'); 1222 | if (author[1]) { 1223 | this.setValue('lastReplyAuthor', author[1].innerHTML); 1224 | } else { 1225 | this.setValue('lastReplyAuthor', author[0].innerHTML); 1226 | } 1227 | this.setValue('moderatorCheckbox', ''); 1228 | } 1229 | 1230 | // Perform string replacement and append the new row to the output. 1231 | topicsContent += '
' + 1232 | this.replaceValues(this.html, this.values) + 1233 | '
'; 1234 | } else if (i !== numRows - 1 && !viewingActiveTopics) { 1235 | // Output the appropriate title row for the topics that follow. 1236 | var titleContents = cells[2].getElementsByTagName('b')[0].textContent; 1237 | switch (titleContents) { 1238 | case 'Announcements': 1239 | rowClass = ' announcement-topic'; 1240 | topicsContent += '
' + this.config.announcementsDefault + '
'; 1241 | break; 1242 | case 'Important Topics': 1243 | rowClass = ' pinned-topic'; 1244 | topicsContent += '
' + this.config.pinnedDefault + '
'; 1245 | break; 1246 | default: 1247 | rowClass = ' regular-topic'; 1248 | topicsContent += '
' + this.config.regularDefault + '
'; 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', '
' + cells[3 + cellOffset].firstElementChild.innerHTML + '
'); 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+='
'+this.config.insertBefore+"
");for(var n=1,a=s.length-1;a>n;n++){var o=s[n].getElementsByTagName("td");this.setValue("forumMarker",o[0].innerHTML),this.setValue("forumTitle",o[1].firstElementChild.innerHTML);var l=o[1].getElementsByClassName("x-viewing-forum")[0];l?this.setValue("forumViewing",l.innerHTML.split("(")[1].split(" Vi")[0]):this.setValue("forumViewing",this.config.viewingDefault),this.setValue("forumId",this.values["{{forumTitle}}"].split("showforum=")[1].split('" alt="')[0]),this.setValue("forumDescription",o[1].getElementsByClassName("forum-desc")[0].innerHTML);var r=o[1].getElementsByClassName("subforums")[0],u="";if(r){for(var c=r.getElementsByTagName("a"),d=0,h=c.length;h>d;d++)if("subforums-macro"!=c[d].classList[0]){var m=this.makeLink(c[d]);u+=d===h-1?m:m+this.config.subforumSeparator}this.setValue("subforums",u)}else this.setValue("subforums",this.config.subforumsNone);var p=o[1].getElementsByClassName("forum-led-by")[0];p&&p.textContent?(this.setValue("moderators",p.innerHTML.split("Forum Led by: ")[1]),this.setValue("redirectHits",0)):p?this.setValue("redirectHits",0):(this.setValue("moderators",this.config.moderatorsNone),this.setValue("redirectHits",o[4].textContent.split("Redirected Hits: ")[1])),this.setValue("topicCount",o[2].textContent),this.setValue("replyCount",o[3].textContent);var f=o[4],g=f.getElementsByTagName("a");g.length>0?(this.setValue("lastPostDate",f.childNodes[0].nodeValue),-1===f.textContent.indexOf("Protected Forum")?(this.setValue("lastPostTitle",this.makeLink(g[1])),this.setValue("lastPostURL",g[1].href.slice(0,-16)),g[2]?this.setValue("lastPostAuthor",this.makeLink(g[2])):this.setValue("lastPostAuthor",f.textContent.split("By: ")[1])):(this.setValue("lastPostTitle",this.config.passwordTitle),this.setValue("lastPostURL",this.config.urlDefault),this.setValue("lastPostAuthor",this.makeLink(g[0])))):(this.setValue("lastPostDate",this.config.dateDefault),this.setValue("lastPostTitle",this.config.titleDefault),this.setValue("lastPostURL",this.config.urlDefault),this.setValue("lastPostAuthor",this.config.authorDefault));var v="";-1!==this.values["{{forumMarker}}"].indexOf("'+this.replaceValues(this.html,this.values)+"
"}this.config.insertAfter&&""!==this.config.insertAfter&&(i+='
'+this.config.insertAfter+"
");var y=document.createElement("div");y.innerHTML=i,y.id="category-"+(t+1),y.className="new-category",e.parentNode.appendChild(y),e.parentNode.removeChild(e)},$cs.extendModule($cs.module.Stats,$cs.module.Default),$cs.module.Stats.prototype.config={keyPrefix:"{{",keySuffix:"}}"},$cs.module.Stats.prototype.name="$cs.module.Stats",$cs.module.Stats.prototype.reserved=["values","execute","getValue","hasValue","initialize","replaceValues","setValue"],$cs.module.Stats.prototype.execute=function(){var e=document.getElementById("boardstats");if(e){var t=e.lastElementChild;t.style.display="none";for(var s=t.getElementsByTagName("td"),i=0,n=s.length;n>i;i+=3)switch(!0){case-1!==s[i].textContent.indexOf("active in the past"):this.setValue("totalUsers",s[i].textContent.split(" user")[0]);var a=s[i+2].getElementsByTagName("b");this.setValue("totalUsersGuests",a[0].textContent),this.setValue("totalUsersRegistered",a[1].textContent),this.setValue("totalUsersAnonymous",a[2].textContent);var o=s[i+2].getElementsByClassName("thin")[0];this.setValue("onlineList",o.innerHTML.split("

")[0]),this.setValue("onlineLegend",o.innerHTML.split("

")[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+='
'+this.replaceValues(this.html,this.values)+"
"}else if(s===u-1||i)i?this.config.activeTopics&&(o+='
'+this.config.noActiveTopics+"
"):o+='
'+this.config.noTopics+"
";else{var v=c[2].getElementsByTagName("b")[0].textContent;switch(v){case"Announcements":l=" announcement-topic",o+='
'+this.config.announcementsDefault+"
";break;case"Important Topics":l=" pinned-topic",o+='
'+this.config.pinnedDefault+"
";break;default:l=" regular-topic",o+='
'+this.config.regularDefault+"
"}}}var y=document.createElement("div");y.id="new-topics",y.innerHTML=o,n.parentNode.insertBefore(y,n),n.parentNode.removeChild(n),this.config.activeTopics&&i&&e.removeChild(e.lastElementChild)}},$cs.module.Topics.prototype.initialize=function(e){$cs.module.Default.prototype.initialize.call(this,e)},$cs.extendModule($cs.module.Posts,$cs.module.Default),$cs.module.Posts.prototype.config={keyPrefix:"{{",keySuffix:"}}",permaLinkDefault:"Permalink",postSignatureDefault:"",quickEdit:!1,formatQuoteCodeTags:!1},$cs.module.Posts.prototype.name="$cs.module.Posts",$cs.module.Posts.prototype.reserved=["attachCodeEventListeners","createEditForm","execute","Fetch","formatQuoteCodeTags","getValue","hasValue","initialize","queryString","replaceValues","setValue","values"],$cs.module.Posts.prototype.queryString=function(e,t){t=t.replace(/[\[]/,"\\[").replace(/[\]]/,"\\]");var s=RegExp("[\\?&]"+t+"=([^&#]*)"),i=s.exec(e);return null===i?"":decodeURIComponent(i[1].replace(/\+/g," "))},$cs.module.Posts.prototype.Fetch={request:function(){return new(XMLHttpRequest||ActiveXObject)("MSXML2.XMLHTTP.3.0")},get:function(e,t){var s=this.request();s.open("GET",e),s.setRequestHeader("X-Requested-With","XMLHttpRequest"),s.onreadystatechange=function(){s.readyState>3&&t&&"function"==typeof t&&t(s.responseText)},s.send()},post:function(e,t,s){var i=this.request(),t=this.formatData(t);i.open("POST",e),i.setRequestHeader("X-Requested-With","XMLHttpRequest"),i.setRequestHeader("Content-type","application/x-www-form-urlencoded"),i.setRequestHeader("Content-length",t.length),i.onreadystatechange=function(){i.readyState>3&&s&&"function"==typeof s&&s(i.responseText)},i.send(t)},filter:function(e){for(var t="",s=0;s127?"&#"+i+";":e.charAt(s)}return encodeURIComponent?encodeURIComponent(t):escape(t)},formatData:function(e){var t=[];for(var s in e)t.push(this.filter(s)+"="+this.filter(e[s]));return t.join("&")}},$cs.module.Posts.prototype.createEditForm=function(e,t,s,i,n,a){var o=document.createElement("form"),l=document.createElement("textarea"),r=document.createElement("div"),u=document.createElement("button"),c=document.createElement("button"),d=document.createElement("button");o.addEventListener("submit",function(e){e.preventDefault()});var h=document.createElement("div");h.innerHTML=n;var m=h.firstElementChild.value,p=h.lastElementChild.value;l.innerHTML=p,l.style.boxSizing="border-box",u.innerHTML="Edit",c.innerHTML="Cancel",d.innerHTML="Full Edit";var f=function(e){var t="(?:\\s*
)((?:.|\n)*?)(?:
\n {8}(?:){2}(?:.|\n)*?(?:\n
)|(?:
\\s*))",i=RegExp(t).exec(e);if(!i)return void console.error("Could not GET edited post. Read failed.");var n=document.createElement("div");if(n.classList.add("cs-quick-edit"),n.innerHTML=i[1],u.parentNode.parentNode.parentNode.replaceChild(n,u.parentNode.parentNode),this.config.formatQuoteCodeTags){var a=n.getElementsByTagName("table");this.formatQuoteCodeTags(a),this.attachCodeEventListeners()}},g=function(n){n.preventDefault();var a="/?act=Post&quickedit=1&CODE=09&f="+e+"&t="+t+"&p="+s+"&st="+i+"&auth_key="+m,o={enablesig:"yes",Post:l.value};this.Fetch.post(a,o,function(e){var t=document.createElement("div");t.innerHTML=e;var s=t.getElementsByTagName("a");if(1!==s.length)return void console.error("Couldn't POST changes. Edit failed.");var i=s[0].href+"&nocache="+(new Date).getTime();this.Fetch.get(i,f.bind(this))}.bind(this))};return u.addEventListener("click",g.bind(this)),c.addEventListener("click",function(e){e.preventDefault(),this.parentNode.parentNode.parentNode.replaceChild(a,this.parentNode.parentNode)}),d.addEventListener("click",function(n){n.preventDefault(),window.location.href="/?act=Post&CODE=08&f="+e+"&t="+t+"&p="+s+"&st="+i}.bind(this)),r.appendChild(u),r.appendChild(c),r.appendChild(d),o.appendChild(l),o.appendChild(r),o},$cs.module.Posts.prototype.formatQuoteCodeTags=function(e){for(var t=e.length;t>-1;t--)if(void 0!==e[t]&&"QUOTE-WRAP"==e[t].id){e[t].style.display="none";var s=e[t].firstElementChild.firstElementChild.firstElementChild.innerHTML.slice(14,-1).split(" @ "),i=s[0],n=s[1];i||(i=""),n||(n="");var a=e[t].firstElementChild.lastElementChild.firstElementChild.innerHTML,o=document.createElement("div");o.classList.add("quote-wrapper"),o.innerHTML='
'+i+''+n+'
'+a+"
",e[t].parentNode.insertBefore(o,e[t].nextSibling),e[t].parentNode.removeChild(e[t])}else if(void 0!==e[t]&&"CODE-WRAP"==e[t].id){e[t].style.display="none";var l=e[t].firstElementChild.lastElementChild.firstElementChild.innerHTML,r=document.createElement("div");r.classList.add("code-wrapper");var u=document.createElement("code");u.classList.add("code-title"),u.style.cursor="pointer",u.appendChild(document.createTextNode("Code (Click to highlight)")),r.appendChild(u);var c=document.createElement("div"),d=document.createElement("pre"),h=document.createElement("code");h.innerHTML=l,d.appendChild(h),c.appendChild(d),c.classList.add("code-contents"),r.appendChild(c),e[t].parentNode.insertBefore(r,e[t].nextSibling),e[t].parentNode.removeChild(e[t])}},$cs.module.Posts.prototype.attachCodeEventListeners=function(){for(var e=document.getElementsByClassName("code-wrapper"),t=0,s=e.length;s>t;t++)e[t].firstElementChild.addEventListener("click",function(e){e.preventDefault();var t,s;document.body.createTextRange?(t=document.body.createTextRange(),t.moveToElementText(this.nextElementSibling),t.select()):window.getSelection&&(s=window.getSelection(),t=document.createRange(),t.selectNodeContents(this.nextElementSibling),s.removeAllRanges(),s.addRange(t))})},$cs.module.Posts.prototype.execute=function(){if(-1!==window.location.href.indexOf("showtopic")||-1!==window.location.href.indexOf("ST")){var e=document.getElementsByClassName("post-normal"),t=document.createElement("div");t.id="new-posts";for(var s=0,i=e.length;i>s;s++){e[s].style.display="none";for(var n=e[s].firstElementChild,a=n.getElementsByTagName("tr"),o=[],l=0,r=a.length;r>l;l++)for(var u=a[l].childNodes,c=0,d=u.length;d>c;c++){var h=u[c];1===h.nodeType&&"TD"===h.tagName&&h.parentNode.parentNode.parentNode===n&&o.push(h)}var m,p=o[0].getElementsByTagName("a"),f=p[0].name.split("entry")[1];m=-1!==window.location.search.indexOf("showtopic")?this.queryString(window.location.search,"showtopic"):this.queryString(window.location.search,"t"),this.setValue("postId",f),p.length>1?this.setValue("postAuthor",o[0].innerHTML.split('normalname">')[1].slice(0,-7)):this.setValue("postAuthor",o[0].innerHTML.split('unreg">')[1].slice(0,-7)),this.setValue("permaLink",''+this.config.permaLinkDefault+""),this.setValue("postDate",o[1].firstElementChild.textContent.split("Posted: ")[1]),this.setValue("postButtonsTop",o[1].lastElementChild.innerHTML);var g=0;-1!==o[2].innerHTML.indexOf("input")?(this.setValue("postCheckbox",o[2].innerHTML),g=1):this.setValue("postCheckbox",""),this.setValue("postMiniprofile",o[2+g].firstElementChild.innerHTML),this.config.quickEdit?this.setValue("postContent",'
'+o[3+g].firstElementChild.innerHTML+"
"):this.setValue("postContent",o[3+g].firstElementChild.innerHTML);var v=o[3+g].lastElementChild;v.previousElementSibling?this.setValue("postSignature",v.innerHTML):this.setValue("postSignature",this.config.postSignatureDefault),this.setValue("postIp",o[4+g].textContent),this.setValue("postButtonsBottom",o[5+g].firstElementChild.innerHTML);var y=document.createElement("div");if(y.id="entry"+f,y.classList.add("new-post",e[s].classList[1]),y.innerHTML=this.replaceValues(this.html,this.values),t.appendChild(y),this.config.quickEdit)for(var p=y.getElementsByTagName("a"),C=0;C hey', function() { 6 | describe('customIndex.values', function() { 7 | it('should be an object', function () { 8 | expect(customIndex.values).to.be.a('object'); 9 | }); 10 | }); 11 | }); -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jcink Custom Structure Tests 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | --------------------------------------------------------------------------------