Template Plugins
diff --git a/6.0/search/search_index.json b/6.0/search/search_index.json
index fe7c063d..f86167dc 100644
--- a/6.0/search/search_index.json
+++ b/6.0/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"WoltLab Suite 6.0 Documentation","text":""},{"location":"#introduction","title":"Introduction","text":"This documentation explains the basic API functionality and the creation of own packages. It is expected that you are somewhat experienced with PHP, object-oriented programming and MySQL.
Head over to the quick start tutorial to learn more.
"},{"location":"#about-woltlab-suite","title":"About WoltLab Suite","text":"WoltLab Suite Core as well as most of the other packages are available on GitHub and are licensed under the terms of the GNU Lesser General Public License 2.1.
"},{"location":"getting-started/","title":"Creating a simple package","text":""},{"location":"getting-started/#setup-and-requirements","title":"Setup and Requirements","text":"This guide will help you to create a simple package that provides a simple test page. It is nothing too fancy, but you can use it as the foundation for your next project.
There are some requirements you should met before starting:
- Text editor with syntax highlighting for PHP, Notepad++ is a solid pick
*.php
and *.tpl
should be encoded with ANSI/ASCII *.xml
are always encoded with UTF-8, but omit the BOM (byte-order-mark) - Use tabs instead of spaces to indent lines
- It is recommended to set the tab width to
8
spaces, this is used in the entire software and will ease reading the source files - An active installation of WoltLab Suite
- An application to create
*.tar
archives, e.g. 7-Zip on Windows
"},{"location":"getting-started/#the-packagexml-file","title":"The package.xml File","text":"We want to create a simple page that will display the sentence \"Hello World\" embedded into the application frame. Create an empty directory in the workspace of your choice to start with.
Create a new file called package.xml
and insert the code below:
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<package xmlns=\"http://www.woltlab.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.woltlab.com http://www.woltlab.com/XSD/2019/package.xsd\" name=\"com.example.test\">\n<packageinformation>\n<!-- com.example.test -->\n<packagename>Simple Package</packagename>\n<packagedescription>A simple package to demonstrate the package system of WoltLab Suite Core</packagedescription>\n<version>1.0.0</version>\n<date>2019-04-28</date>\n</packageinformation>\n<authorinformation>\n<author>Your Name</author>\n<authorurl>http://www.example.com</authorurl>\n</authorinformation>\n<excludedpackages>\n<excludedpackage version=\"6.0.0 Alpha 1\">com.woltlab.wcf</excludedpackage>\n</excludedpackages>\n<instructions type=\"install\">\n<instruction type=\"file\" />\n<instruction type=\"template\" />\n<instruction type=\"page\" />\n</instructions>\n</package>\n
There is an entire chapter on the package system that explains what the code above does and how you can adjust it to fit your needs. For now we'll keep it as it is.
"},{"location":"getting-started/#the-php-class","title":"The PHP Class","text":"The next step is to create the PHP class which will serve our page:
- Create the directory
files
in the same directory where package.xml
is located - Open
files
and create the directory lib
- Open
lib
and create the directory page
- Within the directory
page
, please create the file TestPage.class.php
Copy and paste the following code into the TestPage.class.php
:
<?php\nnamespace wcf\\page;\nuse wcf\\system\\WCF;\n\n/**\n * A simple test page for demonstration purposes.\n *\n * @author YOUR NAME\n * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>\n */\nclass TestPage extends AbstractPage {\n /**\n * @var string\n */\n protected $greet = '';\n\n /**\n * @inheritDoc\n */\n public function readParameters() {\n parent::readParameters();\n\n if (isset($_GET['greet'])) $this->greet = $_GET['greet'];\n }\n\n /**\n * @inheritDoc\n */\n public function readData() {\n parent::readData();\n\n if (empty($this->greet)) {\n $this->greet = 'World';\n }\n }\n\n /**\n * @inheritDoc\n */\n public function assignVariables() {\n parent::assignVariables();\n\n WCF::getTPL()->assign([\n 'greet' => $this->greet\n ]);\n }\n}\n
The class inherits from wcf\\page\\AbstractPage, the default implementation of pages without form controls. It defines quite a few methods that will be automatically invoked in a specific order, for example readParameters()
before readData()
and finally assignVariables()
to pass arbitrary values to the template.
The property $greet
is defined as World
, but can optionally be populated through a GET variable (index.php?test/&greet=You
would output Hello You!
). This extra code illustrates the separation of data processing that takes place within all sort of pages, where all user-supplied data is read from within a single method. It helps organizing the code, but most of all it enforces a clean class logic that does not start reading user input at random places, including the risk to only escape the input of variable $_GET['foo']
4 out of 5 times.
Reading and processing the data is only half the story, now we need a template to display the actual content for our page. You don't need to specify it yourself, it will be automatically guessed based on your namespace and class name, you can read more about it later.
Last but not least, you must not include the closing PHP tag ?>
at the end, it can cause PHP to break on whitespaces and is not required at all.
"},{"location":"getting-started/#the-template","title":"The Template","text":"Navigate back to the root directory of your package until you see both the files
directory and the package.xml
. Now create a directory called templates
, open it and create the file test.tpl
.
{include file='header'}\n\n<div class=\"section\">\n Hello {$greet}!\n</div>\n\n{include file='footer'}\n
Templates are a mixture of HTML and Smarty-like template scripting to overcome the static nature of raw HTML. The above code will display the phrase Hello World!
in the application frame, just as any other page would render. The included templates header
and footer
are responsible for the majority of the overall page functionality, but offer a whole lot of customization abilities to influence their behavior and appearance.
"},{"location":"getting-started/#the-page-definition","title":"The Page Definition","text":"The package now contains the PHP class and the matching template, but it is still missing the page definition. Please create the file page.xml
in your project's root directory, thus on the same level as the package.xml
.
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<data xmlns=\"http://www.woltlab.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.woltlab.com http://www.woltlab.com/XSD/2019/page.xsd\">\n<import>\n<page identifier=\"com.example.test.Test\">\n<controller>wcf\\page\\TestPage</controller>\n<name language=\"en\">Test Page</name>\n<pageType>system</pageType>\n</page>\n</import>\n</data>\n
You can provide a lot more data for a page, including logical nesting and dedicated handler classes for display in menus.
"},{"location":"getting-started/#building-the-package","title":"Building the Package","text":"If you have followed the above guidelines carefully, your package directory should now look like this:
\u251c\u2500\u2500 files\n\u2502 \u2514\u2500\u2500 lib\n\u2502 \u2514\u2500\u2500 page\n\u2502 \u2514\u2500\u2500 TestPage.class.php\n\u251c\u2500\u2500 package.xml\n\u251c\u2500\u2500 page.xml\n\u2514\u2500\u2500 templates\n \u2514\u2500\u2500 test.tpl\n
Both files and templates are archive-based package components, that deploy their payload using tar archives rather than adding the raw files to the package file. Please create the archive files.tar
and add the contents of the files/*
directory, but not the directory files/
itself. Repeat the same process for the templates
directory, but this time with the file name templates.tar
. Place both files in the root of your project.
Last but not least, create the package archive com.example.test.tar
and add all the files listed below.
files.tar
package.xml
page.xml
templates.tar
The archive's filename can be anything you want, all though it is the general convention to use the package name itself for easier recognition.
"},{"location":"getting-started/#installation","title":"Installation","text":"Open the Administration Control Panel and navigate to Configuration > Packages > Install Package
, click on Upload Package
and select the file com.example.test.tar
from your disk. Follow the on-screen instructions until it has been successfully installed.
Open a new browser tab and navigate to your newly created page. If WoltLab Suite is installed at https://example.com/wsc/
, then the URL should read https://example.com/wsc/index.php?test/
.
Congratulations, you have just created your first package!
"},{"location":"getting-started/#developer-tools","title":"Developer Tools","text":"The developer tools provide an interface to synchronize the data of an installed package with a bare repository on the local disk. You can re-import most PIPs at any time and have the changes applied without crafting a manual update. This process simulates a regular package update with a single PIP only, and resets the cache after the import has been completed.
"},{"location":"getting-started/#registering-a-project","title":"Registering a Project","text":"Projects require the absolute path to the package directory, that is, the directory where it can find the package.xml
. It is not required to install an package to register it as a project, but you have to install it in order to work with it. It does not install the package by itself!
There is a special button on the project list that allows for a mass-import of projects based on a search path. Each direct child directory of the provided path will be tested and projects created this way will use the identifier extracted from the package.xml
.
"},{"location":"getting-started/#synchronizing","title":"Synchronizing","text":"The install instructions in the package.xml
are ignored when offering the PIP imports, the detection works entirely based on the default filename for each PIP. On top of that, only PIPs that implement the interface wcf\\system\\devtools\\pip\\IIdempotentPackageInstallationPlugin
are valid for import, as it indicates that importing the PIP multiple times will have no side-effects and that the result is deterministic regardless of the number of times it has been imported.
Some built-in PIPs, such as sql
or script
, do not qualify for this step and remain unavailable at all times. However, you can still craft and perform an actual package update to have these PIPs executed.
"},{"location":"getting-started/#appendix","title":"Appendix","text":""},{"location":"getting-started/#template-guessing","title":"Template Guessing","text":"The class name including the namespace is used to automatically determine the path to the template and its name. The example above used the page class name wcf\\page\\TestPage
that is then split into four distinct parts:
wcf
, the internal abbreviation of WoltLab Suite Core (previously known as WoltLab Community Framework) \\page\\
(ignored) Test
, the actual name that is used for both the template and the URL Page
(page type, ignored)
The fragments 1.
and 3.
from above are used to construct the path to the template: <installDirOfWSC>/templates/test.tpl
(the first letter of Test
is being converted to lower-case).
"},{"location":"javascript/code-snippets/","title":"Code Snippets - JavaScript API","text":"This is a list of code snippets that do not fit into any of the other articles and merely describe how to achieve something very specific, rather than explaining the inner workings of a function.
"},{"location":"javascript/code-snippets/#imageviewer","title":"ImageViewer","text":"The ImageViewer is available on all frontend pages by default, you can easily add images to the viewer by wrapping the thumbnails with a link with the CSS class jsImageViewer
that points to the full version.
<a href=\"http://example.com/full.jpg\" class=\"jsImageViewer\">\n <img src=\"http://example.com/thumbnail.jpg\">\n</a>\n
"},{"location":"javascript/components_confirmation/","title":"Confirmation - JavaScript API","text":"The purpose of confirmation dialogs is to prevent misclicks and to inform the user of potential consequences of their action. A confirmation dialog should always ask a concise question that includes a reference to the object the action is performed upon.
You can exclude extra information or form elements in confirmation dialogs, but these should be kept as compact as possible.
"},{"location":"javascript/components_confirmation/#example","title":"Example","text":"const result = await confirmationFactory()\n.custom(\"Do you want a cookie?\")\n.withoutMessage();\nif (result) {\n// User has confirmed the dialog.\n}\n
Confirmation dialogs are a special type that use the role=\"alertdialog\"
attribute and will always include a cancel button. The dialog itself will be limited to a width of 500px, the title can wrap into multiple lines and there will be no \u201cX\u201d button to close the dialog.
"},{"location":"javascript/components_confirmation/#when-to-use","title":"When to Use","text":"Over the past few years the term \u201cConfirmation Fatique\u201d has emerged that describes the issue of having too many confirmation dialogs even there is no real need for them. A confirmation dialog should only be displayed when the action requires further inputs, for example, a soft delete that requires a reason, or when the action is destructive.
"},{"location":"javascript/components_confirmation/#proper-wording","title":"Proper Wording","text":"The confirmation question should hint the severity of the action, in particular whether or not it is destructive. Destructive actions are those that cannot be undone and either cause a permanent mutation or that cause data loss. All questions should be phrased in one or two ways depending on the action.
Destructive action:
Are you sure you want to delete \u201cExample Object\u201d? (German) Wollen Sie \u201eBeispiel-Objekt\u201c wirklich l\u00f6schen?
All other actions:
Do you want to move \u201cExample Object\u201d to the trash bin? (German) M\u00f6chten Sie \u201eBeispiel-Objekt\u201c in den Papierkorb verschieben?
"},{"location":"javascript/components_confirmation/#available-presets","title":"Available Presets","text":"WoltLab Suite 6.0 currently ships with three presets for common confirmation dialogs.
All three presets have an optional parameter for the title of the related object as part of the question asked to the user. It is strongly recommended to provide the title if it exists, otherwise it can be omitted and an indeterminate variant is used instead.
"},{"location":"javascript/components_confirmation/#soft-delete","title":"Soft Delete","text":"Soft deleting objects with an optional input field for a reason:
const askForReason = true;\nconst { result, reason } = await confirmationFactory().softDelete(\ntheObjectName,\naskForReason\n);\nif (result) {\nconsole.log(\n\"The user has requested a soft delete, the following reason was provided:\",\nreason\n);\n}\n
The reason
will always be a string, but with a length of zero if the result
is false
or if no reason was requested. You can simply omit the value if you do not use the reason.
const askForReason = false;\nconst { result } = await confirmationFactory().softDelete(\ntheObjectName,\naskForReason\n);\nif (result) {\nconsole.log(\"The user has requested a soft delete.\");\n}\n
"},{"location":"javascript/components_confirmation/#restore","title":"Restore","text":"Restore a previously soft deleted object:
const result = await confirmationFactory().restore(theObjectName);\nif (result) {\nconsole.log(\"The user has requested to restore the object.\");\n}\n
"},{"location":"javascript/components_confirmation/#delete","title":"Delete","text":"Permanently delete an object, will inform the user that the action cannot be undone:
const result = await confirmationFactory().delete(theObjectName);\nif (result) {\nconsole.log(\"The user has requested to delete the object.\");\n}\n
"},{"location":"javascript/components_confirmation/#custom-confirmation-prompts","title":"Custom Confirmation Prompts","text":"The custom()
permits a custom confirmation dialog with a user-defined dialog content.
const result = await confirmationFactory()\n.custom(theQuestionToAsk)\n.message(someLengthyExplanation);\nif (result) {\nconsole.log(\"The user has confirmed the dialog.\");\n}\n
"},{"location":"javascript/components_confirmation/#use-custom-html-in-the-dialog-body","title":"Use Custom HTML in the Dialog Body","text":"Some dialogs require additional input elements, for example, the prompt to remove an element has an optional text field for a reason.
const { result, dialog } = await confirmationFactory()\n.custom(theQuestionToAsk)\n.withFormElements((dialog) => {\nconst p = document.createElement(\"<p>Hello World</p>\");\ndialog.content.append(p);\n});\nif (result) {\nconsole.log(\"The user has confirmed the dialog.\");\nconsole.log(\n\"The DOM of the dialog can be accessed through `dialog.content`\",\ndialog.content\n);\n}\n
"},{"location":"javascript/components_dialog/","title":"Dialogs - JavaScript API","text":"Modal dialogs are a powerful tool to draw the viewer\u2019s attention to an important message, question or form. Dialogs naturally interrupt the workflow and prevent the navigation to other sections by making other elements on the page inert.
WoltLab Suite 6.0 ships with four different types of dialogs.
"},{"location":"javascript/components_dialog/#quickstart","title":"Quickstart","text":"There are four different types of dialogs that each fulfill their own specialized role and that provide built-in features to make the development much easier. Please see the following list to make a quick decision of what kind of dialog you need.
- Is this some kind of error message? Use an alert dialog.
- Are you asking the user to confirm an action? Use a confirmation dialog.
- Does the dialog contain form inputs that the user must fill in? Use a prompt dialog.
- Do you want to present information to the user without requiring any action? Use a dialog without controls.
"},{"location":"javascript/components_dialog/#dialogs-without-controls","title":"Dialogs Without Controls","text":"Dialogs may contain just an explanation or extra information that should be presented to the viewer without requiring any further interaction. The dialog can be closed via the \u201cX\u201d button or by clicking the modal backdrop.
const dialog = dialogFactory().fromHtml(\"<p>Hello World</p>\").withoutControls();\ndialog.show(\"Greetings from my dialog\");\n
"},{"location":"javascript/components_dialog/#when-to-use","title":"When to Use","text":"The short answer is: Don\u2019t.
Dialogs without controls are an anti-pattern because they only contain content that does not require the modal appearance of a dialog. More often than not dialogs are used for this kind of content because they are easy to use without thinking about better ways to present the content.
If possible these dialogs should be avoided and the content is presented in a more suitable way, for example, as a flyout or by showing content on an existing or new page.
"},{"location":"javascript/components_dialog/#alerts","title":"Alerts","text":"Alerts are designed to inform the user of something important that requires no further action by the user. Typical examples for alerts are error messages or warnings.
An alert will only provide a single button to acknowledge the dialog and must not contain interactive content. The dialog itself will be limited to a width of 500px, the title can wrap into multiple lines and there will be no \u201cX\u201d button to close the dialog.
const dialog = dialogFactory()\n.fromHtml(\"<p>ERROR: Something went wrong!</p>\")\n.asAlert();\ndialog.show(\"Server Error\");\n
You can customize the label of the primary button to better explain what will happen next. This can be useful for alerts that will have a side-effect when closing the dialog, such as redirect to a different page.
const dialog = dialogFactory()\n.fromHtml(\"<p>Something went wrong, we cannot find your shopping cart.</p>\")\n.asAlert({\nprimary: \"Back to the Store Page\",\n});\n\ndialog.addEventListener(\"primary\", () => {\nwindow.location.href = \"https://example.com/shop/\";\n});\n\ndialog.show(\"The shopping cart is missing\");\n
The primary
event is triggered both by clicking on the primary button and by clicks on the modal backdrop.
"},{"location":"javascript/components_dialog/#when-to-use_1","title":"When to Use","text":"Alerts are a special type of dialog that use the role=\"alert\"
attribute to signal its importance to assistive tools. Use alerts sparingly when there is no other way to communicate that something did not work as expected.
Alerts should not be used for cases where you expect an error to happen. For example, a form control that expectes an input to fall within a restricted range should show an inline error message instead of raising an alert.
"},{"location":"javascript/components_dialog/#confirmation","title":"Confirmation","text":"Confirmation dialogs are supported through a separate factory function that provides a set of presets as well as a generic API. Please see the separate documentation for confirmation dialogs to learn more.
"},{"location":"javascript/components_dialog/#prompts","title":"Prompts","text":"The most common type of dialogs are prompts that are similar to confirmation dialogs, but without the restrictions and with a regular title. These dialogs can be used universally and provide a submit and cancel button by default.
In addition they offer an \u201cextra\u201d button that is placed to the left of the default buttons are can be used to offer a single additional action. A possible use case for an \u201cextra\u201d button would be a dialog that includes an instance of the WYSIWYG editor, the extra button could be used to trigger a message preview.
"},{"location":"javascript/components_dialog/#code-example","title":"Code Example","text":"<button id=\"showMyDialog\">Show the dialog</button>\n\n<template id=\"myDialog\">\n <dl>\n <dt>\n <label for=\"myInput\">Title</label>\n </dt>\n <dd>\n <input type=\"text\" name=\"myInput\" id=\"myInput\" value=\"\" required />\n </dd>\n </dl>\n</template>\n
document.getElementById(\"showMyDialog\")!.addEventListener(\"click\", () => {\nconst dialog = dialogFactory().fromId(\"myDialog\").asPrompt();\n\ndialog.addEventListener(\"primary\", () => {\nconst myInput = document.getElementById(\"myInput\");\n\nconsole.log(\"Provided title:\", myInput.value.trim());\n});\n});\n
"},{"location":"javascript/components_dialog/#custom-buttons","title":"Custom Buttons","text":"The asPrompt()
call permits some level of customization of the form control buttons.
"},{"location":"javascript/components_dialog/#customizing-the-primary-button","title":"Customizing the Primary Button","text":"The primary
option is used to change the default label of the primary button.
dialogFactory()\n.fromId(\"myDialog\")\n.asPrompt({\nprimary: Language.get(\"wcf.dialog.button.primary\"),\n});\n
"},{"location":"javascript/components_dialog/#adding-an-extra-button","title":"Adding an Extra Button","text":"The extra button has no default label, enabling it requires you to provide a readable name.
const dialog = dialogFactory()\n.fromId(\"myDialog\")\n.asPrompt({\nextra: Language.get(\"my.extra.button.name\"),\n});\n\ndialog.addEventListener(\"extra\", () => {\n// The extra button does nothing on its own. If you want\n// to close the button after performing an action you\u2019ll\n// need to call `dialog.close()` yourself.\n});\n
"},{"location":"javascript/components_dialog/#interacting-with-dialogs","title":"Interacting with dialogs","text":"Dialogs are represented by the <woltlab-core-dialog>
element that exposes a set of properties and methods to interact with it.
"},{"location":"javascript/components_dialog/#opening-and-closing-dialogs","title":"Opening and Closing Dialogs","text":"You can open a dialog through the .show()
method that expects the title of the dialog as the only argument. Check the .open
property to determine if the dialog is currently open.
Programmatically closing a dialog is possibly through .close()
.
"},{"location":"javascript/components_dialog/#accessing-the-content","title":"Accessing the Content","text":"All contents of a dialog exists within a child element that can be accessed through the content
property.
// Add some text to the dialog.\nconst p = document.createElement(\"p\");\np.textContent = \"Hello World\";\ndialog.content.append(p);\n\n// Find a text input inside the dialog.\nconst input = dialog.content.querySelector('input[type=\"text\"]');\n
"},{"location":"javascript/components_dialog/#disabling-the-submit-button-of-a-dialog","title":"Disabling the Submit Button of a Dialog","text":"You can prevent the dialog submission until a condition is met, allowing you to dynamically enable or disable the button at will.
dialog.incomplete = false;\n\nconst checkbox = dialog.content.querySelector('input[type=\"checkbox\"]')!;\ncheckbox.addEventListener(\"change\", () => {\n// Block the dialog submission unless the checkbox is checked.\ndialog.incomplete = !checkbox.checked;\n});\n
"},{"location":"javascript/components_dialog/#managing-an-instance-of-a-dialog","title":"Managing an Instance of a Dialog","text":"The old API for dialogs implicitly kept track of the instance by binding it to the this
parameter as seen in calls like UiDialog.open(this);
. The new implementation requires to you to keep track of the dialog on your own.
class MyComponent {\n#dialog?: WoltlabCoreDialogElement;\n\nconstructor() {\nconst button = document.querySelector(\".myButton\") as HTMLButtonElement;\nbutton.addEventListener(\"click\", () => {\nthis.#showGreeting(button.dataset.name);\n});\n}\n\n#showGreeting(name: string | undefined): void {\nconst dialog = this.#getDialog();\n\nconst p = dialog.content.querySelector(\"p\")!;\nif (name === undefined) {\np.textContent = \"Hello World\";\n} else {\np.textContent = `Hello ${name}`;\n}\n\ndialog.show(\"Greetings!\");\n}\n\n#getDialog(): WoltlabCoreDialogElement {\nif (this.#dialog === undefined) {\nthis.#dialog = dialogFactory()\n.fromHtml(\"<p>Hello from MyComponent</p>\")\n.withoutControls();\n}\n\nreturn this.#dialog;\n}\n}\n
"},{"location":"javascript/components_dialog/#event-access","title":"Event Access","text":"You can bind event listeners to specialized events to get notified of events and to modify its behavior.
"},{"location":"javascript/components_dialog/#afterclose","title":"afterClose
","text":"This event cannot be canceled.
Fires when the dialog has closed.
dialog.addEventListener(\"afterClose\", () => {\n// Dialog was closed.\n});\n
"},{"location":"javascript/components_dialog/#close","title":"close
","text":"Fires when the dialog is about to close.
dialog.addEventListener(\"close\", (event) => {\nif (someCondition) {\nevent.preventDefault();\n}\n});\n
"},{"location":"javascript/components_dialog/#cancel","title":"cancel
","text":"Fires only when there is a \u201cCancel\u201d button and the user has either pressed that button or clicked on the modal backdrop. The dialog will close if the event is not canceled.
dialog.addEventListener(\"cancel\", (event) => {\nif (someCondition) {\nevent.preventDefault();\n}\n});\n
"},{"location":"javascript/components_dialog/#extra","title":"extra
","text":"This event cannot be canceled.
Fires when an extra button is present and the button was clicked by the user. This event does nothing on its own and is supported for dialogs of type \u201cPrompt\u201d only.
dialog.addEventListener(\"extra\", () => {\n// The extra button was clicked.\n});\n
"},{"location":"javascript/components_dialog/#primary","title":"primary
","text":"This event cannot be canceled.
Fires only when there is a primary action button and the user has either pressed that button or submitted the form through keyboard controls.
dialog.addEventListener(\"primary\", () => {\n// The primary action button was clicked or the\n// form was submitted through keyboard controls.\n//\n// The `validate` event has completed successfully.\n});\n
"},{"location":"javascript/components_dialog/#validate","title":"validate
","text":"Fires only when there is a form and the user has pressed the primary action button or submitted the form through keyboard controls. Canceling this event is interpreted as a form validation failure.
const input = document.createElement(\"input\");\ndialog.content.append(input);\n\ndialog.addEventListener(\"validate\", (event) => {\nif (input.value.trim() === \"\") {\nevent.preventDefault();\n\n// Display an inline error message.\n}\n});\n
"},{"location":"javascript/components_google_maps/","title":"Google Maps - JavaScript API","text":"The Google Maps component is used to show a map using the Google Maps API.
"},{"location":"javascript/components_google_maps/#example","title":"Example","text":"The component can be included directly as follows:
<woltlab-core-google-maps\n id=\"id\"\n class=\"googleMap\"\n api-key=\"your_api_key\"\n></woltlab-core-google-maps>\n
Alternatively, the component can be included via a template that uses the API key from the configuration and also handles the user content:
{include file='googleMapsElement' googleMapsElementID=\"id\"}\n
"},{"location":"javascript/components_google_maps/#parameters","title":"Parameters","text":""},{"location":"javascript/components_google_maps/#id","title":"id
","text":"ID of the map instance.
"},{"location":"javascript/components_google_maps/#api-key","title":"api-key
","text":"Google Maps API key.
"},{"location":"javascript/components_google_maps/#zoom","title":"zoom
","text":"Defaults to 13
.
Default zoom factor of the map.
"},{"location":"javascript/components_google_maps/#lat","title":"lat
","text":"Defaults to 0
.
Latitude of the default map position.
"},{"location":"javascript/components_google_maps/#lng","title":"lng
","text":"Defaults to 0
.
Longitude of the default map position.
"},{"location":"javascript/components_google_maps/#access-user-location","title":"access-user-location
","text":"If set, the map will try to center based on the user's current position.
"},{"location":"javascript/components_google_maps/#map-related-functions","title":"Map-related Functions","text":""},{"location":"javascript/components_google_maps/#addmarker","title":"addMarker
","text":"Adds a marker to the map.
"},{"location":"javascript/components_google_maps/#example_1","title":"Example","text":"<script data-relocate=\"true\">\nrequire(['WoltLabSuite/Core/Component/GoogleMaps/Marker'], ({ addMarker }) => {\nvoid addMarker(document.getElementById('map_id'), 52.4505, 13.7546, 'Title', true);\n});\n</script>\n
"},{"location":"javascript/components_google_maps/#parameters_1","title":"Parameters","text":""},{"location":"javascript/components_google_maps/#element","title":"element
","text":"<woltlab-core-google-maps>
element.
"},{"location":"javascript/components_google_maps/#latitude","title":"latitude
","text":"Marker position (latitude)
"},{"location":"javascript/components_google_maps/#longitude","title":"longitude
","text":"Marker position (longitude)
"},{"location":"javascript/components_google_maps/#title","title":"title
","text":"Title of the marker.
"},{"location":"javascript/components_google_maps/#focus","title":"focus
","text":"Defaults to false
.
True, to focus the map on the position of the marker.
"},{"location":"javascript/components_google_maps/#adddraggablemarker","title":"addDraggableMarker
","text":"Adds a draggable marker to the map.
"},{"location":"javascript/components_google_maps/#example_2","title":"Example","text":"<script data-relocate=\"true\">\nrequire(['WoltLabSuite/Core/Component/GoogleMaps/Marker'], ({ addDraggableMarker }) => {\nvoid addDraggableMarker(document.getElementById('map_id'), 52.4505, 13.7546);\n});\n</script>\n
"},{"location":"javascript/components_google_maps/#parameters_2","title":"Parameters","text":""},{"location":"javascript/components_google_maps/#element_1","title":"element
","text":"<woltlab-core-google-maps>
element.
"},{"location":"javascript/components_google_maps/#latitude_1","title":"latitude
","text":"Marker position (latitude)
"},{"location":"javascript/components_google_maps/#longitude_1","title":"longitude
","text":"Marker position (longitude)
"},{"location":"javascript/components_google_maps/#geocoding","title":"Geocoding
","text":"Enables the geocoding feature for a map.
"},{"location":"javascript/components_google_maps/#example_3","title":"Example","text":"<input\n type=\"text\"\n data-google-maps-geocoding=\"map_id\"\n data-google-maps-marker\n data-google-maps-geocoding-store=\"prefix\"\n>\n
"},{"location":"javascript/components_google_maps/#parameters_3","title":"Parameters","text":""},{"location":"javascript/components_google_maps/#data-google-maps-geocoding","title":"data-google-maps-geocoding
","text":"ID of the <woltlab-core-google-maps>
element.
"},{"location":"javascript/components_google_maps/#data-google-maps-marker","title":"data-google-maps-marker
","text":"If set, a movable marker is created that is coupled with the input field.
"},{"location":"javascript/components_google_maps/#data-google-maps-geocoding-store","title":"data-google-maps-geocoding-store
","text":"If set, the coordinates (latitude and longitude) are stored comma-separated in a hidden input field. Optionally, a value can be passed that is used as a prefix for the name of the input field.
"},{"location":"javascript/components_google_maps/#markerloader","title":"MarkerLoader
","text":"Handles a large map with many markers where markers are loaded via AJAX.
"},{"location":"javascript/components_google_maps/#example_4","title":"Example","text":"<script data-relocate=\"true\">\nrequire(['WoltLabSuite/Core/Component/GoogleMaps/MarkerLoader'], ({ setup }) => {\nsetup(document.getElementById('map_id'), 'action_classname', {});\n});\n</script>\n
"},{"location":"javascript/components_google_maps/#parameters_4","title":"Parameters","text":""},{"location":"javascript/components_google_maps/#element_2","title":"element
","text":"<woltlab-core-google-maps>
element.
"},{"location":"javascript/components_google_maps/#actionclassname","title":"actionClassName
","text":"Name of the PHP class that is called to retrieve the markers via AJAX.
"},{"location":"javascript/components_google_maps/#additionalparameters","title":"additionalParameters
","text":"Additional parameters that are transmitted when querying the markers via AJAX.
"},{"location":"javascript/components_pagination/","title":"Pagination - JavaScript API","text":"The pagination component is used to expose multiple pages to the end user. This component supports both static URLs and dynamic navigation using DOM events.
"},{"location":"javascript/components_pagination/#example","title":"Example","text":"<woltlab-core-pagination page=\"1\" count=\"10\" url=\"https://www.woltlab.com\"></woltlab-core-pagination>\n
"},{"location":"javascript/components_pagination/#parameters","title":"Parameters","text":""},{"location":"javascript/components_pagination/#page","title":"page
","text":"Defaults to 1
.
The number of the currently displayed page.
"},{"location":"javascript/components_pagination/#count","title":"count
","text":"Defaults to 0
.
Number of available pages. Must be greater than 1
for the pagination to be displayed.
"},{"location":"javascript/components_pagination/#url","title":"url
","text":"Defaults to an empty string.
If defined, static pagination links are created based on the URL with the pageNo
parameter appended to it. Otherwise only the switchPage
event will be fired if a user clicks on a pagination link.
"},{"location":"javascript/components_pagination/#events","title":"Events","text":""},{"location":"javascript/components_pagination/#switchpage","title":"switchPage
","text":"The switchPage
event will be fired when the user clicks on a pagination link. The event detail will contain the number of the selected page. The event can be canceled to prevent navigation.
"},{"location":"javascript/components_pagination/#jumptopage","title":"jumpToPage
","text":"The switchPage
event will be fired when the user clicks on one of the ellipsis buttons within the pagination.
"},{"location":"javascript/general-usage/","title":"General JavaScript Usage","text":"WoltLab Suite 5.4 introduced support for TypeScript, migrating all existing modules to TypeScript. The JavaScript section of the documentation is not yet updated to account for the changes, possibly explaining concepts that cannot be applied as-is when writing TypeScript. You can learn about basic TypeScript use in WoltLab Suite, such as consuming WoltLab Suite\u2019s types in own packages, within in the TypeScript section.
"},{"location":"javascript/general-usage/#the-history-of-the-legacy-api","title":"The History of the Legacy API","text":"The WoltLab Suite 3.0 introduced a new API based on AMD-Modules with ES5-JavaScript that was designed with high performance and visible dependencies in mind. This was a fundamental change in comparison to the legacy API that was build many years before while jQuery was still a thing and we had to deal with ancient browsers such as Internet Explorer 9 that felt short in both CSS and JavaScript capabilities.
Fast forward a few years, the old API is still around and most important, it is actively being used by some components that have not been rewritten yet. This has been done to preserve the backwards-compatibility and to avoid the significant amount of work that it requires to rewrite a component. The components invoked on page initialization have all been rewritten to use the modern API, but some deferred objects that are invoked later during the page runtime may still use the old API.
However, the legacy API is deprecated and you should not rely on it for new components at all. It slowly but steadily gets replaced up until a point where its last bits are finally removed from the code base.
"},{"location":"javascript/general-usage/#embedding-javascript-inside-templates","title":"Embedding JavaScript inside Templates","text":"The <script>
-tags are extracted and moved during template processing, eventually placing them at the very end of the body element while preserving their order of appearance.
This behavior is controlled through the data-relocate=\"true\"
attribute on the <script>
which is mandatory for almost all scripts, mostly because their dependencies (such as jQuery) are moved to the bottom anyway.
<script data-relocate=\"true\">\n$(function() {\n// Code that uses jQuery (Legacy API)\n});\n</script>\n\n<!-- or -->\n\n<script data-relocate=\"true\">\nrequire([\"Some\", \"Dependencies\"], function(Some, Dependencies) {\n// Modern API\n});\n</script>\n
"},{"location":"javascript/general-usage/#including-external-javascript-files","title":"Including External JavaScript Files","text":"The AMD-Modules used in the new API are automatically recognized and lazy-loaded on demand, so unless you have a rather large and pre-compiled code-base, there is nothing else to worry about.
"},{"location":"javascript/general-usage/#debug-variants-and-cache-buster","title":"Debug-Variants and Cache-Buster","text":"Your JavaScript files may change over time and you would want the users' browsers to always load and use the latest version of your files. This can be achieved by appending the special LAST_UPDATE_TIME
constant to your file path. It contains the unix timestamp of the last time any package was installed, updated or removed and thus avoid outdated caches by relying on a unique value, without invalidating the cache more often that it needs to be.
<script data-relocate=\"true\" src=\"{$__wcf->getPath('app')}js/App.js?t={@LAST_UPDATE_TIME}\"></script>\n
For small scripts you can simply serve the full, non-minified version to the user at all times, the differences in size and execution speed are insignificant and are very unlikely to offer any benefits. They might even yield a worse performance, because you'll have to include them statically in the template, even if the code is never called.
However, if you're including a minified build in your app or plugin, you should include a switch to load the uncompressed version in the debug mode, while serving the minified and optimized file to the average visitor. You should use the ENABLE_DEBUG_MODE
constant to decide which version should be loaded.
<script data-relocate=\"true\" src=\"{$__wcf->getPath('app')}js/App{if !ENABLE_DEBUG_MODE}.min{/if}.js?t={@LAST_UPDATE_TIME}\"></script>\n
"},{"location":"javascript/general-usage/#the-accelerated-guest-view-tiny-builds","title":"The Accelerated Guest View (\"Tiny Builds\")","text":"You can learn more on the Accelerated Guest View in the migration docs.
The \u201cAccelerated Guest View\u201d aims to decrease page size and to improve responsiveness by enabling a read-only mode for visitors. If you are providing a separate compiled build for this mode, you'll need to include yet another switch to serve the right version to the visitor.
<script data-relocate=\"true\" src=\"{$__wcf->getPath('app')}js/App{if !ENABLE_DEBUG_MODE}{if VISITOR_USE_TINY_BUILD}.tiny{/if}.min{/if}.js?t={@LAST_UPDATE_TIME}\"></script>\n
"},{"location":"javascript/general-usage/#the-js-template-plugin","title":"The {js}
Template Plugin","text":"The {js}
template plugin exists solely to provide a much easier and less error-prone method to include external JavaScript files.
{js application='app' file='App' hasTiny=true}\n
The hasTiny
attribute is optional, you can set it to false
or just omit it entirely if you do not provide a tiny build for your file.
"},{"location":"javascript/legacy-api/","title":"Legacy JavaScript API","text":""},{"location":"javascript/legacy-api/#introduction","title":"Introduction","text":"The legacy JavaScript API is the original code that was part of the 2.x series of WoltLab Suite, formerly known as WoltLab Community Framework. It has been superseded for the most part by the ES5/AMD-modules API introduced with WoltLab Suite 3.0.
Some parts still exist to this day for backwards-compatibility and because some less important components have not been rewritten yet. The old API is still supported, but marked as deprecated and will continue to be replaced parts by part in future releases, up until their entire removal, including jQuery support.
This guide does not provide any explanation on the usage of those legacy components, but instead serves as a cheat sheet to convert code to use the new API.
"},{"location":"javascript/legacy-api/#classes","title":"Classes","text":""},{"location":"javascript/legacy-api/#singletons","title":"Singletons","text":"Singleton instances are designed to provide a unique \"instance\" of an object regardless of when its first instance was created. Due to the lack of a class
construct in ES5, they are represented by mere objects that act as an instance.
// App.js\nwindow.App = {};\nApp.Foo = {\nbar: function() {}\n};\n\n// --- NEW API ---\n\n// App/Foo.js\ndefine([], function() {\n\"use strict\";\n\nreturn {\nbar: function() {}\n};\n});\n
"},{"location":"javascript/legacy-api/#regular-classes","title":"Regular Classes","text":"// App.js\nwindow.App = {};\nApp.Foo = Class.extend({\nbar: function() {}\n});\n\n// --- NEW API ---\n\n// App/Foo.js\ndefine([], function() {\n\"use strict\";\n\nfunction Foo() {};\nFoo.prototype = {\nbar: function() {}\n};\n\nreturn Foo;\n});\n
"},{"location":"javascript/legacy-api/#inheritance","title":"Inheritance","text":"// App.js\nwindow.App = {};\nApp.Foo = Class.extend({\nbar: function() {}\n});\nApp.Baz = App.Foo.extend({\nmakeSnafucated: function() {}\n});\n\n// --- NEW API ---\n\n// App/Foo.js\ndefine([], function() {\n\"use strict\";\n\nfunction Foo() {};\nFoo.prototype = {\nbar: function() {}\n};\n\nreturn Foo;\n});\n\n// App/Baz.js\ndefine([\"Core\", \"./Foo\"], function(Core, Foo) {\n\"use strict\";\n\nfunction Baz() {};\nCore.inherit(Baz, Foo, {\nmakeSnafucated: function() {}\n});\n\nreturn Baz;\n});\n
"},{"location":"javascript/legacy-api/#ajax-requests","title":"Ajax Requests","text":"// App.js\nApp.Foo = Class.extend({\n_proxy: null,\n\ninit: function() {\nthis._proxy = new WCF.Action.Proxy({\nsuccess: $.proxy(this._success, this)\n});\n},\n\nbar: function() {\nthis._proxy.setOption(\"data\", {\nactionName: \"baz\",\nclassName: \"app\\\\foo\\\\FooAction\",\nobjectIDs: [1, 2, 3],\nparameters: {\nfoo: \"bar\",\nbaz: true\n}\n});\nthis._proxy.sendRequest();\n},\n\n_success: function(data) {\n// ajax request result\n}\n});\n\n// --- NEW API ---\n\n// App/Foo.js\ndefine([\"Ajax\"], function(Ajax) {\n\"use strict\";\n\nfunction Foo() {}\nFoo.prototype = {\nbar: function() {\nAjax.api(this, {\nobjectIDs: [1, 2, 3],\nparameters: {\nfoo: \"bar\",\nbaz: true\n}\n});\n},\n\n// magic method!\n_ajaxSuccess: function(data) {\n// ajax request result\n},\n\n// magic method!\n_ajaxSetup: function() {\nreturn {\nactionName: \"baz\",\nclassName: \"app\\\\foo\\\\FooAction\"\n}\n}\n}\n\nreturn Foo;\n});\n
"},{"location":"javascript/legacy-api/#phrases","title":"Phrases","text":"<script data-relocate=\"true\">\n$(function() {\nWCF.Language.addObject({\n'app.foo.bar': '{lang}app.foo.bar{/lang}'\n});\n\nconsole.log(WCF.Language.get(\"app.foo.bar\"));\n});\n</script>\n\n<!-- NEW API -->\n\n<script data-relocate=\"true\">\nrequire([\"Language\"], function(Language) {\nLanguage.addObject({\n'app.foo.bar': '{jslang}app.foo.bar{/jslang}'\n});\n\nconsole.log(Language.get(\"app.foo.bar\"));\n});\n</script>\n
"},{"location":"javascript/legacy-api/#event-listener","title":"Event-Listener","text":"<script data-relocate=\"true\">\n$(function() {\nWCF.System.Event.addListener(\"app.foo.bar\", \"makeSnafucated\", function(data) {\nconsole.log(\"Event was invoked.\");\n});\n\nWCF.System.Event.fireEvent(\"app.foo.bar\", \"makeSnafucated\", { some: \"data\" });\n});\n</script>\n\n<!-- NEW API -->\n\n<script data-relocate=\"true\">\nrequire([\"EventHandler\"], function(EventHandler) {\nEventHandler.add(\"app.foo.bar\", \"makeSnafucated\", function(data) {\nconsole.log(\"Event was invoked\");\n});\n\nEventHandler.fire(\"app.foo.bar\", \"makeSnafucated\", { some: \"data\" });\n});\n</script>\n
"},{"location":"javascript/new-api_ajax/","title":"Ajax Requests - JavaScript API","text":""},{"location":"javascript/new-api_ajax/#promise-based-api-for-databaseobjectaction","title":"Promise
-based API for DatabaseObjectAction
","text":"WoltLab Suite 5.5 introduces a new API for Ajax requests that uses Promise
s to control the code flow. It does not rely on references to existing objects and does not use arbitrary callbacks to handle the setup and handling of the request.
"},{"location":"javascript/new-api_ajax/#usage","title":"Usage","text":"MyModule.tsimport * as Ajax from \"./Ajax\";\n\ntype ResponseGetLatestFoo = {\ntemplate: string;\n};\n\nexport class MyModule {\nprivate readonly bar: string;\nprivate readonly objectId: number;\n\nconstructor(objectId: number, bar: string, buttonId: string) {\nthis.bar = bar;\nthis.objectId = objectId;\n\nconst button = document.getElementById(buttonId);\nbutton?.addEventListener(\"click\", (event) => void this.click(event));\n}\n\nasync click(event: MouseEvent): Promise<void> {\nevent.preventDefault();\n\nconst button = event.currentTarget as HTMLElement;\nif (button.classList.contains(\"disabled\")) {\nreturn;\n}\nbutton.classList.add(\"disabled\");\n\ntry {\nconst response = (await Ajax.dboAction(\"getLatestFoo\", \"wcf\\\\data\\\\foo\\\\FooAction\")\n.objectIds([this.objectId])\n.payload({ bar: this.bar })\n.dispatch()) as ResponseGetLatestFoo;\n\ndocument.getElementById(\"latestFoo\")!.innerHTML = response.template;\n} finally {\nbutton.classList.remove(\"disabled\");\n}\n}\n}\n\nexport default MyModule;\n
The actual code to dispatch and evaluate a request is only four lines long and offers full IDE auto completion support. This example uses a finally
block to reset the button class once the request has finished, regardless of the result.
If you do not handle the errors (or chose not to handle some errors), the global rejection handler will take care of this and show an dialog that informs about the failed request. This mimics the behavior of the _ajaxFailure()
callback in the legacy API.
"},{"location":"javascript/new-api_ajax/#aborting-in-flight-requests","title":"Aborting in-flight requests","text":"Sometimes new requests are dispatched against the same API before the response from the previous has arrived. This applies to either long running requests or requests that are dispatched in rapid succession, for example, looking up values when the user is actively typing into a search field.
RapidRequests.tsimport * as Ajax from \"./Ajax\";\n\nexport class RapidRequests {\nprivate lastRequest: AbortController | undefined = undefined;\n\nconstructor(inputId: string) {\nconst input = document.getElementById(inputId) as HTMLInputElement;\ninput.addEventListener(\"input\", (event) => void this.input(event));\n}\n\nasync input(event: Event): Promise<void> {\nevent.preventDefault();\n\nconst input = event.currentTarget as HTMLInputElement;\nconst value = input.value.trim();\n\nif (this.lastRequest) {\nthis.lastRequest.abort();\n}\n\nif (value) {\nconst request = Ajax.dboAction(\"getSuggestions\", \"wcf\\\\data\\\\bar\\\\BarAction\").payload({ value });\nthis.lastRequest = request.getAbortController();\n\nconst response = await request.dispatch();\n// Handle the response\n}\n}\n}\n\nexport default RapidRequests;\n
"},{"location":"javascript/new-api_ajax/#ajax-inside-modules-legacy-api","title":"Ajax inside Modules (Legacy API)","text":"The Ajax component was designed to be used from inside modules where an object reference is used to delegate request callbacks. This is acomplished through a set of magic methods that are automatically called when the request is created or its state has changed.
"},{"location":"javascript/new-api_ajax/#_ajaxsetup","title":"_ajaxSetup()
","text":"The lazy initialization is performed upon the first invocation from the callee, using the magic _ajaxSetup()
method to retrieve the basic configuration for this and any future requests.
The data returned by _ajaxSetup()
is cached and the data will be used to pre-populate the request data before sending it. The callee can overwrite any of these properties. It is intended to reduce the overhead when issuing request when these requests share the same properties, such as accessing the same endpoint.
// App/Foo.js\ndefine([\"Ajax\"], function(Ajax) {\n\"use strict\";\n\nfunction Foo() {};\nFoo.prototype = {\none: function() {\n// this will issue an ajax request with the parameter `value` set to `1`\nAjax.api(this);\n},\n\ntwo: function() {\n// this request is almost identical to the one issued with `.one()`, but\n// the value is now set to `2` for this invocation only.\nAjax.api(this, {\nparameters: {\nvalue: 2\n}\n});\n},\n\n_ajaxSetup: function() {\nreturn {\ndata: {\nactionName: \"makeSnafucated\",\nclassName: \"app\\\\data\\\\foo\\\\FooAction\",\nparameters: {\nvalue: 1\n}\n}\n}\n}\n};\n\nreturn Foo;\n});\n
"},{"location":"javascript/new-api_ajax/#request-settings","title":"Request Settings","text":"The object returned by the aforementioned _ajaxSetup()
callback can contain these values:
"},{"location":"javascript/new-api_ajax/#data","title":"data
","text":"Defaults to {}
.
A plain JavaScript object that contains the request data that represents the form data of the request. The parameters
key is recognized by the PHP Ajax API and becomes accessible through $this->parameters
.
"},{"location":"javascript/new-api_ajax/#contenttype","title":"contentType
","text":"Defaults to application/x-www-form-urlencoded; charset=UTF-8
.
The request content type, sets the Content-Type
HTTP header if it is not empty.
"},{"location":"javascript/new-api_ajax/#responsetype","title":"responseType
","text":"Defaults to application/json
.
The server must respond with the Content-Type
HTTP header set to this value, otherwise the request will be treated as failed. Requests for application/json
will have the return body attempted to be evaluated as JSON.
Other content types will only be validated based on the HTTP header, but no additional transformation is performed. For example, setting the responseType
to application/xml
will check the HTTP header, but will not transform the data
parameter, you'll still receive a string in _ajaxSuccess
!
"},{"location":"javascript/new-api_ajax/#type","title":"type
","text":"Defaults to POST
.
The HTTP Verb used for this request.
"},{"location":"javascript/new-api_ajax/#url","title":"url
","text":"Defaults to an empty string.
Manual override for the request endpoint, it will be automatically set to the Core API endpoint if left empty. If the Core API endpoint is used, the options includeRequestedWith
and withCredentials
will be force-set to true.
"},{"location":"javascript/new-api_ajax/#withcredentials","title":"withCredentials
","text":"Enabling this parameter for any domain other than the current will trigger a CORS preflight request.
Defaults to false
.
Include cookies with this requested, is always true when url
is (implicitly) set to the Core API endpoint.
"},{"location":"javascript/new-api_ajax/#autoabort","title":"autoAbort
","text":"Defaults to false
.
When set to true
, any pending responses to earlier requests will be silently discarded when issuing a new request. This only makes sense if the new request is meant to completely replace the result of the previous one, regardless of its reponse body.
Typical use-cases include input field with suggestions, where possible values are requested from the server, but the input changed faster than the server was able to reply. In this particular case the client is not interested in the result for an earlier value, auto-aborting these requests avoids implementing this logic in the requesting code.
"},{"location":"javascript/new-api_ajax/#ignoreerror","title":"ignoreError
","text":"Defaults to false
.
Any failing request will invoke the failure
-callback to check if an error message should be displayed. Enabling this option will suppress the general error overlay that reports a failed request.
You can achieve the same result by returning false
in the failure
-callback.
"},{"location":"javascript/new-api_ajax/#silent","title":"silent
","text":"Defaults to false
.
Enabling this option will suppress the loading indicator overlay for this request, other non-\"silent\" requests will still trigger the loading indicator.
"},{"location":"javascript/new-api_ajax/#includerequestedwith","title":"includeRequestedWith
","text":"Enabling this parameter for any domain other than the current will trigger a CORS preflight request.
Defaults to true
.
Sets the custom HTTP header X-Requested-With: XMLHttpRequest
for the request, it is automatically set to true
when url
is pointing at the WSC API endpoint.
"},{"location":"javascript/new-api_ajax/#failure","title":"failure
","text":"Defaults to null
.
Optional callback function that will be invoked for requests that have failed for one of these reasons: 1. The request timed out. 2. The HTTP status is not 2xx
or 304
. 3. A responseType
was set, but the response HTTP header Content-Type
did not match the expected value. 4. The responseType
was set to application/json
, but the response body was not valid JSON.
The callback function receives the parameter xhr
(the XMLHttpRequest
object) and options
(deep clone of the request parameters). If the callback returns false
, the general error overlay for failed requests will be suppressed.
There will be no error overlay if ignoreError
is set to true
or if the request failed while attempting to evaluate the response body as JSON.
"},{"location":"javascript/new-api_ajax/#finalize","title":"finalize
","text":"Defaults to null
.
Optional callback function that will be invoked once the request has completed, regardless if it succeeded or failed. The only parameter it receives is options
(the request parameters object), but it does not receive the request's XMLHttpRequest
.
"},{"location":"javascript/new-api_ajax/#success","title":"success
","text":"Defaults to null
.
This semi-optional callback function will always be set to _ajaxSuccess()
when invoking Ajax.api()
. It receives four parameters: 1. data
- The request's response body as a string, or a JavaScript object if contentType
was set to application/json
. 2. responseText
- The unmodified response body, it equals the value for data
for non-JSON requests. 3. xhr
- The underlying XMLHttpRequest
object. 4. requestData
- The request parameters that were supplied when the request was issued.
"},{"location":"javascript/new-api_ajax/#_ajaxsuccess","title":"_ajaxSuccess()
","text":"This callback method is automatically called for successful AJAX requests, it receives four parameters, with the first one containing either the response body as a string, or a JavaScript object for JSON requests.
"},{"location":"javascript/new-api_ajax/#_ajaxfailure","title":"_ajaxFailure()
","text":"Optional callback function that is invoked for failed requests, it will be automatically called if the callee implements it, otherwise the global error handler will be executed.
"},{"location":"javascript/new-api_ajax/#single-requests-without-a-module-legacy-api","title":"Single Requests Without a Module (Legacy API)","text":"The Ajax.api()
method expects an object that is used to extract the request configuration as well as providing the callback functions when the request state changes.
You can issue a simple Ajax request without object binding through Ajax.apiOnce()
that will destroy the instance after the request was finalized. This method is significantly more expensive for repeated requests and does not offer deriving modules from altering the behavior. It is strongly recommended to always use Ajax.api()
for requests to the WSC API endpoint.
<script data-relocate=\"true\">\nrequire([\"Ajax\"], function(Ajax) {\nAjax.apiOnce({\ndata: {\nactionName: \"makeSnafucated\",\nclassName: \"app\\\\data\\\\foo\\\\FooAction\",\nparameters: {\nvalue: 3\n}\n},\nsuccess: function(data) {\nelBySel(\".some-element\").textContent = data.bar;\n}\n})\n});\n</script>\n
"},{"location":"javascript/new-api_browser/","title":"Browser and Screen Sizes - JavaScript API","text":""},{"location":"javascript/new-api_browser/#uiscreen","title":"Ui/Screen
","text":"CSS offers powerful media queries that alter the layout depending on the screen sizes, including but not limited to changes between landscape and portrait mode on mobile devices.
The Ui/Screen
module exposes a consistent interface to execute JavaScript code based on the same media queries that are available in the CSS code already. It features support for unmatching and executing code when a rule matches for the first time during the page lifecycle.
"},{"location":"javascript/new-api_browser/#supported-aliases","title":"Supported Aliases","text":"You can pass in custom media queries, but it is strongly recommended to use the built-in media queries that match the same dimensions as your CSS.
Alias Media Query screen-xs
(max-width: 544px)
screen-sm
(min-width: 545px) and (max-width: 768px)
screen-sm-down
(max-width: 768px)
screen-sm-up
(min-width: 545px)
screen-sm-md
(min-width: 545px) and (max-width: 1024px)
screen-md
(min-width: 769px) and (max-width: 1024px)
screen-md-down
(max-width: 1024px)
screen-md-up
(min-width: 769px)
screen-lg
(min-width: 1025px)
"},{"location":"javascript/new-api_browser/#onquery-string-callbacks-object-string","title":"on(query: string, callbacks: Object): string
","text":"Registers a set of callback functions for the provided media query, the possible keys are match
, unmatch
and setup
. The method returns a randomly generated UUIDv4 that is used to identify these callbacks and allows them to be removed via .remove()
.
"},{"location":"javascript/new-api_browser/#removequery-string-uuid-string","title":"remove(query: string, uuid: string)
","text":"Removes all callbacks for a media query that match the UUIDv4 that was previously obtained from the call to .on()
.
"},{"location":"javascript/new-api_browser/#isquery-string-boolean","title":"is(query: string): boolean
","text":"Tests if the provided media query currently matches and returns true on match.
"},{"location":"javascript/new-api_browser/#scrolldisable","title":"scrollDisable()
","text":"Temporarily prevents the page from being scrolled, until .scrollEnable()
is called.
"},{"location":"javascript/new-api_browser/#scrollenable","title":"scrollEnable()
","text":"Enables page scrolling again, unless another pending action has also prevented the page scrolling.
"},{"location":"javascript/new-api_browser/#environment","title":"Environment
","text":"The Environment
module uses a mixture of feature detection and user agent sniffing to determine the browser and platform. In general, its results have proven to be very accurate, but it should be taken with a grain of salt regardless. Especially the browser checks are designed to be your last resort, please use feature detection instead whenever it is possible!
Sometimes it may be necessary to alter the behavior of your code depending on the browser platform (e. g. mobile devices) or based on a specific browser in order to work-around some quirks.
"},{"location":"javascript/new-api_browser/#browser-string","title":"browser(): string
","text":"Attempts to detect browsers based on their technology and supported CSS vendor prefixes, and although somewhat reliable for major browsers, it is highly recommended to use feature detection instead.
Possible values: - chrome
(includes Opera 15+ and Vivaldi) - firefox
- safari
- microsoft
(Internet Explorer and Edge) - other
(default)
"},{"location":"javascript/new-api_browser/#platform-string","title":"platform(): string
","text":"Attempts to detect the browser platform using user agent sniffing.
Possible values: - ios
- android
- windows
(IE Mobile) - mobile
(generic mobile device) - desktop
(default)
"},{"location":"javascript/new-api_core/","title":"Core Modules and Functions - JavaScript API","text":"A brief overview of common methods that may be useful when writing any module.
"},{"location":"javascript/new-api_core/#core","title":"Core
","text":""},{"location":"javascript/new-api_core/#cloneobject-object-object","title":"clone(object: Object): Object
","text":"Creates a deep-clone of the provided object by value, removing any references on the original element, including arrays. However, this does not clone references to non-plain objects, these instances will be copied by reference.
require([\"Core\"], function(Core) {\nvar obj1 = { a: 1 };\nvar obj2 = Core.clone(obj1);\n\nconsole.log(obj1 === obj2); // output: false\nconsole.log(obj2.hasOwnProperty(\"a\") && obj2.a === 1); // output: true\n});\n
"},{"location":"javascript/new-api_core/#extendbase-object-merge-object-object","title":"extend(base: Object, ...merge: Object[]): Object
","text":"Accepts an infinite amount of plain objects as parameters, values will be copied from the 2nd...nth object into the first object. The first parameter will be cloned and the resulting object is returned.
require([\"Core\"], function(Core) {\nvar obj1 = { a: 2 };\nvar obj2 = { a: 1, b: 2 };\nvar obj = Core.extend({\nb: 1\n}, obj1, obj2);\n\nconsole.log(obj.b === 2); // output: true\nconsole.log(obj.hasOwnProperty(\"a\") && obj.a === 2); // output: false\n});\n
"},{"location":"javascript/new-api_core/#inheritbase-object-target-object-merge-object","title":"inherit(base: Object, target: Object, merge?: Object)
","text":"Derives the second object's prototype from the first object, afterwards the derived class will pass the instanceof
check against the original class.
// App.js\nwindow.App = {};\nApp.Foo = Class.extend({\nbar: function() {}\n});\nApp.Baz = App.Foo.extend({\nmakeSnafucated: function() {}\n});\n\n// --- NEW API ---\n\n// App/Foo.js\ndefine([], function() {\n\"use strict\";\n\nfunction Foo() {};\nFoo.prototype = {\nbar: function() {}\n};\n\nreturn Foo;\n});\n\n// App/Baz.js\ndefine([\"Core\", \"./Foo\"], function(Core, Foo) {\n\"use strict\";\n\nfunction Baz() {};\nCore.inherit(Baz, Foo, {\nmakeSnafucated: function() {}\n});\n\nreturn Baz;\n});\n
"},{"location":"javascript/new-api_core/#isplainobjectobject-object-boolean","title":"isPlainObject(object: Object): boolean
","text":"Verifies if an object is a plain JavaScript object and not an object instance.
require([\"Core\"], function(Core) {\nfunction Foo() {}\nFoo.prototype = {\nhello: \"world\";\n};\n\nvar obj1 = { hello: \"world\" };\nvar obj2 = new Foo();\n\nconsole.log(Core.isPlainObject(obj1)); // output: true\nconsole.log(obj1.hello === obj2.hello); // output: true\nconsole.log(Core.isPlainObject(obj2)); // output: false\n});\n
"},{"location":"javascript/new-api_core/#triggereventelement-element-eventname-string","title":"triggerEvent(element: Element, eventName: string)
","text":"Creates and dispatches a synthetic JavaScript event on an element.
require([\"Core\"], function(Core) {\nvar element = elBySel(\".some-element\");\nCore.triggerEvent(element, \"click\");\n});\n
"},{"location":"javascript/new-api_core/#language","title":"Language
","text":""},{"location":"javascript/new-api_core/#addkey-string-value-string","title":"add(key: string, value: string)
","text":"Registers a new phrase.
<script data-relocate=\"true\">\nrequire([\"Language\"], function(Language) {\nLanguage.add('app.foo.bar', '{jslang}app.foo.bar{/jslang}');\n});\n</script>\n
"},{"location":"javascript/new-api_core/#addobjectobject-object","title":"addObject(object: Object)
","text":"Registers a list of phrases using a plain object.
<script data-relocate=\"true\">\nrequire([\"Language\"], function(Language) {\nLanguage.addObject({\n'app.foo.bar': '{jslang}app.foo.bar{/jslang}'\n});\n});\n</script>\n
"},{"location":"javascript/new-api_core/#getkey-string-parameters-object-string","title":"get(key: string, parameters?: Object): string
","text":"Retrieves a phrase by its key, optionally supporting basic template scripting with dynamic variables passed using the parameters
object.
require([\"Language\"], function(Language) {\nvar title = Language.get(\"app.foo.title\");\nvar content = Language.get(\"app.foo.content\", {\nsome: \"value\"\n});\n});\n
"},{"location":"javascript/new-api_core/#stringutil","title":"StringUtil
","text":""},{"location":"javascript/new-api_core/#escapehtmlstr-string-string","title":"escapeHTML(str: string): string
","text":"Escapes special HTML characters by converting them into an HTML entity.
Character Replacement &
&
\"
"
<
<
>
>
"},{"location":"javascript/new-api_core/#escaperegexpstr-string-string","title":"escapeRegExp(str: string): string
","text":"Escapes a list of characters that have a special meaning in regular expressions and could alter the behavior when embedded into regular expressions.
"},{"location":"javascript/new-api_core/#lcfirststr-string-string","title":"lcfirst(str: string): string
","text":"Makes a string's first character lowercase.
"},{"location":"javascript/new-api_core/#ucfirststr-string-string","title":"ucfirst(str: string): string
","text":"Makes a string's first character uppercase.
"},{"location":"javascript/new-api_core/#unescapehtmlstr-string-string","title":"unescapeHTML(str: string): string
","text":"Converts some HTML entities into their original character. This is the reverse function of escapeHTML()
.
"},{"location":"javascript/new-api_dialogs/","title":"Dialogs - JavaScript API","text":"This API has been deprecated in WoltLab Suite 6.0, please refer to the new dialog implementation.
"},{"location":"javascript/new-api_dialogs/#introduction","title":"Introduction","text":"Dialogs are full screen overlays that cover the currently visible window area using a semi-opague backdrop and a prominently placed dialog window in the foreground. They shift the attention away from the original content towards the dialog and usually contain additional details and/or dedicated form inputs.
"},{"location":"javascript/new-api_dialogs/#_dialogsetup","title":"_dialogSetup()
","text":"The lazy initialization is performed upon the first invocation from the callee, using the magic _dialogSetup()
method to retrieve the basic configuration for the dialog construction and any event callbacks.
// App/Foo.js\ndefine([\"Ui/Dialog\"], function(UiDialog) {\n\"use strict\";\n\nfunction Foo() {};\nFoo.prototype = {\nbar: function() {\n// this will open the dialog constructed by _dialogSetup\nUiDialog.open(this);\n},\n\n_dialogSetup: function() {\nreturn {\nid: \"myDialog\",\nsource: \"<p>Hello World!</p>\",\noptions: {\nonClose: function() {\n// the fancy dialog was closed!\n}\n}\n}\n}\n};\n\nreturn Foo;\n});\n
"},{"location":"javascript/new-api_dialogs/#id-string","title":"id: string
","text":"The id
is used to identify a dialog on runtime, but is also part of the first- time setup when the dialog has not been opened before. If source
is undefined
, the module attempts to construct the dialog using an element with the same id.
"},{"location":"javascript/new-api_dialogs/#source-any","title":"source: any
","text":"There are six different types of value that source
does allow and each of them changes how the initial dialog is constructed:
undefined
The dialog exists already and the value of id
should be used to identify the element. null
The HTML is provided using the second argument of .open()
. () => void
If the source
is a function, it is executed and is expected to start the dialog initialization itself. Object
Plain objects are interpreted as parameters for an Ajax request, in particular source.data
will be used to issue the request. It is possible to specify the key source.after
as a callback (content: Element, responseData: Object) => void
that is executed after the dialog was opened. string
The string is expected to be plain HTML that should be used to construct the dialog. DocumentFragment
A new container <div>
with the provided id
is created and the contents of the DocumentFragment
is appended to it. This container is then used for the dialog.
"},{"location":"javascript/new-api_dialogs/#options-object","title":"options: Object
","text":"All configuration options and callbacks are handled through this object.
"},{"location":"javascript/new-api_dialogs/#optionsbackdropcloseonclick-boolean","title":"options.backdropCloseOnClick: boolean
","text":"Defaults to true
.
Clicks on the dialog backdrop will close the top-most dialog. This option will be force-disabled if the option closeable
is set to false
.
"},{"location":"javascript/new-api_dialogs/#optionsclosable-boolean","title":"options.closable: boolean
","text":"Defaults to true
.
Enables the close button in the dialog title, when disabled the dialog can be closed through the .close()
API call only.
"},{"location":"javascript/new-api_dialogs/#optionsclosebuttonlabel-string","title":"options.closeButtonLabel: string
","text":"Defaults to Language.get(\"wcf.global.button.close\")
.
The phrase that is displayed in the tooltip for the close button.
"},{"location":"javascript/new-api_dialogs/#optionscloseconfirmmessage-string","title":"options.closeConfirmMessage: string
","text":"Defaults to \"\"
.
Shows a confirmation dialog using the configured message before closing the dialog. The dialog will not be closed if the dialog is rejected by the user.
"},{"location":"javascript/new-api_dialogs/#optionstitle-string","title":"options.title: string
","text":"Defaults to \"\"
.
The phrase that is displayed in the dialog title.
"},{"location":"javascript/new-api_dialogs/#optionsonbeforeclose-id-string-void","title":"options.onBeforeClose: (id: string) => void
","text":"Defaults to null
.
The callback is executed when the user clicks on the close button or, if enabled, on the backdrop. The callback is responsible to close the dialog by itself, the default close behavior is automatically prevented.
"},{"location":"javascript/new-api_dialogs/#optionsonclose-id-string-void","title":"options.onClose: (id: string) => void
","text":"Defaults to null
.
The callback is notified once the dialog is about to be closed, but is still visible at this point. It is not possible to abort the close operation at this point.
"},{"location":"javascript/new-api_dialogs/#optionsonshow-content-element-void","title":"options.onShow: (content: Element) => void
","text":"Defaults to null
.
Receives the dialog content element as its only argument, allowing the callback to modify the DOM or to register event listeners before the dialog is presented to the user. The dialog is already visible at call time, but the dialog has not been finalized yet.
"},{"location":"javascript/new-api_dialogs/#settitleid-string-object-title-string","title":"setTitle(id: string | Object, title: string)
","text":"Sets the title of a dialog.
"},{"location":"javascript/new-api_dialogs/#setcallbackid-string-object-key-string-value-data-any-void-null","title":"setCallback(id: string | Object, key: string, value: (data: any) => void | null)
","text":"Sets a callback function after the dialog initialization, the special value null
will remove a previously set callback. Valid values for key
are onBeforeClose
, onClose
and onShow
.
"},{"location":"javascript/new-api_dialogs/#rebuildid-string-object","title":"rebuild(id: string | Object)
","text":"Rebuilds a dialog by performing various calculations on the maximum dialog height in regards to the overflow handling and adjustments for embedded forms. This method is automatically invoked whenever a dialog is shown, after invoking the options.onShow
callback.
"},{"location":"javascript/new-api_dialogs/#closeid-string-object","title":"close(id: string | Object)
","text":"Closes an open dialog, this will neither trigger a confirmation dialog, nor does it invoke the options.onBeforeClose
callback. The options.onClose
callback will always be invoked, but it cannot abort the close operation.
"},{"location":"javascript/new-api_dialogs/#getdialogid-string-object-object","title":"getDialog(id: string | Object): Object
","text":"This method returns an internal data object by reference, any modifications made do have an effect on the dialogs behavior and in particular no validation is performed on the modification. It is strongly recommended to use the .set*()
methods only.
Returns the internal dialog data that is attached to a dialog. The most important key is .content
which holds a reference to the dialog's inner content element.
"},{"location":"javascript/new-api_dialogs/#isopenid-string-object-boolean","title":"isOpen(id: string | Object): boolean
","text":"Returns true if the dialog exists and is open.
"},{"location":"javascript/new-api_dom/","title":"Working with the DOM - JavaScript API","text":""},{"location":"javascript/new-api_dom/#domutil","title":"Dom/Util
","text":""},{"location":"javascript/new-api_dom/#createfragmentfromhtmlhtml-string-documentfragment","title":"createFragmentFromHtml(html: string): DocumentFragment
","text":"Parses a HTML string and creates a DocumentFragment
object that holds the resulting nodes.
"},{"location":"javascript/new-api_dom/#identifyelement-element-string","title":"identify(element: Element): string
","text":"Retrieves the unique identifier (id
) of an element. If it does not currently have an id assigned, a generic identifier is used instead.
"},{"location":"javascript/new-api_dom/#outerheightelement-element-styles-cssstyledeclaration-number","title":"outerHeight(element: Element, styles?: CSSStyleDeclaration): number
","text":"Computes the outer height of an element using the element's offsetHeight
and the sum of the rounded down values for margin-top
and margin-bottom
.
"},{"location":"javascript/new-api_dom/#outerwidthelement-element-styles-cssstyledeclaration-number","title":"outerWidth(element: Element, styles?: CSSStyleDeclaration): number
","text":"Computes the outer width of an element using the element's offsetWidth
and the sum of the rounded down values for margin-left
and margin-right
.
"},{"location":"javascript/new-api_dom/#outerdimensionselement-element-height-number-width-number","title":"outerDimensions(element: Element): { height: number, width: number }
","text":"Computes the outer dimensions of an element including its margins.
"},{"location":"javascript/new-api_dom/#offsetelement-element-top-number-left-number","title":"offset(element: Element): { top: number, left: number }
","text":"Computes the element's offset relative to the top left corner of the document.
"},{"location":"javascript/new-api_dom/#setinnerhtmlelement-element-innerhtml-string","title":"setInnerHtml(element: Element, innerHtml: string)
","text":"Sets the inner HTML of an element via element.innerHTML = innerHtml
. Browsers do not evaluate any embedded <script>
tags, therefore this method extracts each of them, creates new <script>
tags and inserts them in their original order of appearance.
"},{"location":"javascript/new-api_dom/#containselement-element-child-element-boolean","title":"contains(element: Element, child: Element): boolean
","text":"Evaluates if element
is a direct or indirect parent element of child
.
"},{"location":"javascript/new-api_dom/#unwrapchildnodeselement-element","title":"unwrapChildNodes(element: Element)
","text":"Moves all child nodes out of element
while maintaining their order, then removes element
from the document.
"},{"location":"javascript/new-api_dom/#domchangelistener","title":"Dom/ChangeListener
","text":"This class is used to observe specific changes to the DOM, for example after an Ajax request has completed. For performance reasons this is a manually-invoked listener that does not rely on a MutationObserver
.
require([\"Dom/ChangeListener\"], function(DomChangeListener) {\nDomChangeListener.add(\"App/Foo\", function() {\n// the DOM may have been altered significantly\n});\n\n// propagate changes to the DOM\nDomChangeListener.trigger();\n});\n
"},{"location":"javascript/new-api_events/","title":"Event Handling - JavaScript API","text":""},{"location":"javascript/new-api_events/#eventkey","title":"EventKey
","text":"This class offers a set of static methods that can be used to determine if some common keys are being pressed. Internally it compares either the .key
property if it is supported or the value of .which
.
require([\"EventKey\"], function(EventKey) {\nelBySel(\".some-input\").addEventListener(\"keydown\", function(event) {\nif (EventKey.Enter(event)) {\n// the `Enter` key was pressed\n}\n});\n});\n
"},{"location":"javascript/new-api_events/#arrowdownevent-keyboardevent-boolean","title":"ArrowDown(event: KeyboardEvent): boolean
","text":"Returns true if the user has pressed the \u2193
key.
"},{"location":"javascript/new-api_events/#arrowleftevent-keyboardevent-boolean","title":"ArrowLeft(event: KeyboardEvent): boolean
","text":"Returns true if the user has pressed the \u2190
key.
"},{"location":"javascript/new-api_events/#arrowrightevent-keyboardevent-boolean","title":"ArrowRight(event: KeyboardEvent): boolean
","text":"Returns true if the user has pressed the \u2192
key.
"},{"location":"javascript/new-api_events/#arrowupevent-keyboardevent-boolean","title":"ArrowUp(event: KeyboardEvent): boolean
","text":"Returns true if the user has pressed the \u2191
key.
"},{"location":"javascript/new-api_events/#commaevent-keyboardevent-boolean","title":"Comma(event: KeyboardEvent): boolean
","text":"Returns true if the user has pressed the ,
key.
"},{"location":"javascript/new-api_events/#enterevent-keyboardevent-boolean","title":"Enter(event: KeyboardEvent): boolean
","text":"Returns true if the user has pressed the \u21b2
key.
"},{"location":"javascript/new-api_events/#escapeevent-keyboardevent-boolean","title":"Escape(event: KeyboardEvent): boolean
","text":"Returns true if the user has pressed the Esc
key.
"},{"location":"javascript/new-api_events/#tabevent-keyboardevent-boolean","title":"Tab(event: KeyboardEvent): boolean
","text":"Returns true if the user has pressed the \u21b9
key.
"},{"location":"javascript/new-api_events/#eventhandler","title":"EventHandler
","text":"A synchronous event system based on string identifiers rather than DOM elements, similar to the PHP event system in WoltLab Suite. Any components can listen to events or trigger events itself at any time.
"},{"location":"javascript/new-api_events/#identifiying-events-with-the-developer-tools","title":"Identifiying Events with the Developer Tools","text":"The Developer Tools offer an easy option to identify existing events that are fired while code is being executed. You can enable this watch mode through your browser's console using Devtools.toggleEventLogging()
:
> Devtools.toggleEventLogging();\n< Event logging enabled\n< [Devtools.EventLogging] Firing event: bar @ com.example.app.foo\n< [Devtools.EventLogging] Firing event: baz @ com.example.app.foo\n
"},{"location":"javascript/new-api_events/#addidentifier-string-action-string-callback-data-object-void-string","title":"add(identifier: string, action: string, callback: (data: Object) => void): string
","text":"Adding an event listeners returns a randomly generated UUIDv4 that is used to identify the listener. This UUID is required to remove a specific listener through the remove()
method.
"},{"location":"javascript/new-api_events/#fireidentifier-string-action-string-data-object","title":"fire(identifier: string, action: string, data?: Object)
","text":"Triggers an event using an optional data
object that is passed to each listener by reference.
"},{"location":"javascript/new-api_events/#removeidentifier-string-action-string-uuid-string","title":"remove(identifier: string, action: string, uuid: string)
","text":"Removes a previously registered event listener using the UUID returned by add()
.
"},{"location":"javascript/new-api_events/#removeallidentifier-string-action-string","title":"removeAll(identifier: string, action: string)
","text":"Removes all event listeners registered for the provided identifier
and action
.
"},{"location":"javascript/new-api_events/#removeallbysuffixidentifier-string-suffix-string","title":"removeAllBySuffix(identifier: string, suffix: string)
","text":"Removes all event listeners for an identifier
whose action ends with the value of suffix
.
"},{"location":"javascript/new-api_ui/","title":"User Interface - JavaScript API","text":""},{"location":"javascript/new-api_ui/#uialignment","title":"Ui/Alignment
","text":"Calculates the alignment of one element relative to another element, with support for boundary constraints, alignment restrictions and additional pointer elements.
"},{"location":"javascript/new-api_ui/#setelement-element-referenceelement-element-options-object","title":"set(element: Element, referenceElement: Element, options: Object)
","text":"Calculates and sets the alignment of the element element
.
"},{"location":"javascript/new-api_ui/#verticaloffset-number","title":"verticalOffset: number
","text":"Defaults to 0
.
Creates a gap between the element and the reference element, in pixels.
"},{"location":"javascript/new-api_ui/#pointer-boolean","title":"pointer: boolean
","text":"Defaults to false
.
Sets the position of the pointer element, requires an existing child of the element with the CSS class .elementPointer
.
"},{"location":"javascript/new-api_ui/#pointeroffset-number","title":"pointerOffset: number
","text":"Defaults to 4
.
The margin from the left/right edge of the element and is used to avoid the arrow from being placed right at the edge.
Does not apply when aligning the element to the reference elemnent's center.
"},{"location":"javascript/new-api_ui/#pointerclassnames-string","title":"pointerClassNames: string[]
","text":"Defaults to []
.
If your element uses CSS-only pointers, such as using the ::before
or ::after
pseudo selectors, you can specifiy two separate CSS class names that control the alignment:
pointerClassNames[0]
is applied to the element when the pointer is displayed at the bottom. pointerClassNames[1]
is used to align the pointer to the right side of the element.
"},{"location":"javascript/new-api_ui/#refdimensionselement-element","title":"refDimensionsElement: Element
","text":"Defaults to null
.
An alternative element that will be used to determine the position and dimensions of the reference element. This can be useful if you reference element is contained in a wrapper element with alternating dimensions.
"},{"location":"javascript/new-api_ui/#horizontal-string","title":"horizontal: string
","text":"This value is automatically flipped for RTL (right-to-left) languages, left
is changed into right
and vice versa.
Defaults to \"left\"
.
Sets the prefered alignment, accepts either left
or right
. The value left
instructs the module to align the element with the left boundary of the reference element.
The horizontal
alignment is used as the default and a flip only occurs, if there is not enough space in the desired direction. If the element exceeds the boundaries in both directions, the value of horizontal
is used.
"},{"location":"javascript/new-api_ui/#vertical-string","title":"vertical: string
","text":"Defaults to \"bottom\"
.
Sets the prefered alignment, accepts either bottom
or top
. The value bottom
instructs the module to align the element below the reference element.
The vertical
alignment is used as the default and a flip only occurs, if there is not enough space in the desired direction. If the element exceeds the boundaries in both directions, the value of vertical
is used.
"},{"location":"javascript/new-api_ui/#allowflip-string","title":"allowFlip: string
","text":"The value for horizontal
is automatically flipped for RTL (right-to-left) languages, left
is changed into right
and vice versa. This setting only controls the behavior when violating space constraints, therefore the aforementioned transformation is always applied.
Defaults to \"both\"
.
Restricts the automatic alignment flipping if the element exceeds the window boundaries in the instructed direction.
both
- No restrictions. horizontal
- Element can be aligned with the left or the right boundary of the reference element, but the vertical position is fixed. vertical
- Element can be aligned below or above the reference element, but the vertical position is fixed. none
- No flipping can occur, the element will be aligned regardless of any space constraints.
"},{"location":"javascript/new-api_ui/#uicloseoverlay","title":"Ui/CloseOverlay
","text":"Register elements that should be closed when the user clicks anywhere else, such as drop-down menus or tooltips.
require([\"Ui/CloseOverlay\"], function(UiCloseOverlay) {\nUiCloseOverlay.add(\"App/Foo\", function() {\n// invoked, close something\n});\n});\n
"},{"location":"javascript/new-api_ui/#addidentifier-string-callback-void","title":"add(identifier: string, callback: () => void)
","text":"Adds a callback that will be invoked when the user clicks anywhere else.
"},{"location":"javascript/new-api_ui/#uiconfirmation","title":"Ui/Confirmation
","text":"Prompt the user to make a decision before carrying out an action, such as a safety warning before permanently deleting content.
require([\"Ui/Confirmation\"], function(UiConfirmation) {\nUiConfirmation.show({\nconfirm: function() {\n// the user has confirmed the dialog\n},\nmessage: \"Do you really want to continue?\"\n});\n});\n
"},{"location":"javascript/new-api_ui/#showoptions-object","title":"show(options: Object)
","text":"Displays a dialog overlay with actions buttons to confirm or reject the dialog.
"},{"location":"javascript/new-api_ui/#cancel-parameters-object-void","title":"cancel: (parameters: Object) => void
","text":"Defaults to null
.
Callback that is invoked when the dialog was rejected.
"},{"location":"javascript/new-api_ui/#confirm-parameters-object-void","title":"confirm: (parameters: Object) => void
","text":"Defaults to null
.
Callback that is invoked when the user has confirmed the dialog.
"},{"location":"javascript/new-api_ui/#message-string","title":"message: string
","text":"Defaults to '\"\"'.
Text that is displayed in the content area of the dialog, optionally this can be HTML, but this requires messageIsHtml
to be enabled.
"},{"location":"javascript/new-api_ui/#messageishtml","title":"messageIsHtml
","text":"Defaults to false
.
The message
option is interpreted as text-only, setting this option to true
will cause the message
to be evaluated as HTML.
"},{"location":"javascript/new-api_ui/#parameters-object","title":"parameters: Object
","text":"Optional list of parameter options that will be passed to the cancel()
and confirm()
callbacks.
"},{"location":"javascript/new-api_ui/#template-string","title":"template: string
","text":"An optional HTML template that will be inserted into the dialog content area, but after the message
section.
"},{"location":"javascript/new-api_ui/#uinotification","title":"Ui/Notification
","text":"Displays a simple notification at the very top of the window, such as a success message for Ajax based actions.
require([\"Ui/Notification\"], function(UiNotification) {\nUiNotification.show(\n\"Your changes have been saved.\",\nfunction() {\n// this callback will be invoked after 2 seconds\n},\n\"success\"\n);\n});\n
"},{"location":"javascript/new-api_ui/#showmessage-string-callback-void-cssclassname-string","title":"show(message: string, callback?: () => void, cssClassName?: string)
","text":"Shows the notification and executes the callback after 2 seconds.
"},{"location":"javascript/new-api_writing-a-module/","title":"Writing a Module - JavaScript API","text":""},{"location":"javascript/new-api_writing-a-module/#introduction","title":"Introduction","text":"The new JavaScript-API was introduced with WoltLab Suite 3.0 and was a major change in all regards. The previously used API heavily relied on larger JavaScript files that contained a lot of different components with hidden dependencies and suffered from extensive jQuery usage for historic reasons.
Eventually a new API was designed that solves the issues with the legacy API by following a few basic principles: 1. Vanilla ES5-JavaScript. It allows us to achieve the best performance across all platforms, there is simply no reason to use jQuery today and the performance penalty on mobile devices is a real issue. 2. Strict usage of modules. Each component is placed in an own file and all dependencies are explicitly declared and injected at the top.Eventually we settled with AMD-style modules using require.js which offers both lazy loading and \"ahead of time\"-compilatio with r.js
. 3. No jQuery-based components on page init. Nothing is more annoying than loading a page and then wait for JavaScript to modify the page before it becomes usable, forcing the user to sit and wait. Heavily optimized vanilla JavaScript components offered the speed we wanted. 4. Limited backwards-compatibility. The new API should make it easy to update existing components by providing similar interfaces, while still allowing legacy code to run side-by-side for best compatibility and to avoid rewritting everything from the start.
"},{"location":"javascript/new-api_writing-a-module/#defining-a-module","title":"Defining a Module","text":"The default location for modules is js/
in the Core's app dir, but every app and plugin can register their own lookup path by providing the path using a template-listener on requirePaths@headIncludeJavaScript
.
For this example we'll assume the file is placed at js/WoltLabSuite/Core/Ui/Foo.js
, the module name is therefore WoltLabSuite/Core/Ui/Foo
, it is automatically derived from the file path and name.
For further instructions on how to define and require modules head over to the RequireJS API.
define([\"Ajax\", \"WoltLabSuite/Core/Ui/Bar\"], function(Ajax, UiBar) {\n\"use strict\";\n\nfunction Foo() { this.init(); }\nFoo.prototype = {\ninit: function() {\nelBySel(\".myButton\").addEventListener(WCF_CLICK_EVENT, this._click.bind(this));\n},\n\n_click: function(event) {\nevent.preventDefault();\n\nif (UiBar.isSnafucated()) {\nAjax.api(this);\n}\n},\n\n_ajaxSuccess: function(data) {\nconsole.log(\"Received response\", data);\n},\n\n_ajaxSetup: function() {\nreturn {\ndata: {\nactionName: \"makeSnafucated\",\nclassName: \"wcf\\\\data\\\\foo\\\\FooAction\"\n}\n};\n}\n}\n\nreturn Foo;\n});\n
"},{"location":"javascript/new-api_writing-a-module/#loading-a-module","title":"Loading a Module","text":"Modules can then be loaded through their derived name:
<script data-relocate=\"true\">\nrequire([\"WoltLabSuite/Core/Ui/Foo\"], function(UiFoo) {\nnew UiFoo();\n});\n</script>\n
"},{"location":"javascript/new-api_writing-a-module/#module-aliases","title":"Module Aliases","text":"Some common modules have short-hand aliases that can be used to include them without writing out their full name. You can still use their original path, but it is strongly recommended to use the aliases for consistency.
Alias Full Path Ajax WoltLabSuite/Core/Ajax AjaxJsonp WoltLabSuite/Core/Ajax/Jsonp AjaxRequest WoltLabSuite/Core/Ajax/Request CallbackList WoltLabSuite/Core/CallbackList ColorUtil WoltLabSuite/Core/ColorUtil Core WoltLabSuite/Core/Core DateUtil WoltLabSuite/Core/Date/Util Devtools WoltLabSuite/Core/Devtools Dom/ChangeListener WoltLabSuite/Core/Dom/Change/Listener Dom/Traverse WoltLabSuite/Core/Dom/Traverse Dom/Util WoltLabSuite/Core/Dom/Util Environment WoltLabSuite/Core/Environment EventHandler WoltLabSuite/Core/Event/Handler EventKey WoltLabSuite/Core/Event/Key Language WoltLabSuite/Core/Language Permission WoltLabSuite/Core/Permission StringUtil WoltLabSuite/Core/StringUtil Ui/Alignment WoltLabSuite/Core/Ui/Alignment Ui/CloseOverlay WoltLabSuite/Core/Ui/CloseOverlay Ui/Confirmation WoltLabSuite/Core/Ui/Confirmation Ui/Dialog WoltLabSuite/Core/Ui/Dialog Ui/Notification WoltLabSuite/Core/Ui/Notification Ui/ReusableDropdown WoltLabSuite/Core/Ui/Dropdown/Reusable Ui/Screen WoltLabSuite/Core/Ui/Screen Ui/Scroll WoltLabSuite/Core/Ui/Scroll Ui/SimpleDropdown WoltLabSuite/Core/Ui/Dropdown/Simple Ui/TabMenu WoltLabSuite/Core/Ui/TabMenu Upload WoltLabSuite/Core/Upload User WoltLabSuite/Core/User"},{"location":"javascript/typescript/","title":"TypeScript","text":""},{"location":"javascript/typescript/#consuming-woltlab-suites-types","title":"Consuming WoltLab Suite\u2019s Types","text":"To consume the types of WoltLab Suite, you will need to install the @woltlab/wcf
npm package using a git URL that refers to the appropriate branch of WoltLab/WCF.
A full package.json
that includes WoltLab Suite, TypeScript, eslint and Prettier could look like the following.
package.json {\n\"devDependencies\": {\n\"@typescript-eslint/eslint-plugin\": \"^5.51.0\",\n\"@typescript-eslint/parser\": \"^5.51.0\",\n\"eslint\": \"^8.33.0\",\n\"eslint-config-prettier\": \"^8.6.0\",\n\"prettier\": \"^2.8.4\",\n\"typescript\": \"^4.9.5\"\n},\n\"dependencies\": {\n\"@woltlab/d.ts\": \"https://github.com/WoltLab/d.ts.git#4040fc083245edeb2f8832d4613b9e76aa9c17a5\"\n}\n}\n
After installing the types using npm, you will also need to configure tsconfig.json
to take the types into account. To do so, you will need to add them to the compilerOptions.paths
option. A complete tsconfig.json
file that matches the configuration of WoltLab Suite could look like the following.
tsconfig.json {\n\"include\": [\n\"node_modules/@woltlab/d.ts/global.d.ts\",\n\"ts/**/*\"\n],\n\"compilerOptions\": {\n\"target\": \"ES2022\",\n\"module\": \"amd\",\n\"rootDir\": \"ts/\",\n\"outDir\": \"files/js/\",\n\"lib\": [\n\"DOM\",\n\"DOM.Iterable\",\n\"ES2022\"\n],\n\"strictNullChecks\": true,\n\"moduleResolution\": \"node\",\n\"esModuleInterop\": true,\n\"noImplicitThis\": true,\n\"strictBindCallApply\": true,\n\"baseUrl\": \".\",\n\"paths\": {\n\"*\": [\n\"node_modules/@woltlab/d.ts/*\"\n]\n},\n\"importHelpers\": true,\n\"newLine\": \"lf\"\n}\n}\n
After this initial set-up, you would place your TypeScript source files into the ts/
folder of your project. The generated JavaScript target files will be placed into files/js/
and thus will be installed by the file PIP.
To update the TypeScript types, the commit hash in package.json
needs to be updated to an appropriate commit in the d.ts repository and npm install
needs to be rerun.
"},{"location":"javascript/typescript/#additional-tools","title":"Additional Tools","text":"WoltLab Suite uses additional tools to ensure the high quality and a consistent code style of the TypeScript modules. The current configuration of these tools is as follows. It is recommended to re-use this configuration as is.
.prettierrc trailingComma: all\nprintWidth: 120\n
.eslintrc.js module.exports = {\nroot: true,\nparser: \"@typescript-eslint/parser\",\nparserOptions: {\ntsconfigRootDir: __dirname,\nproject: [\"./tsconfig.json\"]\n},\nplugins: [\"@typescript-eslint\"],\nextends: [\n\"eslint:recommended\",\n\"plugin:@typescript-eslint/recommended\",\n\"plugin:@typescript-eslint/recommended-requiring-type-checking\",\n\"prettier\"\n],\nrules: {\n\"@typescript-eslint/ban-types\": [\n\"error\", {\ntypes: {\n\"object\": false\n},\nextendDefaults: true\n}\n],\n\"@typescript-eslint/no-explicit-any\": 0,\n\"@typescript-eslint/no-non-null-assertion\": 0,\n\"@typescript-eslint/no-unsafe-assignment\": 0,\n\"@typescript-eslint/no-unsafe-call\": 0,\n\"@typescript-eslint/no-unsafe-member-access\": 0,\n\"@typescript-eslint/no-unsafe-return\": 0,\n\"@typescript-eslint/no-unused-vars\": [\n\"error\", {\n\"argsIgnorePattern\": \"^_\"\n}\n]\n}\n};\n
.eslintignore **/*.js\nvendor/**\n
This .gitattributes
configuration will automatically collapse the generated JavaScript target files in GitHub\u2019s Diff view. You will not need it if you do not use git or GitHub.
.gitattributes files/js/**/*.js linguist-generated\n
"},{"location":"javascript/typescript/#writing-a-simple-module","title":"Writing a simple module","text":"After completing this initial set-up you can start writing your first TypeScript module. The TypeScript compiler can be launched in Watch Mode by running npx tsc -w
.
WoltLab Suite\u2019s modules can be imported using the standard ECMAScript module import syntax by specifying the full module name. The public API of the module can also be exported using the standard ECMAScript module export syntax.
ts/Example.ts import * as Language from \"WoltLabSuite/Core/Language\";\n\nexport function run() {\nalert(Language.get(\"wcf.foo.bar\"));\n}\n
This simple example module will compile to plain JavaScript that is compatible with the AMD loader that is used by WoltLab Suite.
files/js/Example.js define([\"require\", \"exports\", \"tslib\", \"WoltLabSuite/Core/Language\"], function (require, exports, tslib_1, Language) {\n\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.run = void 0;\nLanguage = tslib_1.__importStar(Language);\nfunction run() {\nalert(Language.get(\"wcf.foo.bar\"));\n}\nexports.run = run;\n});\n
Within templates it can be consumed as follows.
<script data-relocate=\"true\">\nrequire([\"Example\"], (Example) => {\nExample.run(); // Alerts the contents of the `wcf.foo.bar` phrase.\n});\n</script>\n
"},{"location":"migration/wcf21/css/","title":"WCF 2.1.x - CSS","text":"The LESS compiler has been in use since WoltLab Community Framework 2.0, but was replaced with a SCSS compiler in WoltLab Suite 3.0. This change was motivated by SCSS becoming the de facto standard for CSS pre-processing and some really annoying shortcomings in the old LESS compiler.
The entire CSS has been rewritten from scratch, please read the docs on CSS to learn what has changed.
"},{"location":"migration/wcf21/package/","title":"WCF 2.1.x - Package Components","text":""},{"location":"migration/wcf21/package/#packagexml","title":"package.xml","text":""},{"location":"migration/wcf21/package/#short-instructions","title":"Short Instructions","text":"Instructions can now omit the filename, causing them to use the default filename if defined by the package installation plugin (in short: PIP
). Unless overridden it will default to the PIP's class name with the first letter being lower-cased, e.g. EventListenerPackageInstallationPlugin
implies the filename eventListener.xml
. The file is always assumed to be in the archive's root, files located in subdirectories need to be explicitly stated, just as it worked before.
Every PIP can define a custom filename if the default value cannot be properly derived. For example the ACPMenu
-pip would default to aCPMenu.xml
, requiring the class to explicitly override the default filename with acpMenu.xml
for readability.
"},{"location":"migration/wcf21/package/#example","title":"Example","text":"<instructions type=\"install\">\n<!-- assumes `eventListener.xml` -->\n<instruction type=\"eventListener\" />\n<!-- assumes `install.sql` -->\n<instruction type=\"sql\" />\n<!-- assumes `language/*.xml` -->\n<instruction type=\"language\" />\n\n<!-- exceptions -->\n\n<!-- assumes `files.tar` -->\n<instruction type=\"file\" />\n<!-- no default value, requires relative path -->\n<instruction type=\"script\">acp/install_com.woltlab.wcf_3.0.php</instruction>\n</instructions>\n
"},{"location":"migration/wcf21/package/#exceptions","title":"Exceptions","text":"These exceptions represent the built-in PIPs only, 3rd party plugins and apps may define their own exceptions.
PIP Default Value acpTemplate
acptemplates.tar
file
files.tar
language
language/*.xml
script
(No default value) sql
install.sql
template
templates.tar
"},{"location":"migration/wcf21/package/#acpmenuxml","title":"acpMenu.xml","text":""},{"location":"migration/wcf21/package/#renamed-categories","title":"Renamed Categories","text":"The following categories have been renamed, menu items need to be adjusted to reflect the new names:
Old Value New Value wcf.acp.menu.link.system
wcf.acp.menu.link.configuration
wcf.acp.menu.link.display
wcf.acp.menu.link.customization
wcf.acp.menu.link.community
wcf.acp.menu.link.application
"},{"location":"migration/wcf21/package/#submenu-items","title":"Submenu Items","text":"Menu items can now offer additional actions to be accessed from within the menu using an icon-based navigation. This step avoids filling the menu with dozens of Add \u2026
links, shifting the focus on to actual items. Adding more than one action is not recommended and you should at maximum specify two actions per item.
"},{"location":"migration/wcf21/package/#example_1","title":"Example","text":"<!-- category -->\n<acpmenuitem name=\"wcf.acp.menu.link.group\">\n<parent>wcf.acp.menu.link.user</parent>\n<showorder>2</showorder>\n</acpmenuitem>\n\n<!-- menu item -->\n<acpmenuitem name=\"wcf.acp.menu.link.group.list\">\n<controller>wcf\\acp\\page\\UserGroupListPage</controller>\n<parent>wcf.acp.menu.link.group</parent>\n<permissions>admin.user.canEditGroup,admin.user.canDeleteGroup</permissions>\n</acpmenuitem>\n<!-- menu item action -->\n<acpmenuitem name=\"wcf.acp.menu.link.group.add\">\n<controller>wcf\\acp\\form\\UserGroupAddForm</controller>\n<!-- actions are defined by menu items of menu items -->\n<parent>wcf.acp.menu.link.group.list</parent>\n<permissions>admin.user.canAddGroup</permissions>\n<!-- required FontAwesome icon name used for display -->\n<icon>fa-plus</icon>\n</acpmenuitem>\n
"},{"location":"migration/wcf21/package/#common-icon-names","title":"Common Icon Names","text":"You should use the same icon names for the (logically) same task, unifying the meaning of items and making the actions predictable.
Meaning Icon Name Result Add or create fa-plus
Search fa-search
Upload fa-upload
"},{"location":"migration/wcf21/package/#boxxml","title":"box.xml","text":"The box PIP has been added.
"},{"location":"migration/wcf21/package/#cronjobxml","title":"cronjob.xml","text":"Legacy cronjobs are assigned a non-deterministic generic name, the only way to assign them a name is removing them and then adding them again.
Cronjobs can now be assigned a name using the name attribute as in <cronjob name=\"com.woltlab.wcf.refreshPackageUpdates\">
, it will be used to identify cronjobs during an update or delete.
"},{"location":"migration/wcf21/package/#eventlistenerxml","title":"eventListener.xml","text":"Legacy event listeners are assigned a non-deterministic generic name, the only way to assign them a name is removing them and then adding them again.
Event listeners can now be assigned a name using the name attribute as in <eventlistener name=\"sessionPageAccessLog\">
, it will be used to identify event listeners during an update or delete.
"},{"location":"migration/wcf21/package/#menuxml","title":"menu.xml","text":"The menu PIP has been added.
"},{"location":"migration/wcf21/package/#menuitemxml","title":"menuItem.xml","text":"The menuItem PIP has been added.
"},{"location":"migration/wcf21/package/#objecttypexml","title":"objectType.xml","text":"The definition com.woltlab.wcf.user.dashboardContainer
has been removed, it was previously used to register pages that qualify for dashboard boxes. Since WoltLab Suite 3.0, all pages registered through the page.xml
are valid containers and therefore there is no need for this definition anymore.
The definitions com.woltlab.wcf.page
and com.woltlab.wcf.user.online.location
have been superseded by the page.xml
, they're no longer supported.
"},{"location":"migration/wcf21/package/#optionxml","title":"option.xml","text":"The module.display
category has been renamed into module.customization
.
"},{"location":"migration/wcf21/package/#pagexml","title":"page.xml","text":"The page PIP has been added.
"},{"location":"migration/wcf21/package/#pagemenuxml","title":"pageMenu.xml","text":"The pageMenu.xml
has been superseded by the page.xml
and is no longer available.
"},{"location":"migration/wcf21/php/","title":"WCF 2.1.x - PHP","text":""},{"location":"migration/wcf21/php/#message-processing","title":"Message Processing","text":"WoltLab Suite 3.0 finally made the transition from raw bbcode to bbcode-flavored HTML, with many new features related to message processing being added. This change impacts both message validation and storing, requiring slightly different APIs to get the job done.
"},{"location":"migration/wcf21/php/#input-processing-for-storage","title":"Input Processing for Storage","text":"The returned HTML is an intermediate representation with a maximum of meta data embedded into it, designed to be stored in the database. Some bbcodes are replaced during this process, for example [b]\u2026[/b]
becomes <strong>\u2026</strong>
, while others are converted into a metacode tag for later processing.
<?php\n$processor = new \\wcf\\system\\html\\input\\HtmlInputProcessor();\n$processor->process($message, $messageObjectType, $messageObjectID);\n$html = $processor->getHtml();\n
The $messageObjectID
can be zero if the element did not exist before, but it should be non-zero when saving an edited message.
"},{"location":"migration/wcf21/php/#embedded-objects","title":"Embedded Objects","text":"Embedded objects need to be registered after saving the message, but once again you can use the processor instance to do the job.
<?php\n$processor = new \\wcf\\system\\html\\input\\HtmlInputProcessor();\n$processor->process($message, $messageObjectType, $messageObjectID);\n$html = $processor->getHtml();\n\n// at this point the message is saved to database and the created object\n// `$example` is a `DatabaseObject` with the id column `$exampleID`\n\n$processor->setObjectID($example->exampleID);\nif (\\wcf\\system\\message\\embedded\\object\\MessageEmbeddedObjectManager::getInstance()->registerObjects($processor)) {\n // there is at least one embedded object, this is also the point at which you\n // would set `hasEmbeddedObjects` to true (if implemented by your type)\n (new \\wcf\\data\\example\\ExampleEditor($example))->update(['hasEmbeddedObjects' => 1]);\n}\n
"},{"location":"migration/wcf21/php/#rendering-the-message","title":"Rendering the Message","text":"The output processor will parse the intermediate HTML and finalize the output for display. This step is highly dynamic and allows for bbcode evaluation and contextual output based on the viewer's permissions.
<?php\n$processor = new \\wcf\\system\\html\\output\\HtmlOutputProcessor();\n$processor->process($html, $messageObjectType, $messageObjectID);\n$renderedHtml = $processor->getHtml();\n
"},{"location":"migration/wcf21/php/#simplified-output","title":"Simplified Output","text":"At some point there can be the need of a simplified output HTML that includes only basic HTML formatting and reduces more sophisticated bbcodes into a simpler representation.
<?php\n$processor = new \\wcf\\system\\html\\output\\HtmlOutputProcessor();\n$processor->setOutputType('text/simplified-html');\n$processor->process(\u2026);\n
"},{"location":"migration/wcf21/php/#plaintext-output","title":"Plaintext Output","text":"The text/plain
output type will strip down the simplified HTML into pure text, suitable for text-only output such as the plaintext representation of an email.
<?php\n$processor = new \\wcf\\system\\html\\output\\HtmlOutputProcessor();\n$processor->setOutputType('text/plain');\n$processor->process(\u2026);\n
"},{"location":"migration/wcf21/php/#rebuilding-data","title":"Rebuilding Data","text":""},{"location":"migration/wcf21/php/#converting-from-bbcode","title":"Converting from BBCode","text":"Enabling message conversion for HTML messages is undefined and yields unexpected results.
Legacy message that still use raw bbcodes must be converted to be properly parsed by the html processors. This process is enabled by setting the fourth parameter of process()
to true
.
<?php\n$processor = new \\wcf\\system\\html\\input\\HtmlInputProcessor();\n$processor->process($html, $messageObjectType, $messageObjectID, true);\n$renderedHtml = $processor->getHtml();\n
"},{"location":"migration/wcf21/php/#extracting-embedded-objects","title":"Extracting Embedded Objects","text":"The process()
method of the input processor is quite expensive, as it runs through the full message validation including the invocation of HTMLPurifier. This is perfectly fine when dealing with single messages, but when you're handling messages in bulk to extract their embedded objects, you're better of with processEmbeddedContent()
. This method deconstructs the message, but skips all validation and expects the input to be perfectly valid, that is the output of a previous run of process()
saved to storage.
<?php\n$processor = new \\wcf\\system\\html\\input\\HtmlInputProcessor();\n$processor->processEmbeddedContent($html, $messageObjectType, $messageObjectID);\n\n// invoke `MessageEmbeddedObjectManager::registerObjects` here\n
"},{"location":"migration/wcf21/php/#breadcrumbs-page-location","title":"Breadcrumbs / Page Location","text":"Breadcrumbs used to be added left to right, but parent locations are added from the bottom to the top, starting with the first ancestor and going upwards. In most cases you simply need to reverse the order.
Breadcrumbs used to be a lose collection of arbitrary links, but are now represented by actual page objects and the control has shifted over to the PageLocationManager
.
<?php\n// before\n\\wcf\\system\\WCF::getBreadcrumbs()->add(new \\wcf\\system\\breadcrumb\\Breadcrumb('title', 'link'));\n\n// after\n\\wcf\\system\\page\\PageLocationManager::getInstance()->addParentLocation($pageIdentifier, $pageObjectID, $object);\n
"},{"location":"migration/wcf21/php/#pages-and-forms","title":"Pages and Forms","text":"The property $activeMenuItem
has been deprecated for the front end and is no longer evaluated at runtime. Recognition of the active item is entirely based around the invoked controller class name and its definition in the page table. You need to properly register your pages for this feature to work.
"},{"location":"migration/wcf21/php/#search","title":"Search","text":""},{"location":"migration/wcf21/php/#isearchableobjecttype","title":"ISearchableObjectType","text":"Added the setLocation()
method that is used to set the current page location based on the search result.
"},{"location":"migration/wcf21/php/#searchindexmanager","title":"SearchIndexManager","text":"The methods SearchIndexManager::add()
and SearchIndexManager::update()
have been deprecated and forward their call to the new method SearchIndexManager::set()
.
"},{"location":"migration/wcf21/templates/","title":"WCF 2.1.x - Templates","text":""},{"location":"migration/wcf21/templates/#page-layout","title":"Page Layout","text":"The template structure has been overhauled and it is no longer required nor recommended to include internal templates, such as documentHeader
, headInclude
or userNotice
. Instead use a simple {include file='header'}
that now takes care of of the entire application frame.
- Templates must not include a trailing
</body></html>
after including the footer
template. - The
documentHeader
, headInclude
and userNotice
template should no longer be included manually, the same goes with the <body>
element, please use {include file='header'}
instead. - The
sidebarOrientation
variable for the header
template has been removed and no longer works. header.boxHeadline
has been unified and now reads header.contentHeader
Please see the full example at the end of this page for more information.
"},{"location":"migration/wcf21/templates/#sidebars","title":"Sidebars","text":"Sidebars are now dynamically populated by the box system, this requires a small change to unify the markup. Additionally the usage of <fieldset>
has been deprecated due to browser inconsistencies and bugs and should be replaced with section.box
.
Previous markup used in WoltLab Community Framework 2.1 and earlier:
<fieldset>\n <legend><!-- Title --></legend>\n\n <div>\n <!-- Content -->\n </div>\n</fieldset>\n
The new markup since WoltLab Suite 3.0:
<section class=\"box\">\n <h2 class=\"boxTitle\"><!-- Title --></h2>\n\n <div class=\"boxContent\">\n <!-- Content -->\n </div>\n</section>\n
"},{"location":"migration/wcf21/templates/#forms","title":"Forms","text":"The input tag for session ids SID_INPUT_TAG
has been deprecated and no longer yields any content, it can be safely removed. In previous versions forms have been wrapped in <div class=\"container containerPadding marginTop\">\u2026</div>
which no longer has any effect and should be removed.
If you're using the preview feature for WYSIWYG-powered input fields, you need to alter the preview button include instruction:
{include file='messageFormPreviewButton' previewMessageObjectType='com.example.foo.bar' previewMessageObjectID=0}\n
The message object id should be non-zero when editing.
"},{"location":"migration/wcf21/templates/#icons","title":"Icons","text":"The old .icon-<iconName>
classes have been removed, you are required to use the official .fa-<iconName>
class names from FontAwesome. This does not affect the generic classes .icon
(indicates an icon) and .icon<size>
(e.g. .icon16
that sets the dimensions), these are still required and have not been deprecated.
Before:
<span class=\"icon icon16 icon-list\">\n
Now:
<span class=\"icon icon16 fa-list\">\n
"},{"location":"migration/wcf21/templates/#changed-icon-names","title":"Changed Icon Names","text":"Quite a few icon names have been renamed, the official wiki lists the new icon names in FontAwesome 4.
"},{"location":"migration/wcf21/templates/#changed-classes","title":"Changed Classes","text":" .dataList
has been replaced and should now read <ol class=\"inlineList commaSeparated\">
(same applies to <ul>
) .framedIconList
has been changed into .userAvatarList
"},{"location":"migration/wcf21/templates/#removed-elements-and-classes","title":"Removed Elements and Classes","text":" <nav class=\"jsClipboardEditor\">
and <div class=\"jsClipboardContainer\">
have been replaced with a floating button. - The anchors
a.toTopLink
have been replaced with a floating button. - Avatars should no longer receive the class
framed
- The
dl.condensed
class, as seen in the editor tab menu, is no longer required. - Anything related to
sidebarCollapsed
has been removed as sidebars are no longer collapsible.
"},{"location":"migration/wcf21/templates/#simple-example","title":"Simple Example","text":"The code below includes only the absolute minimum required to display a page, the content title is already included in the output.
{include file='header'}\n\n<div class=\"section\">\n Hello World!\n</div>\n\n{include file='footer'}\n
"},{"location":"migration/wcf21/templates/#full-example","title":"Full Example","text":"{*\n The page title is automatically set using the page definition, avoid setting it if you can!\n If you really need to modify the title, you can still reference the original title with:\n {$__wcf->getActivePage()->getTitle()}\n*}\n{capture assign='pageTitle'}Custom Page Title{/capture}\n\n{*\n NOTICE: The content header goes here, see the section after this to learn more.\n*}\n\n{* you must not use `headContent` for JavaScript *}\n{capture assign='headContent'}\n <link rel=\"alternate\" type=\"application/rss+xml\" title=\"{lang}wcf.global.button.rss{/lang}\" href=\"\u2026\">\n{/capture}\n\n{* optional, content will be added to the top of the left sidebar *}\n{capture assign='sidebarLeft'}\n \u2026\n\n{event name='boxes'}\n{/capture}\n\n{* optional, content will be added to the top of the right sidebar *}\n{capture assign='sidebarRight'}\n \u2026\n\n{event name='boxes'}\n{/capture}\n\n{capture assign='headerNavigation'}\n <li><a href=\"#\" title=\"Custom Button\" class=\"jsTooltip\"><span class=\"icon icon16 fa-check\"></span> <span class=\"invisible\">Custom Button</span></a></li>\n{/capture}\n\n{include file='header'}\n\n{hascontent}\n <div class=\"paginationTop\">\n{content}\n{pages \u2026}\n{/content}\n </div>\n{/hascontent}\n\n{* the actual content *}\n<div class=\"section\">\n \u2026\n</div>\n\n<footer class=\"contentFooter\">\n{* skip this if you're not using any pagination *}\n{hascontent}\n <div class=\"paginationBottom\">\n{content}{@$pagesLinks}{/content}\n </div>\n{/hascontent}\n\n <nav class=\"contentFooterNavigation\">\n <ul>\n <li><a href=\"\u2026\" class=\"button\"><span class=\"icon icon16 fa-plus\"></span> <span>Custom Button</span></a></li>\n{event name='contentFooterNavigation'}\n </ul>\n </nav>\n</footer>\n\n<script data-relocate=\"true\">\n /* any JavaScript code you need */\n</script>\n\n{* do not include `</body></html>` here, the footer template is the last bit of code! *}\n{include file='footer'}\n
"},{"location":"migration/wcf21/templates/#content-header","title":"Content Header","text":"There are two different methods to set the content header, one sets only the actual values, but leaves the outer HTML untouched, that is generated by the header
template. This is the recommended approach and you should avoid using the alternative method whenever possible.
"},{"location":"migration/wcf21/templates/#recommended-approach","title":"Recommended Approach","text":"{* This is automatically set using the page data and should not be set manually! *}\n{capture assign='contentTitle'}Custom Content Title{/capture}\n\n{capture assign='contentDescription'}Optional description that is displayed right after the title.{/capture}\n\n{capture assign='contentHeaderNavigation'}List of navigation buttons displayed right next to the title.{/capture}\n
"},{"location":"migration/wcf21/templates/#alternative","title":"Alternative","text":"{capture assign='contentHeader'}\n <header class=\"contentHeader\">\n <div class=\"contentHeaderTitle\">\n <h1 class=\"contentTitle\">Custom Content Title</h1>\n <p class=\"contentHeaderDescription\">Custom Content Description</p>\n </div>\n\n <nav class=\"contentHeaderNavigation\">\n <ul>\n <li><a href=\"{link controller='CustomController'}{/link}\" class=\"button\"><span class=\"icon icon16 fa-plus\"></span> <span>Custom Button</span></a></li>\n{event name='contentHeaderNavigation'}\n </ul>\n </nav>\n </header>\n{/capture}\n
"},{"location":"migration/wsc30/css/","title":"Migrating from WSC 3.0 - CSS","text":""},{"location":"migration/wsc30/css/#new-style-variables","title":"New Style Variables","text":"The new style variables are only applied to styles that have the compatibility set to WSC 3.1
"},{"location":"migration/wsc30/css/#wcfcontentcontainer","title":"wcfContentContainer","text":"The page content is encapsulated in a new container that wraps around the inner content, but excludes the sidebars, header and page navigation elements.
$wcfContentContainerBackground
- background color $wcfContentContainerBorder
- border color
"},{"location":"migration/wsc30/css/#wcfeditorbutton","title":"wcfEditorButton","text":"These variables control the appearance of the editor toolbar and its buttons.
$wcfEditorButtonBackground
- button and toolbar background color $wcfEditorButtonBackgroundActive
- active button background color $wcfEditorButtonText
- text color for available buttons $wcfEditorButtonTextActive
- text color for active buttons $wcfEditorButtonTextDisabled
- text color for disabled buttons
"},{"location":"migration/wsc30/css/#color-variables-in-alertscss","title":"Color Variables in alert.scss
","text":"The color values for <small class=\"innerError\">
used to be hardcoded values, but have now been changed to use the values for error messages (wcfStatusError*
) instead.
"},{"location":"migration/wsc30/javascript/","title":"Migrating from WSC 3.0 - JavaScript","text":""},{"location":"migration/wsc30/javascript/#accelerated-guest-view-tiny-builds","title":"Accelerated Guest View / Tiny Builds","text":"The new tiny builds are highly optimized variants of existing JavaScript files and modules, aiming for significant performance improvements for guests and search engines alike. This is accomplished by heavily restricting page interaction to read-only actions whenever possible, which in return removes the need to provide certain JavaScript modules in general.
For example, disallowing guests to write any formatted messages will in return remove the need to provide the WYSIWYG editor at all. But it doesn't stop there, there are a lot of other modules that provide additional features for the editor, and by excluding the editor, we can also exclude these modules too.
Long story short, the tiny mode guarantees that certain actions will never be carried out by guests or search engines, therefore some modules are not going to be needed by them ever.
"},{"location":"migration/wsc30/javascript/#code-templates-for-tiny-builds","title":"Code Templates for Tiny Builds","text":"The following examples assume that you use the virtual constant COMPILER_TARGET_DEFAULT
as a switch for the optimized code path. This is also the constant used by the official build scripts for JavaScript files.
We recommend that you provide a mock implementation for existing code to ensure 3rd party compatibility. It is enough to provide a bare object or class that exposes the original properties using the same primitive data types. This is intended to provide a soft-fail for implementations that are not aware of the tiny mode yet, but is not required for classes that did not exist until now.
"},{"location":"migration/wsc30/javascript/#legacy-javascript","title":"Legacy JavaScript","text":"if (COMPILER_TARGET_DEFAULT) {\nWCF.Example.Foo = {\nmakeSnafucated: function() {\nreturn \"Hello World\";\n}\n};\n\nWCF.Example.Bar = Class.extend({\nfoobar: \"baz\",\n\nfoo: function($bar) {\nreturn $bar + this.foobar;\n}\n});\n}\nelse {\nWCF.Example.Foo = {\nmakeSnafucated: function() {}\n};\n\nWCF.Example.Bar = Class.extend({\nfoobar: \"\",\nfoo: function() {}\n});\n}\n
"},{"location":"migration/wsc30/javascript/#requirejs-modules","title":"require.js Modules","text":"define([\"some\", \"fancy\", \"dependencies\"], function(Some, Fancy, Dependencies) {\n\"use strict\";\n\nif (!COMPILER_TARGET_DEFAULT) {\nvar Fake = function() {};\nFake.prototype = {\ninit: function() {},\nmakeSnafucated: function() {}\n};\nreturn Fake;\n}\n\nfunction MyAwesomeClass(niceArgument) { this.init(niceArgument); }\nMyAwesomeClass.prototype = {\ninit: function(niceArgument) {\nif (niceArgument) {\nthis.makeSnafucated();\n}\n},\n\nmakeSnafucated: function() {\nconsole.log(\"Hello World\");\n}\n}\n\nreturn MyAwesomeClass;\n});\n
"},{"location":"migration/wsc30/javascript/#including-tinified-builds-through-js","title":"Including tinified builds through {js}
","text":"The {js}
template-plugin has been updated to include support for tiny builds controlled through the optional flag hasTiny=true
:
{js application='wcf' file='WCF.Example' hasTiny=true}\n
This line generates a different output depending on the debug mode and the user login-state.
"},{"location":"migration/wsc30/javascript/#real-error-messages-for-ajax-responses","title":"Real Error Messages for AJAX Responses","text":"The errorMessage
property in the returned response object for failed AJAX requests contained an exception-specific but still highly generic error message. This issue has been around for quite a long time and countless of implementations are relying on this false behavior, eventually forcing us to leave the value unchanged.
This problem is solved by adding the new property realErrorMessage
that exposes the message exactly as it was provided and now matches the value that would be displayed to users in traditional forms.
"},{"location":"migration/wsc30/javascript/#example-code","title":"Example Code","text":"define(['Ajax'], function(Ajax) {\nreturn {\n// ...\n_ajaxFailure: function(responseData, responseText, xhr, requestData) {\nconsole.log(responseData.realErrorMessage);\n}\n// ...\n};\n});\n
"},{"location":"migration/wsc30/javascript/#simplified-form-submit-in-dialogs","title":"Simplified Form Submit in Dialogs","text":"Forms embedded in dialogs often do not contain the HTML <form>
-element and instead rely on JavaScript click- and key-handlers to emulate a <form>
-like submit behavior. This has spawned a great amount of nearly identical implementations that all aim to handle the form submit through the Enter
-key, still leaving some dialogs behind.
WoltLab Suite 3.1 offers automatic form submit that is enabled through a set of specific conditions and data attributes:
- There must be a submit button that matches the selector
.formSubmit > input[type=\"submit\"], .formSubmit > button[data-type=\"submit\"]
. - The dialog object provided to
UiDialog.open()
implements the method _dialogSubmit()
. - Input fields require the attribute
data-dialog-submit-on-enter=\"true\"
to be set, the type
must be one of number
, password
, search
, tel
, text
or url
.
Clicking on the submit button or pressing the Enter
-key in any watched input field will start the submit process. This is done automatically and does not require a manual interaction in your code, therefore you should not bind any click listeners on the submit button yourself.
Any input field with the required
attribute set will be validated to contain a non-empty string after processing the value with String.prototype.trim()
. An empty field will abort the submit process and display a visible error message next to the offending field.
"},{"location":"migration/wsc30/javascript/#helper-function-for-inline-error-messages","title":"Helper Function for Inline Error Messages","text":"Displaying inline error messages on-the-fly required quite a few DOM operations that were quite simple but also super repetitive and thus error-prone when incorrectly copied over. The global helper function elInnerError()
was added to provide a simple and consistent behavior of inline error messages.
You can display an error message by invoking elInnerError(elementRef, \"Your Error Message\")
, it will insert a new <small class=\"innerError\">
and sets the given message. If there is already an inner error present, then the message will be replaced instead.
Hiding messages is done by setting the 2nd parameter to false
or an empty string:
elInnerError(elementRef, false)
elInnerError(elementRef, '')
The special values null
and undefined
are supported too, but their usage is discouraged, because they make it harder to understand the intention by reading the code:
elInnerError(elementRef, null)
elInnerError(elementRef)
"},{"location":"migration/wsc30/javascript/#example-code_1","title":"Example Code","text":"require(['Language'], function(Language)) {\nvar input = elBySel('input[type=\"text\"]');\nif (input.value.trim() === '') {\n// displays a new inline error or replaces the message if there is one already\nelInnerError(input, Language.get('wcf.global.form.error.empty'));\n}\nelse {\n// removes the inline error if it exists\nelInnerError(input, false);\n}\n\n// the above condition is equivalent to this:\nelInnerError(input, (input.value.trim() === '' ? Language.get('wcf.global.form.error.empty') : false));\n}\n
"},{"location":"migration/wsc30/package/","title":"Migrating from WSC 3.0 - Package Components","text":""},{"location":"migration/wsc30/package/#cronjob-scheduler-uses-server-timezone","title":"Cronjob Scheduler uses Server Timezone","text":"The execution time of cronjobs was previously calculated based on the coordinated universal time (UTC). This was changed in WoltLab Suite 3.1 to use the server timezone or, to be precise, the default timezone set in the administration control panel.
"},{"location":"migration/wsc30/package/#exclude-pages-from-becoming-a-landing-page","title":"Exclude Pages from becoming a Landing Page","text":"Some pages do not qualify as landing page, because they're designed around specific expectations that aren't matched in all cases. Examples include the user control panel and its sub-pages that cannot be accessed by guests and will therefore break the landing page for those. While it is somewhat to be expected from control panel pages, there are enough pages that fall under the same restrictions, but aren't easily recognized as such by an administrator.
You can exclude these pages by adding <excludeFromLandingPage>1</excludeFromLandingPage>
(case-sensitive) to the relevant pages in your page.xml
.
"},{"location":"migration/wsc30/package/#example-code","title":"Example Code","text":"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<data xmlns=\"http://www.woltlab.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd\">\n<import>\n<page identifier=\"com.example.foo.Bar\">\n<!-- ... -->\n<excludeFromLandingPage>1</excludeFromLandingPage>\n<!-- ... -->\n</page>\n</import>\n</data>\n
"},{"location":"migration/wsc30/package/#new-package-installation-plugin-for-media-providers","title":"New Package Installation Plugin for Media Providers","text":"Please refer to the documentation of the mediaProvider.xml
to learn more.
"},{"location":"migration/wsc30/package/#limited-forward-compatibility-for-plugins","title":"Limited Forward-Compatibility for Plugins","text":"Please refer to the documentation of the <compatibility>
tag in the package.xml
.
"},{"location":"migration/wsc30/php/","title":"Migrating from WSC 3.0 - PHP","text":""},{"location":"migration/wsc30/php/#approval-system-for-comments","title":"Approval-System for Comments","text":"Comments can now be set to require approval by a moderator before being published. This feature is disabled by default if you do not provide a permission in the manager class, enabling it requires a new permission that has to be provided in a special property of your manage implementation.
files/lib/system/comment/manager/ExampleCommentManager.class.php<?php\nclass ExampleCommentManager extends AbstractCommentManager {\n protected $permissionAddWithoutModeration = 'foo.bar.example.canAddCommentWithoutModeration';\n}\n
"},{"location":"migration/wsc30/php/#raw-html-in-user-activity-events","title":"Raw HTML in User Activity Events","text":"User activity events were previously encapsulated inside <div class=\"htmlContent\">\u2026</div>
, with impacts on native elements such as lists. You can now disable the class usage by defining your event as raw HTML:
files/lib/system/user/activity/event/ExampleUserActivityEvent.class.php<?php\nclass ExampleUserActivityEvent {\n // enables raw HTML for output, defaults to `false`\n protected $isRawHtml = true;\n}\n
"},{"location":"migration/wsc30/php/#permission-to-view-likes-of-an-object","title":"Permission to View Likes of an Object","text":"Being able to view the like summary of an object was restricted to users that were able to like the object itself. This creates situations where the object type in general is likable, but the particular object cannot be liked by the current users, while also denying them to view the like summary (but it gets partly exposed through the footer note/summary!).
Implement the interface \\wcf\\data\\like\\IRestrictedLikeObjectTypeProvider
in your object provider to add support for this new permission check.
files/lib/data/example/LikeableExampleProvider.class.php<?php\nclass LikeableExampleProvider extends ExampleProvider implements IRestrictedLikeObjectTypeProvider, IViewableLikeProvider {\n public function canViewLikes(ILikeObject $object) {\n // perform your permission checks here\n return true;\n }\n}\n
"},{"location":"migration/wsc30/php/#developer-tools-sync-feature","title":"Developer Tools: Sync Feature","text":"The synchronization feature of the newly added developer tools works by invoking a package installation plugin (PIP) outside of a regular installation, while simulating the basic environment that is already exposed by the API.
However, not all PIPs qualify for this kind of execution, especially because it could be invoked multiple times in a row by the user. This is solved by requiring a special marking for PIPs that have no side-effects (= idempotent) when invoked any amount of times with the same arguments.
There's another feature that allows all matching PIPs to be executed in a row using a single button click. In order to solve dependencies on other PIPs, any implementing PIP must also provide the method getSyncDependencies()
that returns the dependent PIPs in an arbitrary order.
files/lib/data/package/plugin/ExamplePackageInstallationPlugin.class.php<?php\nclass ExamplePackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {\n public static function getSyncDependencies() {\n // provide a list of dependent PIPs in arbitrary order\n return [];\n }\n}\n
"},{"location":"migration/wsc30/php/#media-providers","title":"Media Providers","text":"Media providers were added through regular SQL queries in earlier versions, but this is neither convenient, nor did it offer a reliable method to update an existing provider. WoltLab Suite 3.1 adds a new mediaProvider
-PIP that also offers a className
parameter to off-load the result evaluation and HTML generation.
"},{"location":"migration/wsc30/php/#example-implementation","title":"Example Implementation","text":"mediaProvider.xml <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<data xmlns=\"http://www.woltlab.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.woltlab.com http://www.woltlab.com/XSD/tornado/mediaProvider.xsd\">\n<import>\n<provider name=\"example\">\n<title>Example Provider</title>\n<regex>https?://example.com/watch?v=(?P<ID>[a-zA-Z0-9])</regex>\n<className>wcf\\system\\bbcode\\media\\provider\\ExampleBBCodeMediaProvider</className>\n</provider>\n</import>\n</data>\n
"},{"location":"migration/wsc30/php/#php-callback","title":"PHP Callback","text":"The full match is provided for $url
, while any capture groups from the regular expression are assigned to $matches
.
<?php\nclass ExampleBBCodeMediaProvider implements IBBCodeMediaProvider {\n public function parse($url, array $matches = []) {\n return \"final HTML output\";\n }\n}\n
"},{"location":"migration/wsc30/php/#re-evaluate-html-messages","title":"Re-Evaluate HTML Messages","text":"You need to manually set the disallowed bbcodes in order to avoid unintentional bbcode evaluation. Please see this commit for a reference implementation inside worker processes.
The HtmlInputProcessor only supported two ways to handle an existing HTML message:
- Load the string through
process()
and run it through the validation and sanitation process, both of them are rather expensive operations and do not qualify for rebuild data workers. - Detect embedded content using
processEmbeddedContent()
which bypasses most tasks that are carried out by process()
which aren't required, but does not allow a modification of the message.
The newly added method reprocess($message, $objectType, $objectID)
solves this short-coming by offering a full bbcode and text re-evaluation while bypassing any input filters, assuming that the input HTML was already filtered previously.
"},{"location":"migration/wsc30/php/#example-usage","title":"Example Usage","text":"<?php\n// rebuild data workers tend to contain code similar to this:\nforeach ($this->objectList as $message) {\n // ...\n if (!$message->enableHtml) {\n // ...\n }\n else {\n // OLD:\n $this->getHtmlInputProcessor()->processEmbeddedContent($message->message, 'com.example.foo.message', $message->messageID);\n\n // REPLACE WITH:\n $this->getHtmlInputProcessor()->reprocess($message->message, 'com.example.foo.message', $message->messageID);\n $data['message'] = $this->getHtmlInputProcessor()->getHtml();\n }\n // ...\n}\n
"},{"location":"migration/wsc30/templates/","title":"Migrating from WSC 3.0 - Templates","text":""},{"location":"migration/wsc30/templates/#comment-system-overhaul","title":"Comment-System Overhaul","text":"Unfortunately, there has been a breaking change related to the creation of comments. You need to apply the changes below before being able to create new comments.
"},{"location":"migration/wsc30/templates/#adding-comments","title":"Adding Comments","text":"Existing implementations need to include a new template right before including the generic commentList
template.
<ul id=\"exampleCommentList\" class=\"commentList containerList\" data-...>\n {include file='commentListAddComment' wysiwygSelector='exampleCommentListAddComment'}\n {include file='commentList'}\n</ul>\n
"},{"location":"migration/wsc30/templates/#redesigned-acp-user-list","title":"Redesigned ACP User List","text":"Custom interaction buttons were previously added through the template event rowButtons
and were merely a link-like element with an icon inside. This is still valid and supported for backwards-compatibility, but it is recommend to adapt to the new drop-down-style options using the new template event dropdownItems
.
<!-- button for usage with the `rowButtons` event -->\n<span class=\"icon icon16 fa-list jsTooltip\" title=\"Button Title\"></span>\n\n<!-- new drop-down item for the `dropdownItems` event -->\n<li><a href=\"#\" class=\"jsMyButton\">Button Title</a></li>\n
"},{"location":"migration/wsc30/templates/#sidebar-toogle-buttons-on-mobile-device","title":"Sidebar Toogle-Buttons on Mobile Device","text":"You cannot override the button label for sidebars containing navigation menus.
The page sidebars are automatically collapsed and presented as one or, when both sidebar are present, two condensed buttons. They use generic sidebar-related labels when open or closed, with the exception of embedded menus which will change the button label to read \"Show/Hide Navigation\".
You can provide a custom label before including the sidebars by assigning the new labels to a few special variables:
{assign var='__sidebarLeftShow' value='Show Left Sidebar'}\n{assign var='__sidebarLeftHide' value='Hide Left Sidebar'}\n{assign var='__sidebarRightShow' value='Show Right Sidebar'}\n{assign var='__sidebarRightHide' value='Hide Right Sidebar'}\n
"},{"location":"migration/wsc31/form-builder/","title":"Migrating from WSC 3.1 - Form Builder","text":""},{"location":"migration/wsc31/form-builder/#example-two-text-form-fields","title":"Example: Two Text Form Fields","text":"As the first example, the pre-WoltLab Suite Core 5.2 versions of the forms to add and edit persons from the first part of the tutorial series will be updated to the new form builder API. This form is the perfect first examples as it is very simple with only two text fields whose only restriction is that they have to be filled out and that their values may not be longer than 255 characters each.
As a reminder, here are the two relevant PHP files and the relevant template file:
files/lib/acp/form/PersonAddForm.class.php <?php\nnamespace wcf\\acp\\form;\nuse wcf\\data\\person\\PersonAction;\nuse wcf\\form\\AbstractForm;\nuse wcf\\system\\exception\\UserInputException;\nuse wcf\\system\\WCF;\nuse wcf\\util\\StringUtil;\n\n/**\n * Shows the form to create a new person.\n * \n * @author Matthias Schmidt\n * @copyright 2001-2019 WoltLab GmbH\n * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>\n * @package WoltLabSuite\\Core\\Acp\\Form\n */\nclass PersonAddForm extends AbstractForm {\n /**\n * @inheritDoc\n */\n public $activeMenuItem = 'wcf.acp.menu.link.person.add';\n\n /**\n * first name of the person\n * @var string\n */\n public $firstName = '';\n\n /**\n * last name of the person\n * @var string\n */\n public $lastName = '';\n\n /**\n * @inheritDoc\n */\n public $neededPermissions = ['admin.content.canManagePeople'];\n\n /**\n * @inheritDoc\n */\n public function assignVariables() {\n parent::assignVariables();\n\n WCF::getTPL()->assign([\n 'action' => 'add',\n 'firstName' => $this->firstName,\n 'lastName' => $this->lastName\n ]);\n }\n\n /**\n * @inheritDoc\n */\n public function readFormParameters() {\n parent::readFormParameters();\n\n if (isset($_POST['firstName'])) $this->firstName = StringUtil::trim($_POST['firstName']);\n if (isset($_POST['lastName'])) $this->lastName = StringUtil::trim($_POST['lastName']);\n }\n\n /**\n * @inheritDoc\n */\n public function save() {\n parent::save();\n\n $this->objectAction = new PersonAction([], 'create', [\n 'data' => array_merge($this->additionalFields, [\n 'firstName' => $this->firstName,\n 'lastName' => $this->lastName\n ])\n ]);\n $this->objectAction->executeAction();\n\n $this->saved();\n\n // reset values\n $this->firstName = '';\n $this->lastName = '';\n\n // show success message\n WCF::getTPL()->assign('success', true);\n }\n\n /**\n * @inheritDoc\n */\n public function validate() {\n parent::validate();\n\n // validate first name\n if (empty($this->firstName)) {\n throw new UserInputException('firstName');\n }\n if (mb_strlen($this->firstName) > 255) {\n throw new UserInputException('firstName', 'tooLong');\n }\n\n // validate last name\n if (empty($this->lastName)) {\n throw new UserInputException('lastName');\n }\n if (mb_strlen($this->lastName) > 255) {\n throw new UserInputException('lastName', 'tooLong');\n }\n }\n}\n
files/lib/acp/form/PersonEditForm.class.php <?php\nnamespace wcf\\acp\\form;\nuse wcf\\data\\person\\Person;\nuse wcf\\data\\person\\PersonAction;\nuse wcf\\form\\AbstractForm;\nuse wcf\\system\\exception\\IllegalLinkException;\nuse wcf\\system\\WCF;\n\n/**\n * Shows the form to edit an existing person.\n * \n * @author Matthias Schmidt\n * @copyright 2001-2019 WoltLab GmbH\n * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>\n * @package WoltLabSuite\\Core\\Acp\\Form\n */\nclass PersonEditForm extends PersonAddForm {\n /**\n * @inheritDoc\n */\n public $activeMenuItem = 'wcf.acp.menu.link.person';\n\n /**\n * edited person object\n * @var Person\n */\n public $person = null;\n\n /**\n * id of the edited person\n * @var integer\n */\n public $personID = 0;\n\n /**\n * @inheritDoc\n */\n public function assignVariables() {\n parent::assignVariables();\n\n WCF::getTPL()->assign([\n 'action' => 'edit',\n 'person' => $this->person\n ]);\n }\n\n /**\n * @inheritDoc\n */\n public function readData() {\n parent::readData();\n\n if (empty($_POST)) {\n $this->firstName = $this->person->firstName;\n $this->lastName = $this->person->lastName;\n }\n }\n\n /**\n * @inheritDoc\n */\n public function readParameters() {\n parent::readParameters();\n\n if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);\n $this->person = new Person($this->personID);\n if (!$this->person->personID) {\n throw new IllegalLinkException();\n }\n }\n\n /**\n * @inheritDoc\n */\n public function save() {\n AbstractForm::save();\n\n $this->objectAction = new PersonAction([$this->person], 'update', [\n 'data' => array_merge($this->additionalFields, [\n 'firstName' => $this->firstName,\n 'lastName' => $this->lastName\n ])\n ]);\n $this->objectAction->executeAction();\n\n $this->saved();\n\n // show success message\n WCF::getTPL()->assign('success', true);\n }\n}\n
acptemplates/personAdd.tpl {include file='header' pageTitle='wcf.acp.person.'|concat:$action}\n\n<header class=\"contentHeader\">\n <div class=\"contentHeaderTitle\">\n <h1 class=\"contentTitle\">{lang}wcf.acp.person.{$action}{/lang}</h1>\n </div>\n\n <nav class=\"contentHeaderNavigation\">\n <ul>\n <li><a href=\"{link controller='PersonList'}{/link}\" class=\"button\"><span class=\"icon icon16 fa-list\"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>\n\n{event name='contentHeaderNavigation'}\n </ul>\n </nav>\n</header>\n\n{include file='formError'}\n\n{if $success|isset}\n <p class=\"success\">{lang}wcf.global.success.{$action}{/lang}</p>\n{/if}\n\n<form method=\"post\" action=\"{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object=$person}{/link}{/if}\">\n <div class=\"section\">\n <dl{if $errorField == 'firstName'} class=\"formError\"{/if}>\n <dt><label for=\"firstName\">{lang}wcf.person.firstName{/lang}</label></dt>\n <dd>\n <input type=\"text\" id=\"firstName\" name=\"firstName\" value=\"{$firstName}\" required autofocus maxlength=\"255\" class=\"long\">\n{if $errorField == 'firstName'}\n <small class=\"innerError\">\n{if $errorType == 'empty'}\n{lang}wcf.global.form.error.empty{/lang}\n{else}\n{lang}wcf.acp.person.firstName.error.{$errorType}{/lang}\n{/if}\n </small>\n{/if}\n </dd>\n </dl>\n\n <dl{if $errorField == 'lastName'} class=\"formError\"{/if}>\n <dt><label for=\"lastName\">{lang}wcf.person.lastName{/lang}</label></dt>\n <dd>\n <input type=\"text\" id=\"lastName\" name=\"lastName\" value=\"{$lastName}\" required maxlength=\"255\" class=\"long\">\n{if $errorField == 'lastName'}\n <small class=\"innerError\">\n{if $errorType == 'empty'}\n{lang}wcf.global.form.error.empty{/lang}\n{else}\n{lang}wcf.acp.person.lastName.error.{$errorType}{/lang}\n{/if}\n </small>\n{/if}\n </dd>\n </dl>\n\n{event name='dataFields'}\n </div>\n\n{event name='sections'}\n\n <div class=\"formSubmit\">\n <input type=\"submit\" value=\"{lang}wcf.global.button.submit{/lang}\" accesskey=\"s\">\n{@SECURITY_TOKEN_INPUT_TAG}\n </div>\n</form>\n\n{include file='footer'}\n
Updating the template is easy as the complete form is replace by a single line of code:
acptemplates/personAdd.tpl {include file='header' pageTitle='wcf.acp.person.'|concat:$action}\n\n<header class=\"contentHeader\">\n <div class=\"contentHeaderTitle\">\n <h1 class=\"contentTitle\">{lang}wcf.acp.person.{$action}{/lang}</h1>\n </div>\n\n <nav class=\"contentHeaderNavigation\">\n <ul>\n <li><a href=\"{link controller='PersonList'}{/link}\" class=\"button\"><span class=\"icon icon16 fa-list\"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>\n\n{event name='contentHeaderNavigation'}\n </ul>\n </nav>\n</header>\n\n{@$form->getHtml()}\n\n{include file='footer'}\n
PersonEditForm
also becomes much simpler: only the edited Person
object must be read:
files/lib/acp/form/PersonEditForm.class.php <?php\nnamespace wcf\\acp\\form;\nuse wcf\\data\\person\\Person;\nuse wcf\\system\\exception\\IllegalLinkException;\n\n/**\n * Shows the form to edit an existing person.\n * \n * @author Matthias Schmidt\n * @copyright 2001-2019 WoltLab GmbH\n * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>\n * @package WoltLabSuite\\Core\\Acp\\Form\n */\nclass PersonEditForm extends PersonAddForm {\n /**\n * @inheritDoc\n */\n public $activeMenuItem = 'wcf.acp.menu.link.person';\n\n /**\n * @inheritDoc\n */\n public function readParameters() {\n parent::readParameters();\n\n if (isset($_REQUEST['id'])) {\n $this->formObject = new Person(intval($_REQUEST['id']));\n if (!$this->formObject->personID) {\n throw new IllegalLinkException();\n }\n }\n }\n}\n
Most of the work is done in PersonAddForm
:
files/lib/acp/form/PersonAddForm.class.php <?php\nnamespace wcf\\acp\\form;\nuse wcf\\data\\person\\PersonAction;\nuse wcf\\form\\AbstractFormBuilderForm;\nuse wcf\\system\\form\\builder\\container\\FormContainer;\nuse wcf\\system\\form\\builder\\field\\TextFormField;\n\n/**\n * Shows the form to create a new person.\n * \n * @author Matthias Schmidt\n * @copyright 2001-2019 WoltLab GmbH\n * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>\n * @package WoltLabSuite\\Core\\Acp\\Form\n */\nclass PersonAddForm extends AbstractFormBuilderForm {\n /**\n * @inheritDoc\n */\n public $activeMenuItem = 'wcf.acp.menu.link.person.add';\n\n /**\n * @inheritDoc\n */\n public $formAction = 'create';\n\n /**\n * @inheritDoc\n */\n public $neededPermissions = ['admin.content.canManagePeople'];\n\n /**\n * @inheritDoc\n */\n public $objectActionClass = PersonAction::class;\n\n /**\n * @inheritDoc\n */\n protected function createForm() {\n parent::createForm();\n\n $dataContainer = FormContainer::create('data')\n ->appendChildren([\n TextFormField::create('firstName')\n ->label('wcf.person.firstName')\n ->required()\n ->maximumLength(255),\n\n TextFormField::create('lastName')\n ->label('wcf.person.lastName')\n ->required()\n ->maximumLength(255)\n ]);\n\n $this->form->appendChild($dataContainer);\n }\n}\n
But, as you can see, the number of lines almost decreased by half. All changes are due to extending AbstractFormBuilderForm
:
$formAction
is added and set to create
as the form is used to create a new person. In the edit form, $formAction
has not to be set explicitly as it is done automatically if a $formObject
is set. $objectActionClass
is set to PersonAction::class
and is the class name of the used AbstractForm::$objectAction
object to create and update the Person
object. AbstractFormBuilderForm::createForm()
is overridden and the form contents are added: a form container representing the div.section
element from the old version and the two form fields with the same ids and labels as before. The contents of the old validate()
method is put into two method calls: required()
to ensure that the form is filled out and maximumLength(255)
to ensure that the names are not longer than 255 characters.
"},{"location":"migration/wsc31/like/","title":"Migrating from WSC 3.1 - Like System","text":""},{"location":"migration/wsc31/like/#introduction","title":"Introduction","text":"With version 5.2 of WoltLab Suite Core the like system was completely replaced by the new reactions system. This makes it necessary to make some adjustments to existing code so that your plugin integrates completely into the new system. However, we have kept these adjustments as small as possible so that it is possible to use the reaction system with slight restrictions even without adjustments.
"},{"location":"migration/wsc31/like/#limitations-if-no-adjustments-are-made-to-the-existing-code","title":"Limitations if no adjustments are made to the existing code","text":"If no adjustments are made to the existing code, the following functions are not available: * Notifications about reactions/likes * Recent Activity Events for reactions/likes
"},{"location":"migration/wsc31/like/#migration","title":"Migration","text":""},{"location":"migration/wsc31/like/#notifications","title":"Notifications","text":""},{"location":"migration/wsc31/like/#mark-notification-as-compatible","title":"Mark notification as compatible","text":"Since there are no more likes with the new version, it makes no sense to send notifications about it. Instead of notifications about likes, notifications about reactions are now sent. However, this only changes the notification text and not the notification itself. To update the notification, we first add the interface \\wcf\\data\\reaction\\object\\IReactionObject
to the \\wcf\\data\\like\\object\\ILikeObject
object (e.g. in WoltLab Suite Forum we added the interface to the class \\wbb\\data\\post\\LikeablePost
). After that the object is marked as \"compatible with WoltLab Suite Core 5.2\" and notifications about reactions are sent again.
"},{"location":"migration/wsc31/like/#language-variables","title":"Language Variables","text":"Next, to display all reactions for the current notification in the notification text, we include the trait \\wcf\\system\\user\\notification\\event\\TReactionUserNotificationEvent
in the user notification event class (typically named like *LikeUserNotificationEvent
). These trait provides a new function that reads out and groups the reactions. The result of this function must now only be passed to the language variable. The name \"reactions\" is typically used as the variable name for the language variable.
As a final step, we only need to change the language variables themselves. To ensure a consistent usability, the same formulations should be used as in the WoltLab Suite Core.
"},{"location":"migration/wsc31/like/#english","title":"English","text":"{prefix}.like.title
Reaction to a {objectName}\n