added new settings-system

v1.18.x
Ole Reglitzki 11 years ago
parent e0a83728d6
commit d76ba519d4

@ -1,68 +1,592 @@
"use strict";
/* global define, socket, app */
define(function () {
/*
settings.js 2.0, because version 1:
- saved everything in "config" hash
- was hand-rolled (mm, salmon hand roll)
- Relied on app.config (!!)
This module should:
- Allow you to save to any specified hash
- Rely on jQuery
- Use sockets
- Be more awesome
*/
const DEFAULT_PLUGINS = [
'settings/checkbox',
'settings/textarea',
'settings/select',
'settings/array',
'settings/key'
];
define(function() {
var Settings = {};
var Settings,
onReady = [],
waitingJobs = 0,
helper;
Settings.load = function(hash, formEl, callback) {
socket.emit('admin.settings.get', {
hash: hash
}, function(err, values) {
if (!err) {
$(formEl).deserialize(values);
if (typeof callback === 'function') {
callback();
/*
* 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!
<input type="checkbox" data-key="cfg1"></input><br>
Yes!
<input type="checkbox" data-key="cfg2"></input><br>
An array of checkboxes that are selected by default:
<div data-key="cfg3" data-attributes='{"data-type":"checkbox"}' data-new='true'></div><br>
A simple input-field of any common type:
<input type="password" data-key="cfg4"></input><br>
A simple textarea:
<textarea data-key="cfg5"></textarea><br>
Array of textareas:
<div data-key="cfg6" data-attributes='{"data-type":"textarea"}' data-new='Hello Kitty, ahem... World!'></div><br>
2D-Array of numbers that persist even when empty (but not empty rows):
<div data-key="cfg7" data-split="<br>" data-attributes='{"data-type":"array","data-attributes":{"type":"number"}}' data-new='[42,21]'></div><br>
Same with persisting empty rows, but not empty numbers, if no row is given null will get saved:
<div data-key="cfg8" data-split="<br>" data-empty="false" data-attributes='{"data-type":"array","data-empty":true,"data-attributes":{"type":"number","data-empty":false}}' data-new='[42,21]'></div><br>
Array of Key-shortcuts (new: Ctrl+Shift+7):
<div data-key="cfg9" data-attributes='{"data-type":"key"}' data-new='Ctrl+Shift+#55'></div><br>
Array of numbers (new: 42, step: 21):
<div data-key="cfg10" data-attributes='{"data-type":"number","step":21}' data-new='42'></div><br>
Select with dynamic options:
<select data-key="cfg11" data-options='[{"value":"2","text":"2"},{"value":"3","text":"3"}]'></select><br>
Select that loads faster:
<select data-key="cfg12"><br>
<option value="2">2</option>
<option value="3">3</option>
</select>
*
* 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.
@param name The name of the hook.
*/
function getHook(type, name) {
var hook, plugin;
if (typeof type !== 'string') {
type = $(type);
type = type.data('type') || type.attr('type') || type.prop('tagName');
}
plugin = Settings.plugins[type.toLowerCase()];
if (plugin == null) {
return void 0;
}
hook = plugin[name];
if (typeof hook === 'function') {
return hook;
} else {
return null;
}
}
helper = {
/**
@returns Object A deep clone of the given object.
*/
deepClone: function (obj) {
if (typeof obj === 'object') {
return JSON.parse(JSON.stringify(obj));
} else {
return obj;
}
},
/**
Creates a new Element with given data.
@param tagName The tag-name of the element to create.
@param data The attributes to set.
@param text The text to add into the element.
@returns HTMLElement The created element.
*/
createElement: function (tagName, data, text) {
var element = document.createElement(tagName);
for (var k in data) {
element.setAttribute(k, data[k]);
}
if (text) {
element.appendChild(document.createTextNode(text));
}
return element;
},
/**
Calls the init-hook of the given element.
@param element The element to initialize.
*/
initElement: function (element) {
var hook = getHook(element, 'init');
if (hook != null) {
hook.call(Settings, $(element));
}
},
/**
Calls the destruct-hook of the given element.
@param element The element to destruct.
*/
destructElement: function (element) {
var hook = getHook(element, 'destruct');
if (hook != null) {
hook.call(Settings, $(element));
}
},
/**
Creates and initializes a new element.
@param type The type of the new element.
@param tagName The tag-name of the new element.
@param data The data to forward to create-hook or use as attributes.
@returns JQuery The created element.
*/
createElementOfType: function (type, tagName, data) {
var element, hook = getHook(type, 'create');
if (hook != null) {
element = $(hook.call(Settings, type, tagName, data));
} else {
if (data == null) {
data = {};
}
if (type != null) {
data.type = type;
}
element = $(helper.createElement(tagName || 'input', data))
}
element.data('type', type);
helper.initElement(element);
return element;
},
/**
Creates a new Array that contains values of given Array depending on trim and empty.
@param array The array to clean.
@param trim Whether to trim each value if it has a trim-function.
@param empty Whether empty values should get added.
@returns Array The filtered and/or modified Array.
*/
cleanArray: function (array, trim, empty) {
var cleaned = [];
if (!trim && empty) {
return array;
}
for (var i = 0; i < array.length; i++) {
var value = array[i];
if (trim) {
value = value === true ? 1 : value === false ? 0 : typeof value.trim === 'function' ? value.trim() : value;
}
if (empty || (value != null ? value.length : void 0)) {
cleaned.push(value);
}
}
return cleaned;
},
isTrue: function (value) {
return value === 'true' || +value === 1;
},
isFalse: function (value) {
return value === 'false' || +value === 0;
},
/**
Calls the get-hook of the given element and returns its result.
If no hook is specified it gets treated as input-field.
@param element The element of that the value should get read.
@returns Object The value of the element.
*/
readValue: function (element) {
var empty = !helper.isFalse(element.data('empty')),
trim = !helper.isFalse(element.data('trim')),
split = element.data('split'),
hook = getHook(element, 'get'),
value;
if (hook != null) {
return hook.call(Settings, element, trim, empty);
}
if (split != null) {
empty = helper.isTrue(element.data('empty')); // default empty-value is false for arrays
value = element.val();
var array = (value != null ? value.split(split || ',') : void 0) || [];
return helper.cleanArray(array, trim, empty);
} else {
value = element.val();
if (trim && value != null && typeof value.trim === 'function') {
value = value.trim();
}
if (empty || value !== void 0 && (value == null || value.length !== 0)) {
return value;
} else {
return void 0;
}
}
},
/**
Calls the set-hook of the given element.
If no hook is specified it gets treated as input-field.
@param element The JQuery-Object of the element to fill.
@param value The value to set.
*/
fillField: function (element, value) {
var hook = getHook(element, 'set'),
trim = element.data('trim');
trim = trim !== 'false' && +trim !== 0;
if (hook != null) {
return hook.call(Settings, element, value, trim);
}
if (value instanceof Array) {
value = value.join(element.data('split') || (trim ? ', ' : ','));
}
if (trim && value && typeof value.trim === 'function') {
value = value.trim();
if (typeof value.toString === 'function') {
value = value.toString();
}
} else if (value != null) {
if (typeof value.toString === 'function') {
value = value.toString();
}
if (trim) {
value = value.trim();
}
} else {
value = '';
}
if (value !== void 0) {
element.val(value);
}
},
/**
Calls the init-hook and {@link helper.fillField} on each field within wrapper-object.
@param wrapper The wrapper-element to set settings within.
*/
initFields: function (wrapper) {
$('[data-key]', wrapper).each(function (ignored, field) {
field = $(field);
var hook = getHook(field, 'init'),
keyParts = field.data('key').split('.'),
value = Settings.get();
if (hook != null) {
hook.call(Settings, field);
}
for (var i = 0; i < keyParts.length; i++) {
var part = keyParts[i];
if (part && value != null) {
value = value[part];
}
}
helper.fillField(field, value);
});
},
/**
Increases the amount of jobs before settings are ready by given amount.
@param amount The amount of jobs to register.
*/
registerReadyJobs: function (amount) {
return waitingJobs += amount;
},
/**
Decreases the amount of jobs before settings are ready by given amount or 1.
If the amount is less or equal 0 all callbacks registered by {@link helper.whenReady} get called.
@param amount The amount of jobs that finished.
*/
beforeReadyJobsDecreased: function (amount) {
if (amount == null) {
amount = 1;
}
if (waitingJobs > 0) {
waitingJobs -= amount;
if (waitingJobs <= 0) {
for (var i = 0; i < onReady.length; i++) {
onReady[i]();
}
onReady = [];
}
}
},
/**
Calls the given callback when the settings are ready.
@param callback The callback.
*/
whenReady: function (callback) {
if (waitingJobs <= 0) {
callback();
} else {
console.log('[settings] Unable to load settings for hash: ', hash);
onReady.push(callback);
}
},
/**
Persists the given settings with given hash.
@param hash The hash to use as settings-id.
@param settings The settings-object to persist.
@param notify Whether to send notification when settings got saved.
@param callback The callback to call when done.
*/
persistSettings: function (hash, settings, notify, callback) {
if (settings != null && settings._settings != null && typeof settings._settings !== 'string') {
settings = helper.deepClone(settings);
settings._settings = JSON.stringify(settings._settings);
}
});
socket.emit('admin.settings.set', {
hash: hash,
values: settings
}, function (err) {
if (notify) {
if (err) {
app.alert({
title: 'Settings Not Saved',
type: 'danger',
message: "NodeBB failed to save the settings.",
timeout: 5000
});
console.log('[settings] Unable to set settings for hash: ', hash);
} else {
app.alert({
title: 'Settings Saved',
type: 'success',
timeout: 2500
});
}
}
if (typeof callback === 'function') {
callback(err);
}
});
},
/**
Sets the settings to use to given settings.
@param settings The settings to use.
*/
use: function (settings) {
try {
settings._settings = JSON.parse(settings._settings);
} catch (_error) {}
Settings.cfg = settings;
}
};
Settings.save = function(hash, formEl, callback) {
formEl = $(formEl);
if (formEl.length) {
var values = formEl.serializeObject();
Settings = {
helper: helper,
plugins: {},
cfg: {},
// "Fix" checkbox values, so that unchecked options are not omitted
formEl.find('input[type="checkbox"]').each(function(idx, inputEl) {
inputEl = $(inputEl);
if (!inputEl.is(':checked')) {
values[inputEl.attr('id')] = 'off';
/**
Returns the saved settings.
@returns Object The settings.
*/
get: function () {
if (Settings.cfg != null && Settings.cfg._settings !== void 0) {
return Settings.cfg._settings;
}
return Settings.cfg;
},
/**
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.
*/
registerPlugin: function (service, types) {
if (types == null) {
types = service.types;
} else {
service.types = types;
}
if (typeof service.use === 'function') {
service.use.call(Settings);
}
for (var i = 0; i < types.length; i++) {
var type = types[i].toLowerCase();
if (Settings.plugins[type] == null) {
Settings.plugins[type] = service;
}
}
},
/**
Sets the settings to given ones, resets the fields within given wrapper and saves the settings server-side.
@param hash The hash to use as settings-id.
@param settings The settings to set.
@param wrapper The wrapper-element to find settings within.
@param callback The callback to call when done.
@param notify Whether to send notification when settings got saved.
*/
set: function (hash, settings, wrapper, callback, notify) {
if (notify == null) {
notify = true;
}
helper.whenReady(function () {
helper.use(settings);
helper.initFields(wrapper || 'form');
helper.persistSettings(hash, settings, notify, callback);
});
socket.emit('admin.settings.set', {
hash: hash,
values: values
}, function(err) {
},
/**
Fetches the settings from server and calls {@link Settings.helper.initFields} once the settings are ready.
@param hash The hash to use as settings-id.
@param wrapper The wrapper-element to set settings within.
@param callback The callback to call when done.
*/
sync: function (hash, wrapper, callback) {
socket.emit('admin.settings.get', {
hash: hash
}, function (err, values) {
if (err) {
console.log('[settings] Unable to load settings for hash: ', hash);
if (typeof callback === 'function') {
callback(err);
}
} else {
helper.whenReady(function () {
helper.use(values);
helper.initFields(wrapper || 'form');
if (typeof callback === 'function') {
callback();
}
});
}
});
},
/**
Reads the settings from fields and saves them server-side.
@param hash The hash to use as settings-id.
@param wrapper The wrapper-element to find settings within.
@param callback The callback to call when done.
@param notify Whether to send notification when settings got saved.
*/
persist: function (hash, wrapper, callback, notify) {
var notSaved = [],
fields = $('[data-key]', wrapper || 'form').toArray();
if (notify == null) {
notify = true;
}
for (var i = 0; i < fields.length; i++) {
var field = $(fields[i]),
value = helper.readValue(field),
parentCfg = Settings.get(),
keyParts = field.data('key').split('.'),
lastKey = keyParts[keyParts.length - 1];
if (keyParts.length > 1) {
for (var j = 0; j < keyParts.length - 1; j++) {
var part = keyParts[j];
if (part && parentCfg != null) {
parentCfg = parentCfg[part];
}
}
}
if (parentCfg != null) {
if (value != null) {
parentCfg[lastKey] = value;
} else {
delete parentCfg[lastKey];
}
} else {
notSaved.push(field.data('key'));
}
}
if (notSaved.length) {
app.alert({
title: 'Settings Saved',
type: 'success',
timeout: 2500
title: 'Attributes Not Saved',
message: "'" + (notSaved.join(', ')) + "' could not be saved. Please contact the plugin-author!",
type: 'danger',
timeout: 5000
});
if (typeof callback === 'function') {
callback();
}
helper.persistSettings(hash, Settings.cfg, notify, callback);
},
load: function(hash, formEl, callback) {
socket.emit('admin.settings.get', {
hash: hash
}, function(err, values) {
if (!err) {
$(formEl).deserialize(values);
if (typeof callback === 'function') {
callback();
}
} else {
console.log('[settings] Unable to load settings for hash: ', hash);
}
});
} else {
console.log('[settings] Form not found.');
},
save: function(hash, formEl, callback) {
formEl = $(formEl);
if (formEl.length) {
var values = formEl.serializeObject();
// "Fix" checkbox values, so that unchecked options are not omitted
formEl.find('input[type="checkbox"]').each(function(idx, inputEl) {
inputEl = $(inputEl);
if (!inputEl.is(':checked')) {
values[inputEl.attr('id')] = 'off';
}
});
socket.emit('admin.settings.set', {
hash: hash,
values: values
}, function(err) {
app.alert({
title: 'Settings Saved',
type: 'success',
timeout: 2500
});
if (typeof callback === 'function') {
callback();
}
});
} else {
console.log('[settings] Form not found.');
}
}
};
helper.registerReadyJobs(1);
require(DEFAULT_PLUGINS, function () {
for (var i = 0; i < arguments.length; i++) {
Settings.registerPlugin(arguments[i]);
}
helper.beforeReadyJobsDecreased();
});
return Settings;
});

@ -0,0 +1,147 @@
define(function () {
var Settings = null,
SettingsArray,
helper = null;
/**
Creates a new button that removes itself and the given elements on click.
Calls {@link Settings.helper.destructElement} for each given field.
@param elements The elements to remove on click.
@returns JQuery The created remove-button.
*/
function createRemoveButton(elements) {
var rm = $(helper.createElement('button', {
"class": 'btn btn-xs btn-primary remove',
title: 'Remove Item'
}, '-'));
rm.click(function (event) {
event.preventDefault();
elements.remove();
rm.remove();
elements.each(function (i, element) {
element = $(element);
if (element.is('[data-key]')) {
helper.destructElement(element);
}
});
});
return rm;
}
/**
Creates a new child-element of given field with given data and calls given callback with elements to add.
@param field Any wrapper that contains all fields of the array.
@param key The key of the array.
@param attributes The attributes to call {@link Settings.helper.createElementOfType} with or to add as
element-attributes.
@param value The value to call {@link Settings.helper.fillField} with.
@param separator The separator to use.
@param insertCb The callback to insert the elements.
*/
function addArrayChildElement(field, key, attributes, value, separator, insertCb) {
attributes = helper.deepClone(attributes);
var type = attributes['data-type'] || attributes.type || 'text',
element = $(helper.createElementOfType(type, attributes.tagName, attributes));
element.attr('data-parent', '_' + key);
delete attributes['data-type'];
delete attributes['tagName'];
for (var name in attributes) {
var val = attributes[name];
if (name.search('data-') === 0) {
element.data(name.substring(5), val);
} else if (name.search('prop-') === 0) {
element.prop(name.substring(5), val);
} else {
element.attr(name, val);
}
}
helper.fillField(element, value);
if ($("[data-parent=\"_" + key + "\"]", field).length) {
insertCb(separator);
}
insertCb(element);
insertCb(createRemoveButton(element.add(separator)));
}
/**
Adds a new button that adds a new child-element to given element on click.
@param element The element to insert the button.
@param key The key to forward to {@link addArrayChildElement}.
@param attributes The attributes to forward to {@link addArrayChildElement}.
@param separator The separator to forward to {@link addArrayChildElement}.
*/
function addAddButton(element, key, attributes, separator) {
var addSpace = $(document.createTextNode(' ')),
newValue = element.data('new') || '',
add = $(helper.createElement('button', {
"class": 'btn btn-sm btn-primary add',
title: 'Expand Array'
}, '+'));
add.click(function (event) {
event.preventDefault();
addArrayChildElement(element, key, attributes, newValue, separator.clone(), function (el) {
addSpace.before(el);
});
});
element.append(addSpace);
element.append(add);
}
SettingsArray = {
types: ['array', 'div'],
use: function () {
helper = (Settings = this).helper;
},
create: function (ignored, tagName) {
return helper.createElement(tagName || 'div');
},
set: function (element, value) {
var attributes = element.data('attributes'),
key = element.data('key') || element.data('parent'),
separator = element.data('split') || ', ';
separator = (function () {
try {
return $(separator);
} catch (_error) {
return $(document.createTextNode(separator));
}
})();
if (typeof attributes !== 'object') {
attributes = {};
}
element.empty();
if (!(value instanceof Array)) {
value = [];
}
for (var i = 0; i < value.length; i++) {
addArrayChildElement(element, key, attributes, value[i], separator.clone(), function (el) {
element.append(el);
});
}
addAddButton(element, key, attributes, separator);
},
get: function (element, trim, empty) {
var key = element.data('key') || element.data('parent'),
children = $("[data-parent=\"_" + key + "\"]", element),
values = [];
children.each(function (i, child) {
child = $(child);
var val = helper.readValue(child),
empty = helper.isTrue(child.data('empty'));
if (empty || val !== void 0 && (val == null || val.length !== 0)) {
return values.push(val);
}
});
if (empty || values.length) {
return values;
} else {
return void 0;
}
}
};
return SettingsArray;
});

@ -0,0 +1,36 @@
define(function () {
var Settings = null,
SettingsCheckbox;
SettingsCheckbox = {
types: ['checkbox'],
use: function () {
Settings = this;
},
create: function () {
return Settings.helper.createElement('input', {
type: 'checkbox'
});
},
set: function (element, value) {
element.prop('checked', value);
},
get: function (element, trim, empty) {
var value = element.prop('checked');
if (value == null) {
return void 0;
}
if (empty) {
return value || void 0;
}
if (trim) {
return value ? 1 : 0;
}
return value;
}
};
return SettingsCheckbox;
});

@ -0,0 +1,221 @@
define(function () {
var Settings = null,
SettingsKey,
helper = null,
lastKey = null,
oldKey = null,
keyMap = Object.freeze({
0: '',
8: 'Backspace',
9: 'Tab',
13: 'Enter',
27: 'Escape',
32: 'Space',
37: 'Left',
38: 'Up',
39: 'Right',
40: 'Down',
45: 'Insert',
46: 'Delete',
187: '=',
189: '-',
190: '.',
191: '/',
219: '[',
220: '\\',
221: ']'
});
function Key() {
this.c = false;
this.a = false;
this.s = false;
this.m = false;
this.code = 0;
this.char = '';
}
/**
Returns either a Key-Object representing the given event or null if only modification-keys got released.
@param event The event to inspect.
@returns Key | null The Key-Object the focused element should be set to.
*/
function getKey(event) {
var anyModChange = event.ctrlKey !== lastKey.c || event.altKey !== lastKey.a || event.shiftKey !== lastKey.s || event.metaKey !== lastKey.m,
modChange = event.ctrlKey + event.altKey + event.shiftKey + event.metaKey - lastKey.c - lastKey.a - lastKey.s - lastKey.m,
key = new Key();
key.c = event.ctrlKey;
key.a = event.altKey;
key.s = event.shiftKey;
key.m = event.metaKey;
lastKey = key;
if (anyModChange) {
if (modChange < 0) {
return null;
}
key.code = oldKey.code;
key.char = oldKey.char;
} else {
key.code = event.which;
key.char = convertKeyCodeToChar(key.code);
}
return oldKey = key;
}
/**
Returns the string that represents the given key-code.
@param code The key-code.
@returns String Representation of the given key-code.
*/
function convertKeyCodeToChar(code) {
code = +code;
if (code === 0) {
return '';
} else if (code >= 48 && code <= 90) {
return String.fromCharCode(code).toUpperCase();
} else if (code >= 112 && code <= 123) {
return "F" + (code - 111);
} else {
return keyMap[code] || ("#" + code);
}
}
/**
Returns a string to identify a Key-Object.
@param key The Key-Object that should get identified.
@param human Whether to show 'Enter a key' when key-char is empty.
@param short Whether to shorten modification-names to first character.
@param separator The separator between modification-names and key-char.
@returns String The string to identify the given key-object the given way.
*/
function getKeyString(key, human, short, separator) {
var str = '';
if (!(key instanceof Key)) {
return str;
}
if (!key.char) {
if (human) {
return 'Enter a key';
} else {
return '';
}
}
if (!separator || /CtrlAShifMea#/.test(separator)) {
separator = human ? ' + ' : '+';
}
if (key.c) {
str += (short ? 'C' : 'Ctrl') + separator;
}
if (key.a) {
str += (short ? 'A' : 'Alt') + separator;
}
if (key.s) {
str += (short ? 'S' : 'Shift') + separator;
}
if (key.m) {
str += (short ? 'M' : 'Meta') + separator;
}
return str + (human ? key.char : key.code ? '#' + key.code : '');
}
/**
Parses the given string into a Key-Object.
@param str The string to parse.
@returns Key The Key-Object that got identified by the given string.
*/
function getKeyFromString(str) {
if (str instanceof Key) {
return str;
}
var key = new Key(),
sep = /([^CtrlAShifMea#\d]+)(?:#|\d)/.exec(str),
parts = sep != null ? str.split(sep[1]) : [str];
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
switch (part) {
case 'C':
case 'Ctrl':
key.c = true;
break;
case 'A':
case 'Alt':
key.a = true;
break;
case 'S':
case 'Shift':
key.s = true;
break;
case 'M':
case 'Meta':
key.m = true;
break;
default:
var num = /\d+/.exec(part);
if (num != null) {
key.code = num[0];
}
key.char = convertKeyCodeToChar(key.code);
}
}
return key;
}
function handleEvent(element, event) {
event = event || window.event;
event.which = event.which || event.keyCode || event.key;
var key = getKey(event);
if (key != null) {
SettingsKey.set(element, key);
}
}
SettingsKey = {
types: ['key'],
use: function () {
helper = (Settings = this).helper;
},
init: function (element) {
element.focus(function () {
oldKey = element.data('keyData') || new Key();
lastKey = new Key();
}).keydown(function (event) {
event.preventDefault();
handleEvent(element, event);
}).keyup(function (event) {
handleEvent(element, event);
});
return element;
},
set: function (element, value) {
var key = getKeyFromString(value || '');
element.data('keyData', key);
if (key.code) {
element.removeClass('alert-danger');
} else {
element.addClass('alert-danger');
}
element.val(getKeyString(key, true, false, ' + '));
},
get: function (element, trim, empty) {
var key = element.data('keyData'),
separator = element.data('split') || element.data('separator') || '+',
short = !helper.isFalse(element.data('short'));
if (trim) {
if (empty || (key != null && key.char)) {
return getKeyString(key, false, short, separator);
} else {
return void 0;
}
} else if (empty || (key != null && key.code)) {
return key;
} else {
return void 0;
}
}
};
return SettingsKey;
});

@ -0,0 +1,49 @@
define(function () {
var Settings = null,
SettingsSelect;
function addOptions(element, options) {
for (var i = 0; i < options.length; i++) {
var optionData = options[i],
value = optionData.text || optionData.value;
delete optionData.text;
element.append($(Settings.helper.createElement('option', optionData)).text(value));
}
}
SettingsSelect = {
types: ['select'],
use: function () {
Settings = this;
},
create: function (ignore, ignored, data) {
var element = $(Settings.helper.createElement('select'));
// prevent data-options from being attached to DOM
addOptions(element, data['data-options']);
delete data['data-options'];
return element;
},
init: function (element) {
var options = element.data('options');
if (options != null) {
addOptions(element, options);
}
},
set: function (element, value) {
element.val(value || '');
},
get: function (element, ignored, empty) {
var value = element.val();
if (empty || value) {
return value;
} else {
return void 0;
}
}
};
return SettingsSelect;
});

@ -0,0 +1,35 @@
define(function () {
var Settings = null,
SettingsArea;
SettingsArea = {
types: ['textarea'],
use: function () {
Settings = this;
},
create: function () {
return Settings.helper.createElement('textarea');
},
set: function (element, value, trim) {
if (trim && value != null && typeof value.trim === 'function') {
value = value.trim();
}
element.val(value || '');
},
get: function (element, trim, empty) {
var value = element.val();
if (trim) {
value = value == null ? void 0 : value.trim();
}
if (empty || value) {
return value;
} else {
return void 0;
}
}
};
return SettingsArea;
});

@ -0,0 +1,233 @@
var meta = require('./meta');
function expandObjBy(obj1, obj2) {
var key, val1, val2;
for (key in obj2) {
val2 = obj2[key];
val1 = obj1[key];
if (!obj1.hasOwnProperty(key) || typeof val2 !== typeof val1) {
obj1[key] = val2;
} else if (typeof val2 === 'object') {
expandObjBy(val1, val2);
}
}
}
function trim(obj1, obj2) {
var key, val1;
for (key in obj1) {
val1 = obj1[key];
if (!obj2.hasOwnProperty(key)) {
delete obj1[key];
} else if (typeof val1 === 'object') {
trim(val1, obj2[key]);
}
}
}
function mergeSettings(cfg, defCfg) {
if (typeof cfg._settings !== typeof defCfg || typeof defCfg !== 'object') {
return cfg._settings = defCfg;
} else {
expandObjBy(cfg._settings, defCfg);
trim(cfg._settings, defCfg);
}
}
/**
A class to manage Objects saved in {@link meta.settings} within property "_settings".
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.
@param defCfg The default settings.
@param callback Gets called once the Settings-object is ready.
@param forceUpdate Whether to trigger structure-update even if the version doesn't differ from saved one.
Should be true while plugin-development to ensure structure-changes within settings persist.
@param reset Whether to reset the settings.
*/
function Settings(hash, version, defCfg, callback, forceUpdate, reset) {
this.hash = hash;
this.version = version || this.version;
this.defCfg = defCfg;
if (reset) {
this.reset(callback);
} else {
this.sync(function () {
this.checkStructure(callback, forceUpdate);
});
}
}
Settings.prototype.hash = '';
Settings.prototype.defCfg = {};
Settings.prototype.cfg = {};
Settings.prototype.version = '0.0.0';
/**
Synchronizes the local object with the saved object (reverts changes).
@param callback Gets called when done.
*/
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);
}
} catch (_error) {}
_this.cfg = settings;
if (typeof callback === 'function') {
callback.apply(_this, err);
}
});
};
/**
Persists the local object.
@param callback Gets called when done.
*/
Settings.prototype.persist = function (callback) {
var conf = this.cfg._settings,
_this = this;
if (typeof conf === 'object') {
conf = JSON.stringify(conf);
}
meta.settings.set(this.hash, {
_settings: conf,
version: this.cfg.version
}, function () {
if (typeof callback === 'function') {
callback.apply(_this, arguments || []);
}
});
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.
@param def The default value, if not set global default value gets used.
@returns Object The setting to be used.
*/
Settings.prototype.get = function (key, def) {
var obj = this.cfg._settings,
parts = (key || '').split('.'),
part;
for (var i = 0; i < parts.length; i++) {
part = parts[i];
if (part && obj != null) {
obj = obj[part];
}
}
if (obj === void 0) {
if (def === void 0) {
def = this.defCfg;
for (var j = 0; j < parts.length; j++) {
part = parts[j];
if (part && def != null) {
def = def[part];
}
}
}
return def;
}
return obj;
};
/**
Returns the settings-wrapper object.
@returns Object The settings-wrapper.
*/
Settings.prototype.getWrapper = function () {
return this.cfg;
};
/**
Creates a new wrapper for the given settings with the given version.
@returns Object The new settings-wrapper.
*/
Settings.prototype.createWrapper = function (version, settings) {
return {
version: version,
_settings: settings
};
};
/**
Creates a new wrapper for the default settings.
@returns Object The new settings-wrapper.
*/
Settings.prototype.createDefaultWrapper = function () {
return this.createWrapper(this.version, this.defCfg);
};
/**
Sets the setting of given key to given value.
@param key The key of the setting to set.
@param val The value to set.
*/
Settings.prototype.set = function (key, val) {
var part, obj, parts;
this.cfg.version = this.version;
if (val == null || !key) {
this.cfg._settings = val || key;
} else {
obj = this.cfg._settings;
parts = key.split('.');
for (var i = 0, _len = parts.length - 1; i < _len; i++) {
if (part = parts[i]) {
if (!obj.hasOwnProperty(part)) {
obj[part] = {};
}
obj = obj[part];
}
}
obj[parts[parts.length - 1]] = val;
}
return this;
};
/**
Resets the saved settings to default settings.
@param callback Gets called when done.
*/
Settings.prototype.reset = function (callback) {
this.set(this.defCfg).persist(callback);
return this;
};
/**
If the version differs the settings get updated and persisted.
@param callback Gets called when done.
@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 (typeof callback === 'function') {
callback();
}
} else {
mergeSettings(this.cfg, this.defCfg);
this.cfg.version = this.version;
this.persist(callback);
}
return this;
};
module.exports = Settings;
Loading…
Cancel
Save