-
Posts
24,413 -
Joined
-
Last visited
-
Days Won
84
Content Type
Downloads
Release Notes
IPS4 Guides
IPS4 Developer Documentation
Invision Community Blog
Development Blog
Deprecation Tracker
Providers Directory
Projects
Release Notes v5
Invision Community 5 Bug Tracker
Forums
Events
Store
Gallery
Everything posted by Rikki
-
Description The hovercard widget can be used to display a popup with remote content, which is attached to a trigger element. Example @todo Usage The hovercard widget is initialized by including the widget key attribute on a link: <a href='...' data-ipsHover>Hover on me</a> The URL that the link points to is also the URL that is called to fetch the hovercard content, by default. Options target (String; optional) If a URL other than the link's href should be used to load the hovercard content, it can be specified in this option. content (Mixed; optional) Sets the content of the hovercard. If a selector is provided, the contents of the matching element will be inserted into to hovercard. Alternatively, a string can be supplied which will be inserted into the hovercard. Using this option means remote content won't be loaded by the hovercard. timeout (Number; optional; default 0.75) The number of seconds between the user hovering on the element and the hovercard beginning to load (and conversely, hiding when the mouse leaves the element). attach (Selector; optional) The element that the hovercard will be attached to. If not specified, the element that the widget is initialized on will be used. width (Number; optional; default 300) The width in pixels at which the hovercard will display. onClick (Boolean; optional; default false) If true, the hovercard will be activated only when the user clicks on the element. By default, the hovercard will show on hover.
-
Description The flashMsg widget can be used to display a 'flash message' to the user - a short notification that appears on the screen for a few seconds, before disappearing. It is commonly used for 'success' messages when an action occurs. Example @todo Usage A flash message can be created with the data API, as follows: <span data-ipsFlashMsg data-ipsFlashMsg-text='My message'> The element used with this method is not important; the widget will not interact with the element in any way. However, in most cases, flash messages will be created on the fly in response to user actions, so the more common use will be calling them programatically. This can be done in javascript modules with the following code: ips.ui.flashMsg.show('My message'); Flash messages can be automatically created in two additional ways: Cookies - set a cookie with the key flmsg. The cookie will be deleted after the message displays. URL - pass the parameter flmsg in the page URL, with the value being the message to show. The value should be URL encoded. Options text (String; required) The text to display in the flash message. A sentence or two maximum is recommended. extraClasses (String; optional; default 'ipsPad') A space-separated string of extra CSS classnames that will be applied to the flash message box. timeout (Number; optional; default 2) The number of seconds for which this flash will be shown before hiding itself. Events closeFlashMsg.flashMsg Hides the currently-shown flash message. If any messages are queued, the next will be shown automatically.
-
Description The dialog widget displays popup windows within the page, typically loading content from a remote source. Usage A dialog is defined simply by including the widget key as an attribute on a trigger element. It is recommended that the trigger element be a link or button, so that if the user has javascript disabled their browser can take them to a full-page version of the dialog contents. <a href='...' data-ipsDialog data-ipsDialog-url='...'>Launch dialog</a> Dialogs offer special functionality for forms contained with the dialog, enabling them to be automatically validated and submitted via AJAX when used with the form helper from the IPS4 PHP framework. See the remoteVerify and remoteSubmit options below. Obtaining a dialog reference If you need to control a dialog programmatically, you can do so by first obtaining the reference to the dialog. To do so, call the getObj method, passing in the element on which the dialog was created as the parameter: // HTML <div id='elementWithDialog' data-ipsDialog> ... </div> // Javascript var dialog = ips.ui.dialog.getObj( $('#elementWithDialog') ); The returned reference to the dialog instance can be used to call the methods shown below. Creating a dialog programmatically Dialogs can be created programmatically by controllers instead of being created on particular elements. To achieve this, call the create method: var dialog = ips.ui.dialog.create( object options ); The options parameter should be an object containing keys for the options below. This method returns a reference to the dialog instance on which you can call methods to control the dialog, for example: dialog.show(); dialog.hide(); Instance methods show( boolean initOnly ) Initializes and shows the dialog. If initOnly is true, the dialog is initialized but not immediately shown. hide() Hides the dialog. remove( boolean hideFirst ) Removes the dialog from the DOM. If hideFirst is true, the dialog will call hide() and remove itself after the animation is completed. setLoading( boolean loading ) If loading is true, indicates to the user that something is being loaded (i.e. shows a spinner in the dialog). If loading is false, removes the loading state. Note: this method doesn't empty the dialog content first. Call instance.updateContent('') manually if desired. updateContent( string newContent ) Updates the contents of the dialog with the provided newContent. New controllers/widgets are automatically initialized after updating. Options url (String; optional) If provided, the dialog contents will be loaded by calling this URL when the dialog is launched. content (Selector; optional) If the dialog content already exists in the DOM, this option should be a selector that will locate the wrapper element for the content. modal (Boolean; optional; default true) If true, the page background will fade, preventing the user from interacting with it until the dialog is dismissed. title (String; optional) Sets the title shown in the dialog window. If not set, no title bar will display. size (String; optional) If provided, will set a special class on the dialog designed to change its size. Currently, narrow, medium, wide and fullscreen are supported as values for this option. If not provided, the default dialog width will be used (defined in CSS). close (Boolean; optional; default true) Determines whether the dialog should be built with a close button. fixed (Boolean; optional; default false) Determines whether the dialog is fixed. A fixed dialog is anchored to the viewport, not the page. Its height will also be fixed, with the content scrolling inside if necessary. If this is false, the dialog will scroll with the page, and it will expand to fit all the content being shown, regardless of length. remoteVerify (Boolean; optional; default true) When the dialog contains a form built using the IPS4 PHP framework form helper, this option instructs the dialog to automatically validate the form values and present any errors to the user. remoteSubmit (Boolean; optional; default false) When the dialog contains a form built using the IPS4 PHP framework form helper, this option instructs the dialog to submit the form values via AJAX. If remoteVerify is also true, two separate AJAX requests are fired; first, the form is validated, and if successful the form is submitted with a second request. If remoteSubmit is true, after a successful submit the dialog is closed automatically. If the flashMessage option is provided, it will be shown at this point. By default, remoteSubmit is false, which means forms submit via a traditional page reload. callback (Function; optional) Specifies a callback function that will be executed after the dialog has loaded remote content. Note: this option cannot be provided via the data API, and is therefore only available when interacting with ips.ui.dialog directly. forceReload (Boolean; optional; default false) By default, once a dialog has loaded its content, it will not do so again even if the dialog is relaunched. By setting this option to true, the dialog will call the url every time it is opened. This option only takes effect when the url option is used. Events emitted by ips.ui.dialog openDialog Triggered when the dialog is shown. Event data: elemID ID of the element that triggered the dialog dialogID ID of the dialog element dialog Reference to the dialog element contentLoaded Boolean indicating whether the dialog content has been loaded (may be false if remote content is used) hideDialog Triggered when the dialog is hidden. Event data: elemID ID of the element that triggered the dialog dialogID ID of the dialog element dialog Reference to the dialog element dialogContentLoaded Triggered after remote content has been loaded and inserted into the dialog. Event data: elemID ID of the element that triggered the dialog dialogID ID of the dialog element dialog Reference to the dialog element contentLoaded Boolean indicating whether the dialog content has been loaded (always true here) Events to which ips.ui.dialog responds closeDialog Instructs the dialog to close. Event data: dialogID (required) The ID of the dialog to close (the event is ignored if the ID does not match the current dialog)
-
Description The Chart widget takes a specially-formatted table of data, and uses Google Charts to render it as an attractive chart. The Chart widget is designed for use soley with the output of the \IPS\Helpers\Chart class and is documented here for completeness only. Consult the documentation for the PHP class for more information. Options type (String; required) Specifies the type of chart to render, e.g. LineChart extraOptions (Object; optional) Object of extra options passed through to the Google Charts library. Events chartInitialized Triggered after the chart has been initialized.
-
Description The Captcha widget is designed to allow captcha to be used dynamically, including situations where it is initialized after the page has loaded (e.g. in popups). The necessary javascript libraries from providers are loaded on the fly. The widget presently supports reCaptcha and KeyCaptcha. Usage The widget is initialized as follows: <div data-ipsCaptcha data-ipsCaptcha-service='recaptcha' data-ipsCaptcha-key='...'> </div> Options service (String; required) Specifies the captcha service to initialize. Either recaptcha or keycaptcha. key (recaptcha only) (String; required) Specifies the reCaptcha API key to use. Not used for KeyCaptcha. lang (recaptcha only) (String; optional; default 'en-US') Specifies the language to load for reCaptcha. Not used for KeyCaptcha. theme (recaptcha only) (String; optional; default 'white') Specifies the theme to load for reCaptcha. Not used for KeyCaptcha. Events The Captcha widget does not emit any events.
-
Description The autocomplete widget adds two groups of functionality to a text input. Firstly, it adds support for suggestions based on what the user is typing, and secondly, it adds support for tokenization, allowing the user to enter multiple distinct values into one field. This functionality is useful for entering tags, for example. Example @todo Usage The autocomplete widget should be initialized on a text input with the correct widget key attribute: <input type='text' placeholder='Enter text to search' name='field_name' data-ipsAutocomplete data-ipsAutocomplete-dataSource='...'> Obtaining an autoComplete reference If you need to control an autoComplete programmatically, you can do so by first obtaining a reference to the instance. To do so, call the getObj method, passing in the element on which the dialog was created as the parameter: // HTML <input type='text' id='inputWithAutocomplete' data-ipsAutocomplete> // Javascript var autocomplete = ips.ui.autocomplete.getObj( $('#inputWithAutocomplete') ); You can then call the methods below on the returned reference. Methods addToken( string value ) Returns boolean; true on success. Adds a token to the autocomplete with the provided value. getTokens() Returns array containing values. Fetches all of the token values currently present in the autocomplete. removeToken( element token ) Returns void. Removes the given token element from the autocomplete. Note that this method expects the DOM/jQuery token element, not the value. removeAll() Returns void. Removes all tokens from the autocomplete. Options dataSource (Mixed; optional) Specifies the data source that the autocomplete widget should use for suggestions. Possible values: URL - when a URL is supplied, the widget will fetch results from this address with the current value as the parameter q, expecting a JSON object in response Selector - which locates a datalist element on the page, the options of which will provides the results None - initializes the autocomplete widget with no dataSource, meaning suggestions will not be offered to the user maxItems (Number; optional) Specifies the maximum number of tokens the user can enter into the field. When the maximum is reached, no further tokens can be added. minLength (Number; optional) Specifies the minimum length a value must be, when freeChoice is true. maxLength (Number; optional) Specifies the maximum length a value can be, when freeChoice is true. unique (Boolean; optional; default false) Determines whether tokens entered must be unique. If true, a duplicate will not be permitted and a tooltip will inform the user why. In addition, when a token is used it is automatically removed from the list of suggestions offered for further tokens. freeChoice (Boolean; optional; default true) If true, the user will be able to enter their own values. If false, they will only be able to select values from the suggestion list. e.g. @todo forceLower (Boolean; optional; default false) Forces all tokens entered to be lowercase. resultsElem (Selector; optional) By default, when suggestions are presented they will appear in a floating list attached to the text field. If a selector is provided in this option, the results will instead be inserted into the element it locates. queryParam (String; optional; default 'q') If a URL is used for dataSource, this parameter provides the parameter name used for the lookup e.g. index.php?q=<term>. addTokenText (Template key; optional; default 'core.autocomplete.addToken') If freeChoice is false, this option specifies the template that will be used to build the link that the user will click to display the suggestions list. fieldTemplate (Template key; optional; default 'core.autocomplete.field') The key to the template that will be used to build the widget wrapper (into which the original text field is inserted). resultsTemplate (Template key; optional; default 'core.autocomplete.resultItem') The key to the template that will be used to build an item within the suggestions list. For example, in a list for selecting colors, the template might show a swatch alongside the name. tokenTemplate (Template key; optional; default 'core.autocomplete.token') The key to the template that will be used to render an individual token when it is created in the field. Events Events are emitted on the original text field. autoCompleteReady Triggered when the autocomplete widget is ready for interaction (i.e. the wrapper is built and existing values have been constructed and inserted). Event data: elemID ID of the element the widget was initialized on elem Reference to the above element currentValues Array of current values in the widget (in this case, values that already existed when the widget was initialized) tokenAdded Triggered when a token is created in the text field, either by selecting a value or hitting the delimiter character (comma by default) Event data: token The value of the token that has been added tokenList Array of all current token values totalTokens Count of the number of tokens added html The HTML representing this item in the results list tokenDeleted Triggered when a token is deleted Event data: token The value of the token that has been deleted tokenList Array of all current token values totalTokens Count of the number of tokens added
-
Description The autoCheck widget makes it simple to select checkboxes of a certain type on a page. This is useful for moderation tools, for example - simply by specifying a filter, topics that match that filter can be selected. Note: The autoCheck widget works in conjunction with the ips.ui.menu widget. It will not work on other kinds of elements. Example @todo Usage The widget itself is initialized on the menu trigger element: <!-- The trigger --> <a href='#elMyMenu_menu' id='elMyMenu' data-ipsMenu data-ipsAutoCheck data-ipsAutoCheck-context='#elContext'> Open Menu </a> Each menu option then receives the special data attribute data-ipsMenuValue (as described in the menu widget documentation) which contains the filter type that the option will check: <li class='ipsMenu_item' data-ipsMenuValue='unapproved'> <a href='...'>Has Unapproved</a> </li> And finally, each checkbox within the context element (see options below) can receive another special data attribute data-state, to indicate which filters it matches: <input type='checkbox' data-state='unread unapproved'> Multiple filter values are simply separated by a space. Options context (Selector; required) A CSS selector which indicates the wrapper element that contains all of the checkboxes which will be checked for filter matches. For example, if a Data List structure is used, this option might be set to the ID of the root list element. Events Events are emitted on the menu trigger element. autoChecked Triggered when the user selects an option that has a data-ipsMenuValue attribute. Event data: menu Reference to the menu trigger element currentFilter The filter that has been selected count The number of checkboxes that matched
-
Description The alert widget is designed to replace the built-in javascript alert, providing a more pleasant user experience as well as more flexible options and built in callbacks. Several other types of alert dialogs are supported too. Usage Unlike most UI widgets, the alert widget is instantiated programatically, rather than using the data API - typically as a response to a user action in a controller, for example. An alert is created as follows: ips.ui.alert.show( { type: 'alert', icon: 'warning', message: 'Alert message', subText: 'Message subtext', callbacks: { ok: function () { // Do something } } }); Each type of alert shows different buttons depending on the use; a callback can be assigned to each button shown for that type. Alert types Alert Shows a message, with a single button to dismiss the alert. Type: alert Available button callbacks: ok Confirm Confirm shows a message with OK and Cancel buttons. Type: confirm Available button callbacks: ok, cancel Prompt Shows a message with OK and Cancel buttons, and prompts the user to input a value. Both callbacks receive the entered value as the first parameter. Type: prompt Available button callbacks: ok, cancel Verify Verify shows a message with Yes and No buttons. Type: verify Available button callbacks: yes, no Events No events are emitted by the alert widget; since callbacks are supported, custom events can be triggered inside them to achieve desired functionality.
-
Initializing widgets UI widgets can now be created on elements without needing to write any code. This is done by specifying special attributes on the element using the HTML5 data attributes. Widgets each define their own behaviors - some wait for user interactions, others might immediately change the display when executed. Usage Each UI widget has a base attribute that identifies it, and causes the element to be passed to the widget module to be handled. All widget attributes have the prefix ips. <!-- If a widget called 'Widget' existed, we could initialize it on an element like this --> <div data-ipsWidget> ... </div> In this example, ipsWidget would be referred to as the widget key. The data- part of the HTML attribute is implied throughout the documentation for individual widgets. Each element can have multiple widgets specified; simply add each necessary widget prefix to the element as above. That said, bear in mind multiple widgets on a single element might cause conflicts if more than one responds to a user interaction. Test to be sure. Note: In many cases, the helpers available in the IPS4 PHP framework will generate all of the required HTML for you, including widget attributes, meaning you won't need to manually specify UI widgets in your HTML. For example, the autocomplete form input type adds the necessary attributes, so you don't need to handle it yourself. Consult the PHP documentation for more information. Passing options Most widgets have a number of options that control their behavior. To pass an option, use this format: <div data-ipsWidget data-ipsWidget-option1='Option value' data-ipsWidget-option2='Another option value' > ... </div> Each option is added as an additional attribute on the element, with the attribute key being data-ipsWidget- followed by the option name. By default, options are passed as strings because they come from the DOM. However, if the option contains only numbers, it will be cast explicitly as a number when passed into the widget. If the option contains the strings true or false, it will be cast as a boolean. In addition, options without a provided value are treated as the boolean true, meaning it is possible to specify true values like so: <div data-ipsWidget data-ipsWidget-showWide> Automatic initialization Widgets are automatically initialized when the data API is used as described. Further to that, when new content is loaded into the page (such as when an AJAX request fires), any widgets within the new content will also be automatically initialized.
-
What's a widget? UI widgets are modules that: Are instantiated on individual DOM nodes Provide some kind of UI functionality Benefit from being reusable Come with an automatic data API functionality A UI widget is created as a standard module, under the ips.ui namespace. Basic widget boilerplate ;( function($, _, undefined){ "use strict"; ips.createModule('ips.ui.widgetName', function(){ var respond = function (elem, options, e) { }; // Register this module as a widget to enable the data API ips.ui.registerWidget( 'widgetName', ips.ui.widgetName ); return { respond: respond }; }); }(jQuery, _)); In the example above, a module is created with the module path ips.ui.widgetName. UI widget modules must expose a response method, either the default respond method or by manually specifying a method when the widget is registered (see below). This is the method that is called when the widget is instantiated. Registering a UI widget A module is registered as a widget by calling the ips.ui.registerWidget method before the module returns its public methods object. ips.ui.registerWidget( string widgetKey, object handlerModule [, array acceptedOptions] [, object widgetOptions] [, function callback] ); The method takes the following parameters: widgetKey (required) The key that is used to reference this widget throughout the software. The key is prefixed with ips when used, to prevent naming collisions. The key forms both the data API attribute key, and the name of the jQuery plugin that can be used to instantiate the widget. Assuming the widget key is set to widget, the data API attribute key and jQuery plugin would be: data-ipsWidget // Data API attribute $('element').ipsWidget(); // jQuery plugin method handlerModule (required) A reference to the module that will respond when the widget is instantiated. In practice, this should be a self reference to the module that is being defined. acceptedOptions (optional) An array of strings defining the options that this widget will accept. When the widget is instantiated, if these options are defined their values will be passed back to the respond method. widgetOptions (optional) An object containing options for how this widget will be registered or instantiated. The available options are: lazyLoad By default, widgets are instantiated as soon as they are seen in the DOM. By setting this option to true the widget is not instantiated until the user interacts with the element. lazyEvent If the lazyLoad option is enabled, this option defines the event that will be watched for. When the event name set here is triggered, the widget will be initialized. makejQueryPlugin By default, widgets will have an associated jQuery plugin created to allow them to be instantiated programatically. This functionality can be disabled by setting this option to false. callback (optional) A widget must have a response method, which is called when it is initialized on an element. By default, a method with the name respond is looked for, but a different response function can be set by passing it as this parameter. Events The javascript framework in IPS4 relies heavily on events. Controllers in particular are completely decoupled and cannot directly communicate with one another. Instead, communication happens by triggering and receiving events. It is therefore important that your widget emits events when key actions happen. This allows controllers, other UI widgets, even third-party scripts to respond when something happens within your widget. As an example, the ips.ui.menu widget emits a number of events, including menuOpened, menuClosed and menuItemSelected. Each of these events also provides a number of data items relevant to the event, which can be used by event handlers listening for those events. It is this event interaction that facilitates dynamic pages within the IPS Community Suite, so it's important your widget also emits relevant events. When you emit an event, it is usually most appropriate to emit it on the element the widget is registered on. Emitting an event uses the standard jQuery syntax: $( elem ).trigger( 'myWidgetEvent', { color: 'red', size: 'large' } ); Widget initialization A widget is initialized either as soon as it is seen in the DOM, or, if the lazyLoad option is enabled, when the user interacts with the element the widget is created on. In both cases, the initialization process is the same. The internal widget manager will call the response method for the widget (respond by default, or whatever function is passed as the callback option), passing in the following parameters: function respond( element elem, array options, event ev ) elem A reference to the element on which the widget has been initialized options An array of options that have been specified on the element, and which were in the list of expected options when the widget was first registered. Any other options are ignored and won't be passed through. ev If the widget is initialized in response to a user interaction because lazyLoad is enabled, this parameter will contain the original event object. It will be undefined if the widget is being initialized on load. Example widget Let's take a trivial example, and assume we're creating a widget which hides an element: ;( function($, _, undefined){ "use strict"; ips.createModule('ips.ui.hideElem', function(){ var respond = function (elem, options, e) { if( options.animate ){ $( elem ).fadeOut(); } else { $( elem ).hide(); } $( elem ).trigger( 'hiddenElement' ); }; ips.ui.registerWidget( 'hideElem', ips.ui.hideElem, [ 'animate' ], { lazyLoad: true, lazyEvent: 'click' } ); return { respond: respond }; }); }(jQuery, _)); When we register the widget, we set up an animate option in the acceptedOptions parameter. In the widgetOptions parameter, we set lazyLoad to true, and the lazyEvent to click - meaning our widget won't be initialized until the user clicks on the element. In our respond method, we simply check if options.animate is true, fading out if so and hiding if not. This example widget could be initialized on an element like so: <button data-ipsHideElem data-ipsHideElem-animate='true'>Click to hide me</button>
-
Overview Controllers are special modules that handle specific functionality on specific pages. They are not necessarily reusable in different contexts, although some may be. A controller is initialized on an individual element, and that element becomes the controller's scope. A controller responds to user events (such as click) and events triggered by UI widgets or sub-controllers, and manipulates its scope element accordingly. The scope of a controllers functionality is entirely flexible. A controller might be initialized on a very small fragment of the page and perform just one task, or it might be initialized on a main wrapper and handle functionality that applies to the whole page. A controller can be initialized on an element even if it's a descendent of another element with a controller; this is a common pattern whereby the child controller can emit events that the parent controller can respond to. Generally, keep a controller narrowly focused on one aspect of the page. If a controller ends up handing distinct and unrelated functionality, split it into smaller controllers. Creating a controller A controller is registered in a slightly different way to standard modules; instead, an object literal is passed to the method ips.controller.register: ;( function($, _, undefined){ "use strict"; ips.controller.register('type.controllerID', { initialize: function () { // Controller events are initialized here } }); }(jQuery, _)); A controller, at the very least, must define an initialize method, which is called when the controller is initialized on an element. The initialize method should set up events that are handled by this controller only; if other initialization tasks are necessary, it is recommended that you define a setup method within the controller, and call that at the start or end of the initialize method as appropriate. Within the controller definition, this refers to the controller instance. That means controller methods can be called internally with this.someMethod(). A reference to the element that the controller is initialized on (its scope) is available with the scope property: // An example which hides the element this.scope.hide(); Method and property names For clarity, it is recommended that event handler method names (i.e. the handlers to events you set up in initialize) are not prefixed with an underscore, while other methods and properties of the controller are. This helps create a clear distinction between core functionality of the controller (the event handlers) and supporting methods. Handling events Event handling is the core purpose of a controller. There are three special methods available for watching and triggering events. Two are named after their jQuery counterparts, but add controller-specific behavior. this.on Used for watching and responding to events. this.on( [element elem,] string eventType [, selector delegate], function callback ); elem A reference to the element on which the event will be watched for. If not specified, this.scope is used by default. eventType The event being watched for. This can be a default browser event, like click, or events from widgets and models, like menuItemSelected. delegate A delegate selector can optionally be specified. See the jQuery documentation for more information. callback A callback function, called when this event is observed. The function specified here is automatically bound to this, so internal methods can be passed simply by referencing them like so: this.on( 'click', this.internalMethod ); this.trigger Used to emit events that originate from this controller (specifically, the scope element). this.trigger( [element elem,] string eventType [, object data] ); elem A reference to the element on which the event will be triggered. If not specified, this.scope is used by default. eventType The event being triggered. data An object containing data relevant to the event. this.triggerOn Used to emit events directly on sub-controllers from a parent controller. this.triggerOn( string controllerName, string eventType [, object data] ); controllerName The controller name to find and trigger the event on. The wildcard character (*) is supported at the end of this parameter and the event will be triggered on all matching controllers. Examples: core.front.core.comment core.front.core.* core.* eventType The event being triggered. data An object containing data relevant to the event.
-
Note: This documentation assumes you are running your installation with IN_DEV enabled. Due to the multi-application model that the IPS Social Suite employs, and the way that resources are bundled and served to the browser, there are a number of places in which different types of javascript files are stored. Development & production When developing for IPS4, you create your javascript files in the application or global directories, with a single file holding a single module. In production, however, IPS4 bundles related files together (concatenating and minifying them in the process) to reduce file size and the number of HTTP requests needed to fetch the scripts. You should be aware of how the bundles are built and from where, because this will dictate how you specify javascript files to load in your backend PHP code. Global resources Global resources are those that are loaded on every page load. This includes: Libraries - jQuery, Underscore etc. Application setup - Bootstrap module, loader module, base controller etc. Global modules - UI widgets, utilities, global controllers etc. Front/admin modules - Controllers global to the front-end/admin control panel, needed on every page in that area. All of the above files are located at <root>/dev/js/. Application resources Applications have their own folder for javascript, located at <appName>/dev/js. Within this folder, there should be three subfolders - admin, front and global. These folders hold javascript specific to those areas (or for the whole application, in the case of global). Within each of those folders respectively, there are further subfolders for different types of javascript file - controllers, templates and mixins. Therefore, within an application, the file structure looks like this: /appName /dev /js /front /controllers /profile ips.profile.main.js ips.profile.body.js /templates ips.templates.profile.js /mixins /admin /controllers /templates /mixins /global /controllers /templates /mixins Bundles for an application are built based on folder/filenames, and use the format <location>_<directory>.js. Taking the structure above as our example, if we loaded front_profile.js, it would contain: /js/front/controllers/profile/ips.profile.body.js /js/front/controllers/profile/ips.profile.main.js /js/front/templates/ips.templates.profile.js This bundle would then be included in your PHP code like so: \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_profile.js', 'core' ) ); Third-party libraries An application has an interface directory at <appName>/interface/. This is where third-party libaries used by individual applications should be stored and loaded from. They are not bundled or minified by IPS4, so you should store the pre-minified rather than the uncompressed version. Assuming you had added a file to interface at <appName>/interface/jquery/jquery.rangyinputs.js, it can be included in your PHP code like so: \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery.rangyinputs.js', 'appName', 'interface' ) );
-
Global scope & closure wrapping Variables must not be leaked into the global scope unless there is an explicit functional need for global access. Within function scope, variables must be declared with the var statement. To help prevent scope leaking, each script file should be entirely wrapped in an anonymous, self-executing closure, passing the jQuery and Underscore objects as parameters: ;( function($, _, undefined){ // Code goes here }(jQuery, _)); This prevents variables from leaking into the global scope, and also ensures jQuery code making use of the dollar variable $ does not conflict with any other code that might be running on the page. Note that a semi-colon appears as the first character. This helps prevent errors caused by incorrect semi-colon insertion in previous code, should this script be concatenated with others. It is a defensive coding practice, and recommended in all files. Strict mode The use of strict mode is required for all default javascript in IPS4. Strict mode helps prevent leaking into the global scope, catches code problems that would otherwise fail silently, and enables javascript engines to perform more optimizations. However, it is important that this is applied using the per-function syntax and not the per-script syntax, otherwise unexpected behavior can occur when script files are concatenated with 3rd-party scripts. Given that all script files should be wrapped in a self-executing closure, the syntax for enabling strict mode looks like this: ;( function($, _, undefined){ "use strict"; // Script here, e.g. module definition etc. }(jQuery, _)); By putting the "use strict"; statement inside the the closure, we ensure all of our code runs in strict mode without affecting any external code by accident. Note that the strict mode statement must be the very first line of the function, as shown. Further information about strict mode and the code requirements it imposes is available at MDN. Documentation & commenting The use of JSDoc is highly encouraged to document code. Refer to the JSDoc documentation for exact syntax. Within scripts, comment blocks /* */ should be reserved for formal documentation and situations where large blocks of existing code need to be commented out temporarily. Line-by-line comments should always use the single-line // comment style. /** * Formal documentation */ // Informal comments to describe code Semi-colons Semi-colons must be used at the end of every statement, even when function/object literals are being assigned. For example: // Simple statements $('#element').doSomething(); var test = 'value'; return 'text'; // Function and object literals var test = function () { ... }; var test2 = { key1: 'value' }; Names All names (variables, methods, properties etc.) should use lowerCamelCase, such as deselectAll, createModule and itemSelector. Logging You should not use console.log for debugging. This does not provide for disabling debugging in production, and can break pages on older browsers if not accounted for. A debugging method is provided via Debug.log that addresses these concerns, and should be used instead. Line length Aim to keep the line length below 120 characters to maintain readability. Longer lines should be split immediately after a comma or operator, with the remainder of the statement indented further to ensure clarity. For example: if( ( options.dataSource && options.dataSource.indexOf('#') === 0 && $( options.dataSource ).length ) || originalTextField.is('[list]') ){ jQuery chaining It is recommended that multiple jQuery methods chained on an element are placed on newlines to improve clarity. Additionally, indentation should be used to reflect the current working level of the chain. For example: $('selector') .find('.classname') .hide() .end() .find('.otherClassname') .css('width', '100%') .show() .end() .append( $('<div/>').attr( { id: 'someID' }); By indenting in this way, we make it clear that the hide() method applies only to the elements matching .classname, before calling end() to return to the original working level - and so on.
-
In IPS4, most javascript is split into self-contained, named modules. Modules and javascript files have a one-to-one relationship - each javascript file should contain one module only. Modules prevent variables leaking into the global scope (which must be avoided), and helps ensure code stays focused. All modules appear under the ips namespace. Types of Module A number of primary types of module exist within the software: Utility Modules Exist on the ips.utils namespace, and provide assorted functionality that other modules can make use of, including cookie handling and methods for handling responsive design. UI widget modules Exist on the ips.ui namespace, and define interface widgets to add interactivity to pages, and which can be reused using special data attributes. Controllers Controllers are modules that handle specific functionality for one particular page or part of a page. They respond to events from UI widgets and other controllers and update their scope accordingly. Mixins Mixins are used to extend controllers with additional functionality. Other types There are a number of other types of non-modularized file used too: Libraries/interfaces Libraries are 3rd party scripts that provide additional functionality. jQuery and Underscore are two examples. Templates Template files define special Mustache templates, that modules can use when rendering HTML to the browser. Languages Language files define strings which can be translated into other languages.
-
I want to briefly show our new cover photo support. Cover photos allow users to upload an image to represent something in the community; we currently support them in profiles and calendar events and may roll out support to other areas later. Here's a video of it in action for a calendar event. It's really simple to use, and of course still works responsively like the rest of our default theme. We hope it adds a new element of customization for content in your community. Developers For developers, supporting cover photos in your own addons is as easy as you'd expect. A helper is available which handles the nitty-gritty for you; you simply add $item->coverPhoto() to your template, override a couple of methods in your controller, and optionally build your own menu to control the user interaction (or you can let the helper output them for you, as in the video above). That's it! As always, screenshots are from pre-release software and are subject to change before release.
-
We've previously shown how responsiveness works in the AdminCP, but I'd like to briefly introduce responsiveness on the front end, and pick a few views to show you as examples (this will be a screenshot-heavy entry!) What is responsiveness? Before we get to that, allow me to recap what responsiveness is. Responsive design is a method by which you design one page in such a way that it adapts for the available screen space on the device the user is using. This means that one theme handles both the full desktop view and the condensed mobile view with some clever CSS, in contrast to 3.x where we had a separate mobile skin. When we took the decision to use responsive design for IPS4, one key aim was to ensure that the mobile view isn't feature reduced. We want all functionality and all areas of the suite to be available regardless of device, and with only a couple of exceptions we're on track to deliver this. Primary navigation In mobile view, the primary navigation collapses and moves to a menu accessible with the icon in the top-right. The breadcrumb becomes a 'Back' control, taking you up a level from the current page: The primary navigation, when opened, looks like this: Moderation Given that the responsive theme supports all functionality, this naturally includes moderation. IPS4 support full moderation capabilities regardless of the device you're using. Here's an example of moderating images in Gallery. Notice the menu to quickly select types of content to moderate, as well as the floating toolbar at the bottom of the screen to choose actions. Settings page Taking the settings area as an example, here's the same screen at the three supported breakpoints - desktop, tablet and mobile. Profile view Here's profile view (which we covered in more detail here) as seen on a phone: Calendar Calendar views on mobile: Gallery Viewing albums & images in a category: Blog The blog homepage: And viewing a blog: Forums Submitting a topic on mobile: Conclusion So that wraps up this round-up of responsive views. Naturally, there's many more views than this in the suite and we can't show screenshots of every single one, but hopefully this entry has given you a taste of a variety of views, and a better idea of how we're approaching mobile users in IPS4. As always, screenshots are from pre-release software and are subject to change before release.
-
Profiles are one of the key sections of a community, as everyone knows. They are what represent your users; where their information is shown and their content is gathered. When users contribute quality content to your community, their profile is where other users go to find it in one place. In short, it's an important area. In IPS4, profiles have had a complete makeover. There's a lot to cover, so I'll start with a numbered screenshot, and address each section individually (please note this is a large image; if you're on mobile, you may wish to wait to view it full-size). 1 - Header images In 3.x, users could customize their profiles by uploading a background image. In practice, this didn't work well when the software was integrated into an existing website design, and the options presented often ended up with a garish profile. In addition, social networks like Facebook and Twitter have adjusted user expectations on how profiles are customized. In IPS4, instead of page backgrounds, users instead get to customize their profile header image. This provides the best of both worlds - ample space to choose something creative, but it's contained and won't mess up a website design. 2 - Reputation The user's current reputation count is shown prominently in the info column, letting other users know if this member is an asset to the community. 3 - Warnings For moderators/staff, the profile now provides quick access to warning tools. By expanding the panel, they can see a brief history of recent warnings: And clicking one of these pops up the warning details: New warnings can also be issued inline, of course. 4 - Followers Followers replace friends in IPS4, and the user's followers are shown in this block. Instead of requiring mutual acknowledgement as with the traditional friends system (an approach that isn't entirely useful in a community of anonymous users), in IPS4 you follow users whom you find interesting in order to be updated when they contribute to the community. Users can of course prevent others from following them, if that is a concern to them. We'll have more details on how followers works in a later entry. 5 - About the user Traditional information about the user is shown in the next block, including custom profile fields. 6 - Recent visitors Recent visitors to this user's profile are shown next. As with 3.x, this can be toggled on and off by the profile owner. In 4.x, this is done by clicking the X in the corner of the block. 7 - Follow/Message member These primary buttons enable others to follow the user (if enabled), and send a new message inline, without leaving the page. 8 - User's content In 3.x, browsing a user's content was handled by the search area of the community (though links were available in the user's profile and hovercard). We felt this wasn't the best place for it, though. After all, a user's content should be available in their profile. That's what this button does. It switches the profile view to 'content browsing' mode, where you can see everything the user has done. It's smooth and buttery, and because it all loads dynamically, it feels like a true part of the profile. Here's a video of it in action (14MB) 9 - Long-form custom profile fields IPS4 supports various kinds of custom profile fields, including rich-text editors for long, styled content. Those custom profile fields will be shown in the main section of the profile where they get the space they need to be effective. About Me is a default field, but you can of course add your own too for your users to fill in. 10 - User's 'Nodes' A node is a fancy developer term for content containers that a user creates themselves, like gallery albums and blogs (as opposed to forum categories, which are created by the admin). In IPS4, a user's 'nodes' are shown right on their profile page, making it easy to find more interesting content from the user. In this screenshot, you can see my profile is showing my albums, my blogs, and other blogs to which I contribute. For developers, supporting your application in this section is easy too. 11 - Status feed The status feed from 3.x is of course still present, and the interaction is all inline without leaving the page. Conclusion That's profiles in 4.0. We hope the new focus on content and streamlined design provides a better experience for your users! As always, screenshots are from pre-release software and are subject to change before release.
-
IP.Board 3.x supports "My Media", which enables you to share other content from within the community by using the "My Media" button on the editor. This results in: http://community.invisionpower.com/files/file/4464-ips-gdk-for-ipboard-32-amp-33/ While this works, it has a few shortcomings: The styling of the block isn't really designed for each type of content it might show Users have to click the My Media button, then browse for the item, when they probably already know the URL they want to link to Not all content types are supported; e.g. you can't use My Media to link to a topic. For developers, implementing support for My Media in other applications was a process involving extension files and multiple methods We wanted to make sharing existing content much easier in IPS4, both for users and developers. "Embeddable content" is our solution. How to use it To embed content from elsewhere in the community, here's a step by step guide: Paste a link to it That's it! When you paste a link to almost any kind of content, whether it's a forum topic, calendar event, gallery album or more, IPS4 will automatically embed a small preview of the content, designed specifically for that content. In order to not disrupt an existing paragraph of text however, the embedded block won't be used if the link is surrounded by text. Embedded content only shows if the link is pasted on its own line, giving users more control over their post. Here's what a post looks like with a few embedded types shown: Embedded content can be used anywhere as you'd expect, including posts and comments, but also status updates, IP.Content articles, and so on. For developers Supporting embedded content in your apps is very easy; your content model simply has to implement IPSContentEmbeddable: class _Topic extends IPSContentItem implements ... IPSContentEmbeddable Your controller then simply looks for an embed request and returns HTML - that's it. Our default blocks also have their own template and CSS file, so theme designers can change the styling on a per-theme basis. Conclusion Our hope is that this easier method of embedding content encourages more cross-posting and highlighting of good content in IPS4. The process is almost wholly automatic, meaning users don't have to think in order to share great content with others. As always, screenshots are from pre-release software and are subject to change before release.
-
One of the most distinctive uses for a forum is that of a 'knowledge community', where users visit in order to get help with a problem or question. Our own Pre-sales forum uses this model, but we also have many customers who run forums that are almost exclusively knowledge-based (such as Roxio and Evernote). IP.Board 3.x introduced the concept of a "Best Answer" flag, allowing topic creators and staff the ability to highlight the reply to a topic that they deem best answers the question. This shows a snippet of the post in green at the top of the topic. Many sites now use this feature, but for IPS4 we wanted to expand the functionality offered for these types of forums. Question & Answer Forums Forums in IPS4 will enable you to set a forum as a "Q&A Forum". This adjusts the forum to be specifically designed for knowledge sharing. Instead of topics and posts, it has questions and answers. On the forum index, the forum will be shown as a Q&A forum with its forum icon (unless you've set a custom forum icon for that forum): Forum View When you enter the forum, instead of the normal topic listing, you see a list of questions: You'll see here that questions that have a best answer are indicated with a green checkbox. You'll also notice that one of the stats on the right hand side is 'votes'. In Q&A forums, questions can be voted up or down by users, in order to give them more visibility. More popular questions will bubble to the top (depending on the age of the question). You can of course still order by more traditional methods, if you wish. Popular questions from the past 30 days are also highlighted at the top of the forum, providing an up-to-date 'knowledgebase' that other users can see. Using our own presales forum as an example, if someone asked a question about an important feature and it was voted highly, other users visiting the forum would see it right at the top, which is great for content visibility and helping users get the answers they're looking for with minimal fuss. Question View Clicking into a question shows an adjusted topic view: The question (i.e. the first post) is shown at the top of the page on all pages, with answers listed below. You'll see that replies can also be voted up and down - in fact, this determines the order in which answers are shown inside the question. Popular answers, as determined by the community, will appear at the top, with worse or incorrect answers being pushed down. This is great for quickly finding the best information for the question at hand; in IP.Board 3.x, all too often a high-quality answer will appear in the middle of a topic and unfortunately go unnoticed by the topic creator or others looking for an answer. You can still sort answers by date, if you prefer. In the screenshot above you can also see the first post is marked as the best answer. "Best Answer" always appears at the top, regardless of its vote count. Question/answer ratings are separate from reputation, so you can of course still "Like" posts even if you don't think they're a good answer to the question. Conclusion So that's the new Q&A feature for IP.Board. We think it'll a big step forward for knowledge-driven communities using IP.Board, or even individual forums in other communities (like our pre-sales forum), helping users find answers to their questions more efficiently, and ultimately making your communities more useful. As always, screenshots are from pre-release software and are subject to change before release.
-
Different staff members typically have different roles within a community - especially larger communities, where you may have staff responsible for the theme, others handling tickets and different staff maintaining the system. In 3.x, we had a 'Bookmarks' system in the AdminCP that allowed you to create a menu of frequently-used sections in an effort to make them easier to get to, rather than navigating the main menus. As with every feature in IPS4, we took some time to think about what this Bookmark feature aimed to achieve, and whether it was the best way to achieve it (seriously - we have considered everything you'll see in IPS4 very carefully; nothing gets a free pass). We determined through speaking to administrators that the primary use of this feature was actually to get to one place quickly - whatever place that might be. It appeared to be rarely used as an actual bookmarks menu, and besides, duplicating browser functionality should always send up a red flag. We decided to rethink the idea. What we decided to do instead is allow AdminCP menus, both primary and secondary, to be reordered on a per-admin basis. This means each admin can set the AdminCP menu up however works best for themselves. If you use the theme system a lot, you can make that your first menu item. Or, if you use the ticket system in Nexus, you can put that first. Here's how it works:
-
The submissions process in IP.Downloads has a certain complexity that may not be apparent at first. As well as simple file uploads, we also support adding files from URLs and from the local file system, and screenshots can also be added in these ways. Which category you choose to submit to affects which of these options are available. In addition, via the AdminCP you can bulk-upload files - but not via the front-end. For IP.Downloads 4, we wanted to improve this process with interface and functionality changes. Submitting Files Here's a video demonstration of how creating a single file record in IP.Downloads works in v4: We've worked hard to improve the flow for users here - while they are uploading files (which may be large and take some time), they can continue adding the other file information such as screenshots and meta data. While that's happening, the upload progress is always shown at the top of the screen. In the video you'll also see how image uploading is handled, as well as prefixes in the tag system, which are now supported throughout the IPS Community Suite. Bulk Uploading Instead of going to the AdminCP to bulk-submit files, single- and bulk-uploads are now handled through exactly the same process on the front end. This means users can be granted bulk-upload permissions without requiring AdminCP access, a big improvement on the current implementation. To bulk upload, a user clicks the "Submit a file" button as normal, and chooses "I want to submit multiple files at once". They see the same upload area, but this time, the file information step is handled separately after the page is submitted. Each uploaded file has a separate block for file information and its own set of screenshots. We'll of course be showing more of the IP.Downloads homepage and file view later, but we hope that gives you a taste of what to expect in IP.Downloads in IPS4.
-
Reminder: this blog covers the technical details of 4.0's programming. For details on 4.0's features, follow our main blog. Reviewing controllers Some time ago, I blogged about the javascript framework we've built for IPS4. In it, I covered the most important component: controllers. To recap, a controller is a special object within the framework, and is applied on specific elements. That element is the controller's scope, and the controller works on it to provide its functionality. For example, a simple controller might look like this: ips.controller.register('core.global.core.example', { initialize: function () { this.on( 'click', this.showAlert ); }, showAlert: function (e) { alert( "This button's text is: " + this.scope.html() ); } }); It would be used on an element like so: <button data-controller='core.global.core.example'>Click me</button> Here we're registering core.global.core.example as a controller. This represents the controller path - it's in the format app.module.group.controllerName. Though it seems longwinded, this allows IPS4 to dynamically load controllers on-demand, rather than loading them all when the page is loaded. In the initialize method (called automatically), we set up an event handler for a click. When the element is clicked, you'd see an alert saying "This button's text is: Click me". So, that's how controllers work. Almost all page behavior in IPS4 is handled through controllers. But how would you change a method in an existing controller, say if you were writing an addon, or if you had two controllers that were fairly similar, and wanted to provide a base controller they both shared? Mixins To enable that, we have mixins. Mixins allow you to specify functions which are inherited by objects - in this case, our controller objects. This means, using mixins we can add new functions to a controller without needing to edit the controller itself. A mixin is defined like so: ips.controller.mixin('addAnotherMethod', 'core.global.core.example', true, function () { this.anotherMethod = function () { alert('Inside anotherMethod'); }; }); The ips.controller.mixin method takes up to 4 parameters: ips.controller.mixin( name, controller, automaticallyExtend, fnDefinition ) name: Name to identify this mixin controller: The controller this mixin extends automaticallyExtend: [optional, default false] Does this mixin automatically extend the controller (more on that below) fnDetinition: The function definition applied to the controller In this example, our mixin adds a method named anotherMethod to our core.global.core.example controller shown earlier. Let's talk more about the automaticallyExtend parameter. Mixins can be applied to controllers in one of two ways - either automatically on a global basis, or manually on a case-by-case basis. Mixins are manually specified like so: <button data-controller='core.global.core.example( addAnotherMethod )'>Click me</button> This means the mixin is used on this element - but another element using core.global.core.example wouldn't get it. This is useful when you're building your own apps or addons; you can write simple controllers that implement base functionality, then extend them with functionality for specific cases by specifying the mixin name in your HTML. We use this ourselves - for example, we have a base table controller that handles sorting, filtering and so forth. We then have a mixin for AdminCP tables, and a mixin for front-end tables, which add functionality specific to those areas, reducing code duplication. If you're extending an IPS controller in an IPS app, though, modifying the HTML isn't typically an option. Instead, you can specify the mixin as global, and it will be applied to all elements where that controller is used. This means you can write your own mixins that work with our default controllers without having to touch our controller code (and that's a good thing). Advice So that shows how to add new methods to a controller. But what if you want to work with the methods that already exist in the controller? In the above example you'd only be able to overwrite an existing method - certainly not ideal, because 1) you would break any other mixins using the default method, 2) if we made an update to a method in a later release, your mixin would break it. To facilitate working with existing controller methods, we're using a model called advice. This adds three special methods to a mixin: before, after and around. These let you 'hook into' existing methods and provide additional code for them. Let's rewrite our example mixin from above to take advantage of it: ips.controller.mixin('changeBackground', 'core.global.core.example', function () { this.before('showAlert', function () { this.scope.css({ background: 'red' }); }; }); Here, I'm using the before method. I'm hooking into showAlert method (from the controller), and changing the background color of the scope element. So what happens when the link is clicked? First the background changes to red, and then an alert box is shown. We've added the background changing functionality without needing to edit the controller at all. Here's two other ways of doing the same thing, using the other two special methods: ips.controller.mixin('changeBackground', 'core.global.core.example', function () { this.after('showAlert', function () { this.scope.css({ background: 'red' }); }; this.around('showAlert', function (origFn) { this.scope.css({ background: 'red' }); origFn(); }; }); The after method is fairly self-evident. With the around method, the original function is passed in as an argument, allowing you to determine when it is executed by your mixin. All three of these methods will stack, so multiple mixins can hook into the same method, and they'll be executed in order, each receiving the previous. Conclusion I hope this introduction to mixins proves useful to developers; it shows how our core app controllers can be extended in a non-destructive way, but also how your own apps can use the mixin functionality to create an inheritance model to make your life easier. Javascript in IPS4 makes extensive use of custom events, so the preferred way of adding new functionality is to listen for appropriate events and act on them - but the mixin support described above provides a mechanism by which you can adapt existing event handlers.
-
In 3.x, we support HTML emails being sent by the software. However, due to constraints we had at the time, HTML emails use pretty much the same content as plain text emails, but wrapped in a simple HTML wrapper. Additionally, users had to explicitly decide whether they wanted to receive HTML or plain text emails via a preference setting - quite an anachronism. All in all, not a very satisfactory user experience. Email handling in 4.0 In 4.0, users no longer choose which type of email to receive. Our email handler sends both types in a single email, and the email client chooses the most appropriate to show based on its capabilities. If it can display a fancy HTML version, that's what they'll see by default, but plain text is used if not. Email template system In 3.x, email content is defined by the language system, and each email has one language string which forms the content for both the plain text and HTML versions. Clearly, if we were going to improve the HTML templates we ship with, this would have to change. In 4.x, each type of email has two templates - one for HTML, one for plain text. This means a better display of content can be created for HTML emails, while keeping the plain text ones simple and to the point. Email templates make use of the skinning system foundation (which we'll reveal later), meaning they have full use of logic, template tags and more - so we can also customize the emails depending on the user they are being sent to (note though that email templates are not per-skin; they are global to the site). And, of course, email templates can be added and edited via an interface in the AdminCP. This isn't groundbreaking stuff, but a vast improvement on email handling in 3.x. Email template design We also wanted to improve our email templates, so that each type of email sent was designed specifically for the purpose. The data shown in a registration email will be different to a topic digest, for example, and the email should reflect that. Coding email templates is not a trivial thing, unfortunately. The latest version of Microsoft Outlook uses the Microsoft Word rendering engine(!!), while GMail strips out all CSS included in style tags - and that's just the start of the gotchas. This makes designing email templates a tricky business, and one that requires lots of testing to ensure compatibility. For our first 10 templates alone, I reviewed 900 screenshots to spot problems. As a result, we've taken the approach of creating email templates which are simple in appearance and would work well for most sites, with the goal of hopefully avoiding the need of most sites to edit them at all (though you can, if you wish). The colors we've used are fairly neutral, for this reason. For those mail agents that are a little more... advanced, our email templates in 4.0 will be responsive. They will look great on mobile devices as well as desktop clients. I have included some examples of email templates, along with their mobile counterparts. I should note at this point that this does not reveal the main skin design. As discussed above, emails are intentionally separate in design. Admin-completed registration Friend request New personal message New profile comment
-
We've been hard at work on IPS 4.0 for some time now, and we're finally at a stage where we are ready to reveal the new AdminCP to you. I won't be showing you everything the ACP has to offer - some things will be revealed in more detail in later blog entries. But lets get to an overview. Background information IPS4 brings with it a new CSS framework that aims to modularize our styles. This is something we started to work towards in IPB 3.2, but at that time we couldn't completely replace our structure. We no longer have a monolithic ipb_styles.css file. We now have a bunch of small CSS files, and each one handles something in particular. There's one each for forms, tables, pagination, buttons, layout and so on. This brings a few key benefits. Firstly, when we need to make a bug fix in, say, the forms CSS file, IPS4 will still be able to automatically upgrade all the other css files for you. In 3.x, one bug fix in ipb_styles.css could mean the whole file had to be manually upgraded. Secondly, it will be a lot more obvious for skinners where to look for particular things. Need to style a button? Look like buttons.css. Easy. And thirdly, if you're building pages in IP.Content, and you want to use our button styles, you can simply include that one CSS file without needing to include the entire CSS framework. CSS is of course concatenated and compressed before being delivered to the browser, but in a development environment, it exists as I described it above. In IPS4, both front end and AdminCP share the same CSS (and Javascript) framework. Skinners will be able to ship skins that work on both the front end and AdminCP with only a little extra work - and, of course, when we make bug fixes to the framework, it'll fix both areas. Before we go further, I want to make this part clear: The front-end and AdminCP look different. What you'll see shortly isn't what the front-end looks like. We will reveal that separately later. While the same framework is used, the AdminCP extends and overrides parts of it to suit its needs and style. Goals What did we want to achieve with the AdminCP? Our current AdminCP is often regarded as the best out of the big forum software platforms, so redesigning is a big undertaking. Better user of space. Our current ACP uses vertical space for the main menu, and horizontal space for the application menu. In an era of widescreen desktops being standard, this could be improved. Get rid of dropdown menus. The main menu currently uses dropdowns for navigation, but this can be difficult to use - especially if you want access something in a 3rd party app, meaning you have to traverse the Other Apps menu. More consistency across pages. Our current ACP has some interactive tables (e.g. the member list) - but not every table makes use of the functionality. We should be enhancing every page with similar functionality, if it makes sense. Better styling. People aren't a fan of pink, it turns out. I guess it'll have to go. The blue gradients are showing their age too. And the big one: Better mobile support. You can't effectively use the AdminCP on a mobile device. It's time you were able to manage your entire community from your phone with all of the same functionality, right? Responsive by default That last one is what we're most excited about. The AdminCP in IPS4 is fully responsive, and allows you to do everything just on a phone or tablet. What is responsiveness? It means that the page automatically changes to better suit the device you're using. While a desktop user would see full navigation menus and tables of data, a mobile user will see a reduced view (but with all the same data present!). Whether you need to manage your members, change some settings, send a bulk email or run some diagnostics, it can all be done on the go. This is a first for the big community software platforms, as far as I'm aware. Preview Here is a sample page from the new AdminCP, as seen on a desktop, with the same page shown at a mobile resolution: Although I won't include it here, tablets will see an 'intermediate' view with a reduced menu on the left. So, let's go over some of the key features of the screenshots. Navigation First, and perhaps most importantly, is the navigation. On a desktop, your applications are now arranged down the left-hand side, with their respective section menus available simply by hovering on the application - no dropdown menus to traverse. The application menu can be reordered per-admin, allowing each staff member to set the menu up to best suit their role. On a mobile, there's obviously not the space for a wide navigation menu. Therefore, the application/module menu is activated by clicking the top-right icon. This opens a sidebar, from which you can navigate: Tables What you see in the screenshots are our new default way of displaying tables of data. On the desktop view, we have filters across the top, a search box (and advanced search popup), and table headers can be clicked to dynamically sort the data via ajax. On a mobile view, this all collapses down - filters and sorting become menus, while table rows collapse to show data in a more suitable view. Responsive tables are a tricky thing to do right and there's a few different approaches, but given the types of data our AdminCP tables typically show, we think this is the best approach for us. Forms As has been discussed in some of our developer blogs, the IPS 4.0 framework supports a wide range of form field types - everything from text inputs to tree selectors to matrices. All of these field types work both on desktop and with a responsive mobile view. Here's a simple AdminCP form on both desktop and mobile: Tabs Tabs are used extensively, where appropriate. Here's a screenshot showing a typical tabbed page (and it also shows a tree view): Video of the mobile view in action I've taken a short video of the member section in action, showing filtering, live searching and the advanced search popup. I'm using the iOS simulator here, which has some display jitters and requires me to use the mouse, but it should give you a good idea of how the AdminCP will work on a phone. Conclusion So there we go - an overview of the new AdminCP. We still have more to show you. Individual features and pages that are noteworthy will be blogged about in due course in more detail, so keep an eye on this blog and our developer blog for more. Please do bear in mind that this is pre-alpha software, and everything you see is subject to change. We look forward to your feedback!
-
Javascript is a key component of front-end web development - it's essential for a modern web app to provide a good user experience, and javascript is central to enabling that. Getting it right in 4.0 has been one of our goals from the start. Problems To begin, let's summarize some of the issues with javascript in the current 3.x series: Lack of file organization (a single JS directory that contains dozens of unrelated script files) Different script types are bundled together (interface widgets, routes and full applications all in one place) Lack of modularity (each JS file is pretty much a floating island in how it might implement functionality, with no formalized structure) Simple things requiring code (how many times have you had to write half a dozen lines of JS just to launch a popup?) New dom nodes aren't initialized automatically (load some new HTML via ajax, and your popups won't work without manually hooking them up) Resolving these problems informed the javascript framework that we've built for 4.0. It all ultimately comes down to organizing code better and making the lives of developers easier (ours and yours!). The solution Our solution has been to build a framework which is modularized and heavily event-driven. In most cases, modules never call each other directly, but instead communicate with events which may (or may not) be responded to by another module. More on this later. The framework breaks down into four types of module: Widgets - interface widgets like popups, tooltips and menus Utility modules - things like cookie handling or URL manipulation Controllers - modules which work on a particular dom node. More on these later. Models - modules which handle data. In the vast majority of cases this is simply fetching data from the server. There's also 'interfaces', which are third party scripts like CKEditor, jQuery plugins and so forth. They aren't part of the framework, so I won't discuss them here. The groundwork Before getting to specific types of modules, we needed to lay the groundwork. Javascript 4.0 is modularized, with only a single global variable (ips) being created in the page. All other scripts are defined as modules, whether they are interface widgets, utilities or anything else. A module is defined as a function which returns an object containing public methods (the revealing module pattern, if you're interested). Here's an example module: ;( function($, _, undefined){ "use strict"; ips.createModule('ips.myModule', function () { // Private methods var _privateMethod = function () { }; // Functions that become public methods var init = function () { }, publicMethod = function () { }; // Expose public methods return { init: init, publicMethod: publicMethod } }); }(jQuery, _)); This pattern works well for our purpose, because it enables a module to contain private methods for doing internal work, while exposing only those methods which should be public to the outside world. This example module could then be used like so: ips.myModule.publicMethod(); So this keeps everything neatly organized, and ensures variables don't leak into the global scope (which we want to avoid at all costs). When the document is ready, the module is automatically initialized (though you can also have functions that execute before DOMReady if necessary). Interface widgets It's fair to say that interface widgets make up a large proportion of the JS used in our software - a web app such as ours has an intrinsic need for popups, menus, tooltips and so on. As mentioned above though, the big hinderance in 3.x is that these widgets have to be created manually (or, in a few simple cases, a special classname is added to an element to initialize it). This feels unnecessary when all the developer wants to do is show a simple widget that is otherwise standard. To alleviate this hassle, interface widgets in 4.0 all support a data API. What this means is that any widget can be created simply by adding some parameters to an HTML element, and specifying some options. Need a dialog box that loads a remote page? Simply do: <a href='...' data-ipsDialog data-ipsDialog-title='My dialog' data-ipsDialog-url='http://...'>Click me to open a dialog</a> Or if you need a hovercard, just do: <a href='...' data-ipsHover>This will launch a hovercard</a> We already have around two dozen widgets built, covering everything from dialogs, menus and tooltips, to keyboard navigation, tab bars and autocomplete - all supporting initialization with data attributes. Building your own widgets is easy - they are built as a module, and then simply define some settings to be accepted. The widget module does the rest. They can either be initialized when they're first seen in the dom (which you'd want for something like an image slider widget), or when an event occurs (such as hovering on a link, in the case of hovercards). Whenever new content is loaded into the page, widgets will be found and initialized automatically, too. Most widgets emit events when certain things occur - when we get to Controllers, you'll see why that is useful. Utilities Utilities are simple modules that don't need much discussion. They simply provide methods which do something useful - for example, fetch/set a cookie, write to the user's local browser database, or handle timestamps. Controllers Controllers are the meat of the application. Whereas interface widgets are used (and reused) as dumb tools on a page, controllers provide the logic for particular elements, sections and pages. They would, for example, handle the interactions in the topic listing, or the interactions with a post. Notice the word interaction - controllers are specifically designed to deal with events on the page. In fact, that's almost all they do! Controllers are initialized by specifying the controller name on an element, like so: <div id='topic_list' data-controller='forums.topicList'> </div> This div becomes the controller's scope. The controller can manipulate content inside the div, watch for events, and so on. Controllers, in general, should be as specific and succinct - so simply specifying a page-wide controller then handling everything inside it is discouraged. If we take the topic list in forum view as an example: <div id='topic_list' data-controller='forums.topicList'> <ul> <li data-controller='forums.topicRow'> ... </li> <li data-controller='forums.topicRow'> ... </li> <li data-controller='forums.topicRow'> ... </li> </ul> </div> Each topic row might specify the forums.topicRow controller which handles locking, pinning, or marking that topic. The topic list itself might specify the forums.topicList controller, which handles sorting and loading more topics. By doing it this way, controllers become responsible only for a specific portion of the functionality, which keeps them lean and simple. Controllers are entirely decoupled and cannot reference each other - which is by design, given that a controller is only interested and responsible for its own scope. To communicate, events are used. A controller can trigger events on the page, which other controllers, widgets and models might respond to (and in turn emit their own events). Continuing the example above, let's assume one of our topic rows is being deleted. The forums.topicRow controller handles removing the HTML from the DOM, but it doesn't care what happens after that - it's not its responsibility. However, it emits a deletedTopic event to let the page know. The forums.topicList controller sees this event, and because it does care, it loads a new topic entry into the list. By using events like this, we can build interfaces that respond fluidly to user interactions while still maintaining separation of concerns. So, how does a controller deal with events? Because we're using jQuery, event handling in controllers piggy-backs with the on and trigger methods. In the controller's initialize method (which is specifically for setting up event handlers), you simply do: this.on( 'menuItemSelected', '#menuid', this.handleMenuClick ); Usually when setting up events in an object using jQuery, you need to use $.proxy to properly control the scope of this, but in controllers, this is handled for you automatically - you just specify the method name. Notice the event we're observing here - menuItemSelected. This is an event that the ui.menu widget emits, and it illustrates how widgets and controllers can interact. Controllers can watch for events from widgets, then do something with the information given, all without ever directly referring to each other. Triggering an event is similar: this.trigger( 'doSomething', { color: 'yellow', size: 'big' }); This is the same syntax as jQuery's own trigger, except that the controller will ensure the parameters object is passed between different event handlers in the same chain. This will hopefully be clearer when you get your hands on it. Models Models are quite similar to controllers (they also use the special on and trigger methods), but their only purpose is to handle data. By decoupling data handling from controllers, we can centralize data getting/setting so that any controller can use it. Let's say we have a user model, which handles data for the current user. It might have event handlers for adding a friend, for example, so when it sees the event addFriend, it handles it appropriately. Let's also assume we have a controller on each post in a topic, there's three posts, and that the controllers are observing the click event on the 'add friend' button. Here's the sequence of events: (controller1) click 'add friend button' (controller1) emit 'addFriend' event (user model) adds a friend via ajax (user model) emits 'addedFriend' event (controller1) updates friend icon (controller2) updates friend icon (controller3) updates friend icon Even though it was controller1 that requested that the model adds a friend, all controllers respond to the event the model emits and updates the friend icon in its own post. This again shows the power of using events as the primary communication system - anyone can respond, and the caller doesn't have to deal with maintaining associations. Conclusion So that's about it - the new JS framework in IPS4. Hopefully this in-depth post has covered everything you need to know at this stage. You'll be pleased to know that most of the framework and widgets are already documented, and that will be available when IPS4 hits beta. Do note that everything covered here is subject to change or removal, as usual in our development blogs. If you have any questions, feel free to ask!