From c23df60e1b9cf09262e81e0448217075db973aa5 Mon Sep 17 00:00:00 2001 From: FrissDieGurke Date: Sun, 4 May 2014 12:34:22 +0200 Subject: [PATCH 1/3] removed unnecessary method + doc within Settings Framework --- public/src/modules/settings.js | 93 ---------------------------------- src/settings.js | 18 +------ 2 files changed, 1 insertion(+), 110 deletions(-) diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index 177a685137..af330fc598 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -13,79 +13,6 @@ define(function () { waitingJobs = 0, helper; - /* - * Attributes of HTML-tags that get used by default plugins: - * + data-key: the key to save/load the value within configuration-object - * + data-type: highest priority type-definition to determine what kind of element it is or which plugin to hook - * + type: normal priority type-definition - * + data-empty: if 'false' or '0' then values that are assumed as empty turn into null. data-empty of arrays affect - * their child-elements - * + data-trim: if not 'false' or '0' then values will get trimmed as defined by the elements type - * + data-split: if set and the element doesn't belong to any plugin, it's value will get split and joined by its - * value into the input-field - * array-elements: - * + data-split: separator (HTML allowed) between the elements, defaults to ', ' - * + data-new: value to insert into new created elements - * + data-attributes: an object to set the attributes of the child HTML-elements. tagName as special key will set - * the tag-name of the child HTML-elements - * key-fields: - * + data-trim: if 'false' or '0' then the value will get saved as string else as object providing following - * properties: ctrl, alt, shift, meta, code, char - * + data-split: separator between different modifiers and the key-code of the value that gets saved - * (only takes effect if trimming) - * + data-short: if not 'false' or '0' then modifier-keys get saved as first uppercase character - * (only takes effect if trimming) - * select: - * + data-options: an array of {"text":"Displayed Text","value":"some_value"}-like objects - * - * The name of the HTML-tag is lowest priority type-definition - * - * Examples-HTML: - No! -
- Yes! -
- An array of checkboxes that are selected by default: -

- A simple input-field of any common type: -
- A simple textarea: -
- Array of textareas: -

- 2D-Array of numbers that persist even when empty (but not empty rows): -

- Same with persisting empty rows, but not empty numbers, if no row is given null will get saved: -

- Array of Key-shortcuts (new: Ctrl+Shift+7): -

- Array of numbers (new: 42, step: 21): -

- Select with dynamic options: -
- Select that loads faster: - - * - * Matching configuration: - { - cfg1: false, - cfg2: true, - cfg3: [false, false, true], - cfg4: 'hello world', - cfg5: 'some\nlong\ntext', - cfg6: ['some\nlong\ntexts', 'and another one'], - cfg7: [[]], - cfg8: [[]], - cfg9: [], - cfg10: [], - cfg11: 3, - cfg12: 2 - } - */ - /** Returns the hook of given name that matches the given type or element. @param type The type of the element to get the matching hook for, or the element itself. @@ -404,26 +331,6 @@ define(function () { }, /** Registers a new plugin and calls its use-hook. - A plugin is an object containing a types-property to define its default bindings. - A plugin may also provide the following properties of type function with [return-value] (parameters): - use [void] - gets called when the Settings initializes the plugin. - init [void] (element) - gets called on page-load and every time after the create-hook. - ; element: The element to initialize. - create [JQuery-Object] (type, tagName, data) - gets called when a new HTML-instance needs to get created. - ; type: A string that identifies the plugin itself within this Settings-instance if set as data-type. - ; tagName: The tag-name that gets requested. - ; data: Additional data, plugin-dependent meaning. - destruct [void] (element) - gets called after a HTML-instance got removed from DOM - ; element: The element that got removed. - set [void] (element, value, trim) - gets called when the value of the element should be set to the given value. - ; element: The element to set its value. - ; value: The value to set. - ; trim: Whether the value is considered as trimmed. - get [value] (element, trim, empty) - gets called when the value of the given instance is needed. - ; element: The element to get its value. - ; trim: Whether the result should be trimmed. - ; empty: Whether considered as empty values should get saved too. - All passed elements are JQuery-Objects. @param service The plugin to register. @param types The types to bind the plugin to. */ diff --git a/src/settings.js b/src/settings.js index a8d8594658..b18635d4ff 100644 --- a/src/settings.js +++ b/src/settings.js @@ -27,7 +27,7 @@ function trim(obj1, obj2) { function mergeSettings(cfg, defCfg) { if (typeof cfg._settings !== typeof defCfg || typeof defCfg !== 'object') { - return cfg._settings = defCfg; + cfg._settings = defCfg; } else { expandObjBy(cfg._settings, defCfg); trim(cfg._settings, defCfg); @@ -103,22 +103,6 @@ Settings.prototype.persist = function (callback) { return this; }; -/** - Persists the settings if no settings are saved. - @param callback Gets called when done. - */ -Settings.prototype.persistOnEmpty = function (callback) { - var _this = this; - meta.settings.get(this.hash, function (err, settings) { - if (!settings._settings) { - _this.persist(callback); - } else if (typeof callback === 'function') { - callback.call(_this); - } - }); - return this; -}; - /** Returns the setting of given key or default value if not set. @param key The key of the setting to return. From cbef92bb629ef19ae14d7daaa576b7a63f8695c0 Mon Sep 17 00:00:00 2001 From: FrissDieGurke Date: Sun, 4 May 2014 12:37:49 +0200 Subject: [PATCH 2/3] Added settings-framework documentation --- docs/index.rst | 1 + docs/plugins/settings.rst | 306 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 docs/plugins/settings.rst diff --git a/docs/index.rst b/docs/index.rst index d403812123..c48eb05adc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,6 +73,7 @@ Plugin System plugins/create plugins/hooks + plugins/settings Widgets System -------------- diff --git a/docs/plugins/settings.rst b/docs/plugins/settings.rst new file mode 100644 index 0000000000..ad1954d853 --- /dev/null +++ b/docs/plugins/settings.rst @@ -0,0 +1,306 @@ +Settings Framework +========================== + +If you want to make your plugin customizable you may use the Settings Framework NodeBB offers. + +Server-Side Access +------------------ + +First you need some default settings, just create a new object for this: + +.. code:: javascript + + var defaultSettings = { + booleans: { + someBool: true, + moreBools: [false, false, true] + }, + strings: { + someString: 'hello world', + multiLineString: 'some\nlong\ntext', + arrayOfStrings: ['some\nlong\ntexts', 'and another one'] + }, + numbers: { + multiArrayDimensions: [[42,42],[21,21]], + multiArrayDimensions2: [[42,42],[]], + justSomeNumbers: [], + oneNumber: 3, + anotherNumber: 2 + }, + someKeys: ['C+S+#13'] // Ctrl+Shift+Enter + }; + +Now you can use the server-side settings-module to access the saved settings like this: + +.. code:: javascript + + var Settings = module.parent.require('./settings'); + var mySettings = new Settings('myPlugin', '0.1', defaultSettings, function() { + // the settings are ready and can accessed. + console.log(mySettings === this); // true + console.log(this.get('someString') === mySettings.get().someString); // true + }); + +The second parameter should change at least every time the structure of default settings changes. Because of this it's +recommended to use your plugins version. + +To use the settings client-side you need to create a WebSocket that delivers the result of ``mySettings.get()``. + +The mySettings-object will cache the settings, so be sure to use methods like ``mySettings.sync(callback)`` when the +settings got changed from somewhere else and ``mySettings.persist(callback)`` when you finished +``mySettings.set(key, value)`` calls. + +You need to create a socket-listener like following to allow the admin to initiate a synchronization with the settings +stored within database: + +.. code:: javascript + + var SocketAdmin = module.parent.require('./socket.io/admin'); + SocketAdmin.settings.syncMyPlugin = function() { + mySettings.sync(); + }; + +If you want to add a reset-functionality you need to create another socket-listener: + +.. code:: javascript + + SocketAdmin.settings.getMyPluginDefaults = function (socket, data, callback) { + callback(null, mySettings.createDefaultWrapper()); + }; + +The methods of the ``mySettings`` object you probably want to use: + ++ ``constructor()`` ++ ``sync([callback])`` + Reloads the settings from database, overrides local changes. ++ ``persist([callback])`` + Saves the local changes within database. ++ ``get([key])`` + Returns the setting(s) identified by given key. If no key is provided the whole settings-object gets returned. If no + such setting is saved the default value gets returned. ++ ``set([key, ]value)`` + Sets the setting of given key to given value. Remember that it's just a local change, you need to call ``persist`` + in order to save the changes. ++ ``reset([callback])`` + Persists the default settings. ++ ``getWrapper()`` + Returns the local object as it would get saved within database. ++ ``createWrapper(version, settings)`` + Creates an object like it would get saved within database containing given information and settings. ++ ``createDefaultWrapper()`` + Creates an object like it would get saved within database containing the default settings. + +Client-Side Access +------------------ + +The next step is making the settings available to the admin. + +You need to use the :doc:`hooks ` ``filter:admin.header.build`` (to display a link to your page within ACP) and +``action:app.load`` (to create the needed route). + +Within your page you can access the client-side Settings API via + +.. code:: javascript + + require(['settings'], function (settings) { + var wrapper = $('#my_form_id'); + // [1] + settings.sync('myPlugin', wrapper); + // [2] + }); + +To make a button with the id ``save`` actually save the settings you can add the following at ``[2]``: + +.. code:: javascript + + $('#save').click(function(event) { + event.preventDefault(); + settings.persist('myPlugin', wrapper, function(){ + socket.emit('admin.settings.syncMyPlugin'); + }); + }); + +As said before the server-side settings-object caches the settings, so we emit a WebSocket to notify the server to +synchronize the settings after they got persisted. + +To use a reset-button you can add the following at ``[2]``: + +.. code:: javascript + + $('#reset').click(function(event) { + event.preventDefault(); + socket.emit('admin.settings.getMyPluginDefaults', null, function (err, data) { + settings.set('myPlugin', data, wrapper, function(){ + socket.emit('admin.settings.syncMyPlugin'); + }); + }); + }); + +There you go, the basic structure is done. +Now you need to add the form-fields. + +Each field needs an attribute ``data-key`` to reference its position within the settings. +The Framework does support any fields whose jQuery-object provides the value via the ``val()`` method. + +The plugin to use for a field gets determined by its ``data-type``, ``type`` or tag-name in this order. + +Additionally the following plugins are registered by default: + * array (types: div, array) + An Array of any other fields. + Uses the object within ``data-attributes`` to define the array-elements. + Uses ``data-new`` to define the value of new created elements. + * key (types: key) + A field to input keyboard-combinations. + * checkbox, select, textarea + Handle appropriate fields. + +A full list of all attributes that may influence the behavior of the default Framework: + + * data-key: the key to save/load the value within configuration-object + * data-type: highest priority type-definition to determine what kind of element it is or which plugin to associate + * type: normal priority type-definition + * data-empty: if ``false`` or ``0`` then values that are assumed as empty turn into null. data-empty of arrays affect their child-elements + * data-trim: if not ``false`` or ``0`` then values will get trimmed as defined by the elements type + * data-split: if set and the element doesn't belong to any plugin, it's value will get split and joined by its value into the field + * array-elements: + + data-split: separator (HTML allowed) between the elements, defaults to ``', '`` + + data-new: value to insert into new created elements + + data-attributes: an object to set the attributes of the child HTML-elements. tagName as special key will set the tag-name of the child HTML-elements + * key-fields: + + data-trim: if ``false`` or ``0`` then the value will get saved as string else as object providing following properties: ``ctrl``, ``alt``, ``shift``, ``meta``, ``code``, ``char`` + + data-split: separator between different modifiers and the key-code of the value that gets saved (only takes effect if trimming) + + data-short: if not ``false`` or ``0`` then modifier-keys get saved as first uppercase character (only takes effect if trimming) + * select: + + data-options: an array of objects containing ``text`` and ``value`` attributes. + +The methods of the ``settings`` module: + ++ ``registerPlugin(plugin[, types])`` + Registers the given plugin and associates it to the given types if any, otherwise the plugins default types will get + used. ++ ``get()`` + Returns the saved object. ++ ``set(hash, settings[, wrapper[, callback[, notify]]])`` + Refills the fields with given settings and persists them. + ``hash`` Identifies your plugins settings. + ``settings`` The object to save in database (settings-wrapper if you use server-side Settings Framework). + ``wrapper`` (default: 'form') The DOM-Element that contains all fields to fill. + ``callback`` (default: null) Gets called when done. + ``notify`` (default: true) Whether to display saved- and fail-notifications. ++ ``sync(hash[, wrapper[, callback]])`` + Resets the settings to saved ones and refills the fields. ++ ``persist(hash[, wrapper[, callback[, notify]]])`` + Reads the settings from given wrapper (default: 'form') and saves them within database. + +For Settings 2.0 support the methods ``load`` and ``save`` are still available but not recommended. + +Client-Side Example Template +------------------ + +An example template-file to use the same settings we already used server-side: + +.. code:: html + +

My Plugin

+
+ +
+
+

+

Settings

+ A boolean:
+ An array of checkboxes that are selected by default: +

+ + A simple input-field of any common type:
+ A simple textarea:
+ Array of textareas: +

+ + 2D-Array of numbers that persist even when empty (but not empty rows): +

+ Same with persisting empty rows, but not empty numbers, if no row is given null will get saved: +

+ Array of numbers (new: 42, step: 21): +

+ Select with dynamic options: +
+ Select that loads faster: + + + Array of Key-shortcuts (new: Ctrl+Shift+7): +

+

+
+ + +
+ + + +Custom Settings-Elements +------------------ + +If you want do define your own element-structure you can create a **plugin** for the Settings Framework. + +This allows you to use a whole object like a single field which - besides comfort in using multiple similar objects - +allows you to use them within arrays. + +A plugin is basically an object that contains at least an attribute ``types`` that contains an array of strings that +associate DOM-elements with your plugin. + +You can add a plugin at ``[1]`` using the method ``settings.registerPlugin``. + +To customize the way the associated fields get interpreted you may add the following methods to your plugin-object: + +All given elements are instances of JQuery. + +All methods get called within Settings-scope. + ++ ``use()`` + Gets called when the plugin gets registered. ++ ``[HTML-Element|JQuery] create(type, tagName, data)`` + Gets called when a new element should get created (eg. by expansion of an array). ++ ``destruct(element)`` + Gets called when the given element got removed from DOM (eg. by array-splice). ++ ``init(element)`` + Gets called when an element should get initialized (eg. after creation). ++ ``[value] get(element, trim, empty)`` + Gets called whenever the value of the given element is requested. + ``trim`` Whether the result should get trimmed. + ``empty`` Whether considered as empty values should get saved too. ++ ``set(element, value, trim)`` + Gets called whenever the value of the given element should be set to given one. + ``trim`` Whether the value is assumed as trimmed. + +For further impression take a look at the +`default plugins `_. + +You should also take a look at the helper-functions within +`Settings `_ in order to create +your own plugins. There are a few methods that take response to call the methods of other plugins when fittingly. \ No newline at end of file From c4606e7009f036ad9fe268e889d09d5801616ec9 Mon Sep 17 00:00:00 2001 From: FrissDieGurke Date: Sun, 4 May 2014 13:08:20 +0200 Subject: [PATCH 3/3] minified settings-framework wrapper-length --- public/src/modules/settings.js | 10 ++++----- src/settings.js | 37 ++++++++++++++++------------------ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index af330fc598..fa9ac0ac3d 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -271,9 +271,9 @@ define(function () { @param callback The callback to call when done. */ persistSettings: function (hash, settings, notify, callback) { - if (settings != null && settings._settings != null && typeof settings._settings !== 'string') { + if (settings != null && settings._ != null && typeof settings._ !== 'string') { settings = helper.deepClone(settings); - settings._settings = JSON.stringify(settings._settings); + settings._ = JSON.stringify(settings._); } socket.emit('admin.settings.set', { hash: hash, @@ -307,7 +307,7 @@ define(function () { */ use: function (settings) { try { - settings._settings = JSON.parse(settings._settings); + settings._ = JSON.parse(settings._); } catch (_error) {} Settings.cfg = settings; } @@ -324,8 +324,8 @@ define(function () { @returns Object The settings. */ get: function () { - if (Settings.cfg != null && Settings.cfg._settings !== void 0) { - return Settings.cfg._settings; + if (Settings.cfg != null && Settings.cfg._ !== void 0) { + return Settings.cfg._; } return Settings.cfg; }, diff --git a/src/settings.js b/src/settings.js index b18635d4ff..2286f26b1d 100644 --- a/src/settings.js +++ b/src/settings.js @@ -26,16 +26,16 @@ function trim(obj1, obj2) { } function mergeSettings(cfg, defCfg) { - if (typeof cfg._settings !== typeof defCfg || typeof defCfg !== 'object') { - cfg._settings = defCfg; + if (typeof cfg._ !== typeof defCfg || typeof defCfg !== 'object') { + cfg._ = defCfg; } else { - expandObjBy(cfg._settings, defCfg); - trim(cfg._settings, defCfg); + expandObjBy(cfg._, defCfg); + trim(cfg._, defCfg); } } /** - A class to manage Objects saved in {@link meta.settings} within property "_settings". + A class to manage Objects saved in {@link meta.settings} within property "_". Constructor, synchronizes the settings and repairs them if version differs. @param hash The hash to use for {@link meta.settings}. @param version The version of the settings, used to determine whether the saved settings may be corrupt. @@ -71,8 +71,8 @@ Settings.prototype.sync = function (callback) { var _this = this; meta.settings.get(this.hash, function (err, settings) { try { - if (settings._settings) { - settings._settings = JSON.parse(settings._settings); + if (settings._) { + settings._ = JSON.parse(settings._); } } catch (_error) {} _this.cfg = settings; @@ -87,15 +87,12 @@ Settings.prototype.sync = function (callback) { @param callback Gets called when done. */ Settings.prototype.persist = function (callback) { - var conf = this.cfg._settings, + var conf = this.cfg._, _this = this; if (typeof conf === 'object') { conf = JSON.stringify(conf); } - meta.settings.set(this.hash, { - _settings: conf, - version: this.cfg.version - }, function () { + meta.settings.set(this.hash, this.createWrapper(this.cfg.v, conf), function () { if (typeof callback === 'function') { callback.apply(_this, arguments || []); } @@ -110,7 +107,7 @@ Settings.prototype.persist = function (callback) { @returns Object The setting to be used. */ Settings.prototype.get = function (key, def) { - var obj = this.cfg._settings, + var obj = this.cfg._, parts = (key || '').split('.'), part; for (var i = 0; i < parts.length; i++) { @@ -148,8 +145,8 @@ Settings.prototype.getWrapper = function () { */ Settings.prototype.createWrapper = function (version, settings) { return { - version: version, - _settings: settings + v: version, + _: settings }; }; @@ -168,11 +165,11 @@ Settings.prototype.createDefaultWrapper = function () { */ Settings.prototype.set = function (key, val) { var part, obj, parts; - this.cfg.version = this.version; + this.cfg.v = this.version; if (val == null || !key) { - this.cfg._settings = val || key; + this.cfg._ = val || key; } else { - obj = this.cfg._settings; + obj = this.cfg._; parts = key.split('.'); for (var i = 0, _len = parts.length - 1; i < _len; i++) { if (part = parts[i]) { @@ -202,13 +199,13 @@ Settings.prototype.reset = function (callback) { @param force Whether to update and persist the settings even if the versions ara equal. */ Settings.prototype.checkStructure = function (callback, force) { - if (!force && this.cfg.version === this.version) { + if (!force && this.cfg.v === this.version) { if (typeof callback === 'function') { callback(); } } else { mergeSettings(this.cfg, this.defCfg); - this.cfg.version = this.version; + this.cfg.v = this.version; this.persist(callback); } return this;