pageflow 15.1.0.beta4 → 15.1.0.beta5
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pageflow might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -1
- data/entry_types/scrolled/package/editor.js +315 -0
- data/entry_types/scrolled/package/frontend.js +2892 -0
- data/entry_types/scrolled/package/package.json +42 -0
- data/lib/pageflow/version.rb +1 -1
- data/packages/pageflow/config/jest/index.js +27 -0
- data/packages/pageflow/config/jest/transformers/cssModules.js +1 -0
- data/packages/pageflow/config/jest/transformers/jst.js +8 -0
- data/packages/pageflow/config/jest/transformers/upwardBabel.js +5 -0
- data/packages/pageflow/config/webpack.js +15 -0
- data/packages/pageflow/editor.js +8465 -0
- data/packages/pageflow/package.json +27 -0
- data/packages/pageflow/testHelpers.js +268 -0
- data/packages/pageflow/ui.js +2708 -0
- metadata +13 -1
@@ -0,0 +1,27 @@
|
|
1
|
+
{
|
2
|
+
"name": "pageflow",
|
3
|
+
"version": "15.1.0",
|
4
|
+
"description": "Multimedia storytelling for the web",
|
5
|
+
"module": "dist/index.js",
|
6
|
+
"repository": "https://github.com/codevise/pageflow",
|
7
|
+
"author": "Codevise Solutions GmbH <info@codevise.de>",
|
8
|
+
"license": "MIT",
|
9
|
+
"devDependencies": {
|
10
|
+
"babel-jest": "^24.9.0",
|
11
|
+
"eslint": "^6.6.0",
|
12
|
+
"eslint-import-resolver-jest": "^3.0.0",
|
13
|
+
"eslint-plugin-import": "^2.18.2",
|
14
|
+
"eslint-plugin-jest": "^23.0.4",
|
15
|
+
"jest": "^24.9.0",
|
16
|
+
"jest-jquery-matchers": "^2.1.0",
|
17
|
+
"jest-sinon": "^1.0.0",
|
18
|
+
"sinon": "^7.5.0"
|
19
|
+
},
|
20
|
+
"scripts": {
|
21
|
+
"test": "jest",
|
22
|
+
"lint": "eslint ."
|
23
|
+
},
|
24
|
+
"dependencies": {
|
25
|
+
"core-js": "^3.4.1"
|
26
|
+
}
|
27
|
+
}
|
@@ -0,0 +1,268 @@
|
|
1
|
+
import Backbone from 'backbone';
|
2
|
+
import _ from 'underscore';
|
3
|
+
import { Entry, Theme, FileTypes, FilesCollection, SubsetCollection, ImageFile, WidgetTypes, EditorApi, VideoFile, TextTrackFile } from 'pageflow/editor';
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Build editor Backbone models for tests.
|
7
|
+
*/
|
8
|
+
|
9
|
+
var factories = {
|
10
|
+
/**
|
11
|
+
* Build an entry model.
|
12
|
+
*
|
13
|
+
* @param {Function} model - Entry type specific entry model
|
14
|
+
* @param {Object} [attributes] - Model attributes
|
15
|
+
* @param {Object} [options]
|
16
|
+
* @param {Object} [options.entryTypeSeed] - Seed data passed to `Entry#setupFromEntryTypeSeed`.
|
17
|
+
* @param {FileTypes} [options.fileTypes] - Use {@link #factoriesfiletypes factories.fileTypes} to construct this object.
|
18
|
+
* @param {Object} [options.filesAttributes] - An object mapping (underscored) file collection names to arrays of file attributes.
|
19
|
+
* @returns {Entry} - An entry Backbone model.
|
20
|
+
*
|
21
|
+
* @example
|
22
|
+
*
|
23
|
+
* import {factories} from 'pageflow/testHelpers';
|
24
|
+
* import {PagedEntry} from 'editor/models/PagedEntry';
|
25
|
+
*
|
26
|
+
* const entry = factories.entry(PagedEntry, {slug: 'some-entry'}, {
|
27
|
+
* entryTypeSeed: {some: 'data'},
|
28
|
+
* fileTypes: factories.fileTypes(f => f.withImageFileType()),
|
29
|
+
* filesAttributes: {
|
30
|
+
* image_files: [{id: 100, perma_id: 1, basename: 'image'}]
|
31
|
+
* }
|
32
|
+
* });
|
33
|
+
*/
|
34
|
+
entry: function entry(model, attributes) {
|
35
|
+
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
36
|
+
|
37
|
+
if (typeof model !== 'function') {
|
38
|
+
return factories.entry(Entry, model, attributes);
|
39
|
+
}
|
40
|
+
|
41
|
+
ensureFileTypes(options);
|
42
|
+
ensureFilesCollections(options);
|
43
|
+
var entry = new model(attributes, _.extend({
|
44
|
+
storylines: new Backbone.Collection(),
|
45
|
+
chapters: new Backbone.Collection()
|
46
|
+
}, options));
|
47
|
+
|
48
|
+
if (entry.setupFromEntryTypeSeed && options.entryTypeSeed) {
|
49
|
+
entry.setupFromEntryTypeSeed(options.entryTypeSeed);
|
50
|
+
}
|
51
|
+
|
52
|
+
return entry;
|
53
|
+
},
|
54
|
+
theme: function theme(attributes, options) {
|
55
|
+
return new Theme(attributes, options);
|
56
|
+
},
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Construct a file type registry that can be passed to {@link
|
60
|
+
* #factoriesentry factories.entry}.
|
61
|
+
*
|
62
|
+
* The passed function receives a builder object with the following
|
63
|
+
* methods that register a corresponding file type:
|
64
|
+
*
|
65
|
+
* - `withImageFileType([options])`: Registers a file type with collection name `image_files`.
|
66
|
+
* - `withVideoFileType([options])`: Registers a file type with collection name `video_files`.
|
67
|
+
* - `withTextTrackFileType([options])`: Registers a file type with collection name `text_track_files`.
|
68
|
+
*
|
69
|
+
* @param {Function} fn - Build function.
|
70
|
+
* @returns {FileTypes} - A file Type registry
|
71
|
+
*/
|
72
|
+
fileTypes: function fileTypes(fn) {
|
73
|
+
var fileTypes = new FileTypes();
|
74
|
+
var fileTypesSetupArray = [];
|
75
|
+
var builder = {
|
76
|
+
withImageFileType: function withImageFileType(options) {
|
77
|
+
fileTypes.register('image_files', _.extend({
|
78
|
+
model: ImageFile,
|
79
|
+
matchUpload: /^image/,
|
80
|
+
topLevelType: true
|
81
|
+
}, options));
|
82
|
+
fileTypesSetupArray.push({
|
83
|
+
collectionName: 'image_files',
|
84
|
+
typeName: 'Pageflow::ImageFile',
|
85
|
+
i18nKey: 'pageflow/image_files'
|
86
|
+
});
|
87
|
+
return this;
|
88
|
+
},
|
89
|
+
withVideoFileType: function withVideoFileType(options) {
|
90
|
+
fileTypes.register('video_files', _.extend({
|
91
|
+
model: VideoFile,
|
92
|
+
matchUpload: /^video/,
|
93
|
+
topLevelType: true
|
94
|
+
}, options));
|
95
|
+
fileTypesSetupArray.push({
|
96
|
+
collectionName: 'video_files',
|
97
|
+
typeName: 'Pageflow::VideoFile',
|
98
|
+
i18nKey: 'pageflow/video_files',
|
99
|
+
nestedFileTypes: [{
|
100
|
+
collectionName: 'text_track_files'
|
101
|
+
}]
|
102
|
+
});
|
103
|
+
return this;
|
104
|
+
},
|
105
|
+
withTextTrackFileType: function withTextTrackFileType(options) {
|
106
|
+
fileTypes.register('text_track_files', _.extend({
|
107
|
+
model: TextTrackFile,
|
108
|
+
matchUpload: /vtt$/
|
109
|
+
}, options));
|
110
|
+
fileTypesSetupArray.push({
|
111
|
+
collectionName: 'text_track_files',
|
112
|
+
typeName: 'Pageflow::TextTrackFile',
|
113
|
+
i18nKey: 'pageflow/text_track_files'
|
114
|
+
});
|
115
|
+
return this;
|
116
|
+
}
|
117
|
+
};
|
118
|
+
fn.call(builder, builder);
|
119
|
+
fileTypes.setup(fileTypesSetupArray);
|
120
|
+
return fileTypes;
|
121
|
+
},
|
122
|
+
|
123
|
+
/**
|
124
|
+
* Shorthand for calling {@link #factoriesfiletypes
|
125
|
+
* factories.fileTypes} with a builder function that calls
|
126
|
+
* `withImageFileType`.
|
127
|
+
*
|
128
|
+
* @param {Object} options - File type options passed to withImageFileType,
|
129
|
+
* @returns {FileTypes} - A file Type registry.
|
130
|
+
*/
|
131
|
+
fileTypesWithImageFileType: function fileTypesWithImageFileType(options) {
|
132
|
+
return this.fileTypes(function () {
|
133
|
+
this.withImageFileType(options);
|
134
|
+
});
|
135
|
+
},
|
136
|
+
imageFileType: function imageFileType(options) {
|
137
|
+
return factories.fileTypesWithImageFileType(options).first();
|
138
|
+
},
|
139
|
+
fileType: function fileType(options) {
|
140
|
+
return factories.imageFileType(options);
|
141
|
+
},
|
142
|
+
filesCollection: function filesCollection(options) {
|
143
|
+
return FilesCollection.createForFileType(options.fileType, [{}, {}]);
|
144
|
+
},
|
145
|
+
nestedFilesCollection: function nestedFilesCollection(options) {
|
146
|
+
return new SubsetCollection({
|
147
|
+
parentModel: factories.file({
|
148
|
+
file_name: options.parentFileName
|
149
|
+
}),
|
150
|
+
filter: function filter() {
|
151
|
+
return true;
|
152
|
+
},
|
153
|
+
parent: factories.filesCollection({
|
154
|
+
fileType: options.fileType
|
155
|
+
})
|
156
|
+
});
|
157
|
+
},
|
158
|
+
videoFileWithTextTrackFiles: function videoFileWithTextTrackFiles(options) {
|
159
|
+
var fileTypes = this.fileTypes(function () {
|
160
|
+
this.withVideoFileType(options.videoFileTypeOptions);
|
161
|
+
this.withTextTrackFileType(options.textTrackFileTypeOptions);
|
162
|
+
});
|
163
|
+
var fileAttributes = {
|
164
|
+
video_files: [_.extend({
|
165
|
+
id: 1,
|
166
|
+
state: 'encoded'
|
167
|
+
}, options.videoFileAttributes)],
|
168
|
+
text_track_files: _.map(options.textTrackFilesAttributes, function (attributes) {
|
169
|
+
return _.extend({
|
170
|
+
parent_file_id: 1,
|
171
|
+
parent_file_model_type: 'Pageflow::VideoFile'
|
172
|
+
}, attributes);
|
173
|
+
})
|
174
|
+
};
|
175
|
+
var entry = factories.entry({}, {
|
176
|
+
files: FilesCollection.createForFileTypes(fileTypes, fileAttributes || {}),
|
177
|
+
fileTypes: fileTypes
|
178
|
+
});
|
179
|
+
var videoFiles = entry.getFileCollection(fileTypes.findByCollectionName('video_files'));
|
180
|
+
var textTrackFiles = entry.getFileCollection(fileTypes.findByCollectionName('text_track_files'));
|
181
|
+
return {
|
182
|
+
entry: entry,
|
183
|
+
videoFile: videoFiles.first(),
|
184
|
+
videoFiles: videoFiles,
|
185
|
+
textTrackFiles: textTrackFiles
|
186
|
+
};
|
187
|
+
},
|
188
|
+
imageFilesFixture: function imageFilesFixture(options) {
|
189
|
+
var fileTypes = this.fileTypes(function () {
|
190
|
+
this.withImageFileType(options.fileTypeOptions);
|
191
|
+
});
|
192
|
+
var fileAttributes = {
|
193
|
+
image_files: [_.extend({
|
194
|
+
id: 1,
|
195
|
+
state: 'processed'
|
196
|
+
}, options.imageFileAttributes)]
|
197
|
+
};
|
198
|
+
var entry = factories.entry({}, {
|
199
|
+
files: FilesCollection.createForFileTypes(fileTypes, fileAttributes || {}),
|
200
|
+
fileTypes: fileTypes
|
201
|
+
});
|
202
|
+
var imageFiles = entry.getFileCollection(fileTypes.findByCollectionName('image_files'));
|
203
|
+
return {
|
204
|
+
entry: entry,
|
205
|
+
imageFile: imageFiles.first(),
|
206
|
+
imageFiles: imageFiles
|
207
|
+
};
|
208
|
+
},
|
209
|
+
imageFile: function imageFile(attributes, options) {
|
210
|
+
return new ImageFile(attributes, _.extend({
|
211
|
+
fileType: this.imageFileType()
|
212
|
+
}, options));
|
213
|
+
},
|
214
|
+
file: function file(attributes, options) {
|
215
|
+
return this.imageFile(attributes, options);
|
216
|
+
},
|
217
|
+
widgetTypes: function widgetTypes(attributesList, beforeSetup) {
|
218
|
+
var widgetTypes = new WidgetTypes();
|
219
|
+
var attributesListsByRole = {};
|
220
|
+
|
221
|
+
_(attributesList).each(function (attributes) {
|
222
|
+
attributesListsByRole[attributes.role] = attributesListsByRole[attributes.role] || [];
|
223
|
+
attributesListsByRole[attributes.role].push(_.extend({
|
224
|
+
translationKey: 'widget_name.' + attributes.name
|
225
|
+
}, attributes));
|
226
|
+
});
|
227
|
+
|
228
|
+
if (beforeSetup) {
|
229
|
+
beforeSetup(widgetTypes);
|
230
|
+
}
|
231
|
+
|
232
|
+
widgetTypes.setup(attributesListsByRole);
|
233
|
+
return widgetTypes;
|
234
|
+
},
|
235
|
+
editorApi: function editorApi(beforeSetup) {
|
236
|
+
var api = new EditorApi();
|
237
|
+
|
238
|
+
if (beforeSetup) {
|
239
|
+
beforeSetup(api);
|
240
|
+
}
|
241
|
+
|
242
|
+
api.pageTypes.setup(_.map(api.pageTypes.clientSideConfigs, function (config, name) {
|
243
|
+
return {
|
244
|
+
name: name,
|
245
|
+
translation_key_prefix: 'pageflow.' + name,
|
246
|
+
translation_key: 'pageflow.' + name + '.name',
|
247
|
+
category_translation_key: 'pageflow.' + name + '.category',
|
248
|
+
description_translation_key: 'pageflow.' + name + '.description'
|
249
|
+
};
|
250
|
+
}));
|
251
|
+
return api;
|
252
|
+
}
|
253
|
+
};
|
254
|
+
|
255
|
+
function ensureFileTypes(options) {
|
256
|
+
if (!options.fileTypes) {
|
257
|
+
options.fileTypes = new FileTypes();
|
258
|
+
options.fileTypes.setup([]);
|
259
|
+
}
|
260
|
+
}
|
261
|
+
|
262
|
+
function ensureFilesCollections(options) {
|
263
|
+
if (!options.files) {
|
264
|
+
options.files = FilesCollection.createForFileTypes(options.fileTypes, options.filesAttributes);
|
265
|
+
}
|
266
|
+
}
|
267
|
+
|
268
|
+
export { factories };
|
@@ -0,0 +1,2708 @@
|
|
1
|
+
import Marionette from 'backbone.marionette';
|
2
|
+
import _ from 'underscore';
|
3
|
+
import $ from 'jquery';
|
4
|
+
import I18n$1 from 'i18n-js';
|
5
|
+
import Backbone from 'backbone';
|
6
|
+
import ChildViewContainer from 'backbone.babysitter';
|
7
|
+
import IScroll from 'iscroll';
|
8
|
+
import 'jquery.minicolors';
|
9
|
+
import wysihtml5 from 'wysihtml5';
|
10
|
+
import Cocktail from 'cocktail';
|
11
|
+
|
12
|
+
/*global JST*/
|
13
|
+
|
14
|
+
Marionette.Renderer.render = function (template, data) {
|
15
|
+
if (_.isFunction(template)) {
|
16
|
+
return template(data);
|
17
|
+
}
|
18
|
+
|
19
|
+
if (template.indexOf('templates/') === 0) {
|
20
|
+
template = 'pageflow/editor/' + template;
|
21
|
+
}
|
22
|
+
|
23
|
+
if (!JST[template]) {
|
24
|
+
throw "Template '" + template + "' not found!";
|
25
|
+
}
|
26
|
+
|
27
|
+
return JST[template](data);
|
28
|
+
};
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Returns an array of translation keys based on the `prefixes`
|
32
|
+
* option and the given `keyName`.
|
33
|
+
*
|
34
|
+
* @param {string} keyName
|
35
|
+
* Suffix to append to prefixes.
|
36
|
+
*
|
37
|
+
* @param {string[]} [options.prefixes]
|
38
|
+
* Array of translation key prefixes.
|
39
|
+
*
|
40
|
+
* @param {string} [options.fallbackPrefix]
|
41
|
+
* Optional additional prefix to form a model based translation
|
42
|
+
* key of the form
|
43
|
+
* `prefix.fallbackModelI18nKey.propertyName.keyName`.
|
44
|
+
*
|
45
|
+
* @param {string} [options.fallbackModelI18nKey]
|
46
|
+
* Required if `fallbackPrefix` option is present.
|
47
|
+
*
|
48
|
+
* @return {string[]}
|
49
|
+
* @memberof i18nUtils
|
50
|
+
* @since 12.0
|
51
|
+
*/
|
52
|
+
|
53
|
+
function attributeTranslationKeys(attributeName, keyName, options) {
|
54
|
+
var result = [];
|
55
|
+
|
56
|
+
if (options.prefixes) {
|
57
|
+
result = result.concat(_(options.prefixes).map(function (prefix) {
|
58
|
+
return prefix + '.' + attributeName + '.' + keyName;
|
59
|
+
}, this));
|
60
|
+
}
|
61
|
+
|
62
|
+
if (options && options.fallbackPrefix) {
|
63
|
+
result.push(options.fallbackPrefix + '.' + options.fallbackModelI18nKey + '.' + attributeName);
|
64
|
+
}
|
65
|
+
|
66
|
+
return result;
|
67
|
+
}
|
68
|
+
/**
|
69
|
+
* Takes the same parameters as {@link
|
70
|
+
* #i18nutilsattributetranslationkeys attributeTranslationKeys}, but returns the first existing
|
71
|
+
* translation.
|
72
|
+
*
|
73
|
+
* @return {string}
|
74
|
+
* @memberof i18nUtils
|
75
|
+
* @since 12.0
|
76
|
+
*/
|
77
|
+
|
78
|
+
function attributeTranslation(attributeName, keyName, options) {
|
79
|
+
return findTranslation(attributeTranslationKeys(attributeName, keyName, options));
|
80
|
+
}
|
81
|
+
/**
|
82
|
+
* Find the first key for which a translation exists and return the
|
83
|
+
* translation.
|
84
|
+
*
|
85
|
+
* @param {string[]} keys
|
86
|
+
* Translation key candidates.
|
87
|
+
*
|
88
|
+
* @param {string} [options.defaultValue]
|
89
|
+
* Value to return if none of the keys has a translation. Is
|
90
|
+
* treated like an HTML translation if html flag is set.
|
91
|
+
*
|
92
|
+
* @param {boolean} [options.html]
|
93
|
+
* If true, also search for keys ending in '_html' and HTML-escape
|
94
|
+
* keys that do not end in 'html'
|
95
|
+
*
|
96
|
+
* @memberof i18nUtils
|
97
|
+
* @return {string}
|
98
|
+
*/
|
99
|
+
|
100
|
+
function findTranslation(keys, options) {
|
101
|
+
options = options || {};
|
102
|
+
|
103
|
+
if (options.html) {
|
104
|
+
keys = translationKeysWithSuffix(keys, 'html');
|
105
|
+
}
|
106
|
+
|
107
|
+
return _.chain(keys).reverse().reduce(function (result, key) {
|
108
|
+
var unescapedTranslation = I18n$1.t(key, _.extend({}, options, {
|
109
|
+
defaultValue: result
|
110
|
+
}));
|
111
|
+
|
112
|
+
if (!options.html || key.match(/_html$/) || result == unescapedTranslation) {
|
113
|
+
return unescapedTranslation;
|
114
|
+
} else {
|
115
|
+
return $('<div />').text(unescapedTranslation).html();
|
116
|
+
}
|
117
|
+
}, options.defaultValue).value();
|
118
|
+
}
|
119
|
+
/**
|
120
|
+
* Return the first key for which a translation exists. Returns the
|
121
|
+
* first if non of the keys has a translation.
|
122
|
+
*
|
123
|
+
* @param {string[]} keys
|
124
|
+
* Translation key candidates.
|
125
|
+
*
|
126
|
+
* @memberof i18nUtils
|
127
|
+
* @return {string}
|
128
|
+
*/
|
129
|
+
|
130
|
+
function findKeyWithTranslation(keys) {
|
131
|
+
var missing = '_not_translated';
|
132
|
+
return _(keys).detect(function (key) {
|
133
|
+
return I18n$1.t(key, {
|
134
|
+
defaultValue: missing
|
135
|
+
}) !== missing;
|
136
|
+
}) || _.first(keys);
|
137
|
+
}
|
138
|
+
function translationKeysWithSuffix(keys, suffix) {
|
139
|
+
return _.chain(keys).map(function (key) {
|
140
|
+
return [key + '_' + suffix, key];
|
141
|
+
}).flatten().value();
|
142
|
+
}
|
143
|
+
|
144
|
+
var i18nUtils = /*#__PURE__*/Object.freeze({
|
145
|
+
__proto__: null,
|
146
|
+
attributeTranslationKeys: attributeTranslationKeys,
|
147
|
+
attributeTranslation: attributeTranslation,
|
148
|
+
findTranslation: findTranslation,
|
149
|
+
findKeyWithTranslation: findKeyWithTranslation,
|
150
|
+
translationKeysWithSuffix: translationKeysWithSuffix
|
151
|
+
});
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Create object that can be passed to Marionette ui property from CSS
|
155
|
+
* module object.
|
156
|
+
*
|
157
|
+
* @param {Object} styles
|
158
|
+
* Class name mapping imported from `.module.css` file.
|
159
|
+
*
|
160
|
+
* @param {...string} classNames
|
161
|
+
* Keys from the styles object that shall be used in the ui object.
|
162
|
+
*
|
163
|
+
* @return {Object}
|
164
|
+
*
|
165
|
+
* @example
|
166
|
+
*
|
167
|
+
* // MyView.module.css
|
168
|
+
*
|
169
|
+
* .container {}
|
170
|
+
*
|
171
|
+
* // MyView.js
|
172
|
+
*
|
173
|
+
* import Marionette from 'marionette';
|
174
|
+
* import {cssModulesUtils} from 'pageflow/ui';
|
175
|
+
*
|
176
|
+
* import styles from './MyView.module.css';
|
177
|
+
*
|
178
|
+
* export const MyView = Marionette.ItemView({
|
179
|
+
* template: () => `
|
180
|
+
* <div class=${styles.container}></div>
|
181
|
+
* `,
|
182
|
+
*
|
183
|
+
* ui: cssModulesUtils.ui(styles, 'container');
|
184
|
+
*
|
185
|
+
* onRender() {
|
186
|
+
* this.ui.container // => JQuery wrapper for container element
|
187
|
+
* }
|
188
|
+
* });
|
189
|
+
*
|
190
|
+
* @memberof cssModulesUtils
|
191
|
+
*/
|
192
|
+
function ui(styles) {
|
193
|
+
for (var _len = arguments.length, classNames = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
194
|
+
classNames[_key - 1] = arguments[_key];
|
195
|
+
}
|
196
|
+
|
197
|
+
return classNames.reduce(function (result, className) {
|
198
|
+
result[className] = ".".concat(styles[className]);
|
199
|
+
return result;
|
200
|
+
}, {});
|
201
|
+
}
|
202
|
+
|
203
|
+
var cssModulesUtils = /*#__PURE__*/Object.freeze({
|
204
|
+
__proto__: null,
|
205
|
+
ui: ui
|
206
|
+
});
|
207
|
+
|
208
|
+
// https://github.com/jashkenas/backbone/issues/2601
|
209
|
+
|
210
|
+
function BaseObject(options) {
|
211
|
+
this.initialize.apply(this, arguments);
|
212
|
+
}
|
213
|
+
|
214
|
+
_.extend(BaseObject.prototype, Backbone.Events, {
|
215
|
+
initialize: function initialize(options) {}
|
216
|
+
}); // The self-propagating extend function that Backbone classes use.
|
217
|
+
|
218
|
+
|
219
|
+
BaseObject.extend = Backbone.Model.extend;
|
220
|
+
|
221
|
+
var CollectionView = Marionette.View.extend({
|
222
|
+
initialize: function initialize() {
|
223
|
+
this.rendered = false;
|
224
|
+
this.itemViews = new ChildViewContainer();
|
225
|
+
this.collection.map(this.addItem, this);
|
226
|
+
this.listenTo(this.collection, 'add', this.addItem);
|
227
|
+
this.listenTo(this.collection, 'remove', this.removeItem);
|
228
|
+
this.listenTo(this.collection, 'sort', this.sort);
|
229
|
+
|
230
|
+
if (this.options.loadingViewConstructor) {
|
231
|
+
this.listenTo(this.collection, 'request', function () {
|
232
|
+
this.loading = true;
|
233
|
+
this.togglePlaceHolder();
|
234
|
+
});
|
235
|
+
this.listenTo(this.collection, 'sync', function () {
|
236
|
+
this.loading = false;
|
237
|
+
this.togglePlaceHolder();
|
238
|
+
});
|
239
|
+
}
|
240
|
+
},
|
241
|
+
render: function render() {
|
242
|
+
if (!this.rendered) {
|
243
|
+
this.$el.append(this.itemViews.map(function (itemView) {
|
244
|
+
itemView.$el.data('view', itemView);
|
245
|
+
return itemView.render().el;
|
246
|
+
}));
|
247
|
+
this.togglePlaceHolder();
|
248
|
+
this.rendered = true;
|
249
|
+
}
|
250
|
+
|
251
|
+
return this;
|
252
|
+
},
|
253
|
+
onClose: function onClose() {
|
254
|
+
this.itemViews.call('close');
|
255
|
+
this.closePlaceHolderView();
|
256
|
+
},
|
257
|
+
addItem: function addItem(item) {
|
258
|
+
var view = new this.options.itemViewConstructor(_.extend({
|
259
|
+
model: item
|
260
|
+
}, this.getItemViewOptions(item)));
|
261
|
+
this.itemViews.add(view);
|
262
|
+
|
263
|
+
if (this.rendered) {
|
264
|
+
var index = this.collection.indexOf(item);
|
265
|
+
view.render();
|
266
|
+
view.$el.data('view', view);
|
267
|
+
|
268
|
+
if (index > 0) {
|
269
|
+
this.$el.children().eq(index - 1).after(view.el);
|
270
|
+
} else {
|
271
|
+
this.$el.prepend(view.el);
|
272
|
+
}
|
273
|
+
|
274
|
+
this.togglePlaceHolder();
|
275
|
+
}
|
276
|
+
},
|
277
|
+
removeItem: function removeItem(item) {
|
278
|
+
var view = this.itemViews.findByModel(item);
|
279
|
+
|
280
|
+
if (view) {
|
281
|
+
this.itemViews.remove(view);
|
282
|
+
view.close();
|
283
|
+
this.togglePlaceHolder();
|
284
|
+
}
|
285
|
+
},
|
286
|
+
sort: function sort() {
|
287
|
+
var last = null;
|
288
|
+
this.collection.each(function (item) {
|
289
|
+
var itemView = this.itemViews.findByModel(item);
|
290
|
+
var element;
|
291
|
+
|
292
|
+
if (!itemView) {
|
293
|
+
return;
|
294
|
+
}
|
295
|
+
|
296
|
+
element = itemView.$el;
|
297
|
+
|
298
|
+
if (last) {
|
299
|
+
last.after(element);
|
300
|
+
} else {
|
301
|
+
this.$el.prepend(element);
|
302
|
+
}
|
303
|
+
|
304
|
+
last = element;
|
305
|
+
}, this);
|
306
|
+
},
|
307
|
+
getItemViewOptions: function getItemViewOptions(item) {
|
308
|
+
if (typeof this.options.itemViewOptions === 'function') {
|
309
|
+
return this.options.itemViewOptions(item);
|
310
|
+
} else {
|
311
|
+
return this.options.itemViewOptions || {};
|
312
|
+
}
|
313
|
+
},
|
314
|
+
closePlaceHolderView: function closePlaceHolderView() {
|
315
|
+
if (this.placeHolderView) {
|
316
|
+
this.placeHolderView.close();
|
317
|
+
this.placeHolderView = null;
|
318
|
+
}
|
319
|
+
},
|
320
|
+
togglePlaceHolder: function togglePlaceHolder() {
|
321
|
+
var lastPlaceholderConstructor = this.placeHolderConstructor;
|
322
|
+
this.placeHolderConstructor = this.getPlaceHolderConstructor();
|
323
|
+
|
324
|
+
if (this.itemViews.length || !this.placeHolderConstructor) {
|
325
|
+
this.closePlaceHolderView();
|
326
|
+
} else if (!this.placeHolderView || lastPlaceholderConstructor !== this.placeHolderConstructor) {
|
327
|
+
this.closePlaceHolderView();
|
328
|
+
this.placeHolderView = new this.placeHolderConstructor();
|
329
|
+
this.$el.append(this.placeHolderView.render().el);
|
330
|
+
}
|
331
|
+
},
|
332
|
+
getPlaceHolderConstructor: function getPlaceHolderConstructor() {
|
333
|
+
if (this.loading && this.options.loadingViewConstructor) {
|
334
|
+
return this.options.loadingViewConstructor;
|
335
|
+
} else if (this.options.blankSlateViewConstructor) {
|
336
|
+
return this.options.blankSlateViewConstructor;
|
337
|
+
}
|
338
|
+
}
|
339
|
+
});
|
340
|
+
|
341
|
+
var SortableCollectionView = CollectionView.extend({
|
342
|
+
render: function render() {
|
343
|
+
CollectionView.prototype.render.call(this);
|
344
|
+
this.$el.sortable({
|
345
|
+
connectWith: this.options.connectWith,
|
346
|
+
placeholder: 'sortable-placeholder',
|
347
|
+
forcePlaceholderSize: true,
|
348
|
+
delay: 200,
|
349
|
+
update: _.bind(function (event, ui) {
|
350
|
+
if (ui.item.parent().is(this.el)) {
|
351
|
+
this.updateOrder();
|
352
|
+
}
|
353
|
+
}, this),
|
354
|
+
receive: _.bind(function (event, ui) {
|
355
|
+
var view = ui.item.data('view');
|
356
|
+
this.reindexPositions();
|
357
|
+
this.itemViews.add(view);
|
358
|
+
this.collection.add(view.model);
|
359
|
+
}, this),
|
360
|
+
remove: _.bind(function (event, ui) {
|
361
|
+
var view = ui.item.data('view');
|
362
|
+
this.itemViews.remove(view);
|
363
|
+
this.collection.remove(view.model);
|
364
|
+
}, this)
|
365
|
+
});
|
366
|
+
return this;
|
367
|
+
},
|
368
|
+
addItem: function addItem(item) {
|
369
|
+
if (!this.itemViews.findByModel(item)) {
|
370
|
+
CollectionView.prototype.addItem.call(this, item);
|
371
|
+
}
|
372
|
+
},
|
373
|
+
removeItem: function removeItem(item) {
|
374
|
+
if (this.itemViews.findByModel(item)) {
|
375
|
+
CollectionView.prototype.removeItem.call(this, item);
|
376
|
+
}
|
377
|
+
},
|
378
|
+
updateOrder: function updateOrder() {
|
379
|
+
this.reindexPositions();
|
380
|
+
this.collection.sort();
|
381
|
+
this.collection.saveOrder();
|
382
|
+
},
|
383
|
+
reindexPositions: function reindexPositions() {
|
384
|
+
this.$el.children().each(function (index) {
|
385
|
+
$(this).data('view').model.set('position', index);
|
386
|
+
});
|
387
|
+
}
|
388
|
+
});
|
389
|
+
|
390
|
+
var ConfigurationEditorTabView = Marionette.View.extend({
|
391
|
+
className: 'configuration_editor_tab',
|
392
|
+
initialize: function initialize() {
|
393
|
+
this.inputs = new ChildViewContainer();
|
394
|
+
this.groups = this.options.groups || ConfigurationEditorTabView.groups;
|
395
|
+
},
|
396
|
+
input: function input(propertyName, view, options) {
|
397
|
+
this.view(view, _.extend({
|
398
|
+
placeholderModel: this.options.placeholderModel,
|
399
|
+
propertyName: propertyName,
|
400
|
+
attributeTranslationKeyPrefixes: this.options.attributeTranslationKeyPrefixes
|
401
|
+
}, options || {}));
|
402
|
+
},
|
403
|
+
view: function view(_view, options) {
|
404
|
+
this.inputs.add(new _view(_.extend({
|
405
|
+
model: this.model,
|
406
|
+
parentTab: this.options.tab
|
407
|
+
}, options || {})));
|
408
|
+
},
|
409
|
+
group: function group(name, options) {
|
410
|
+
this.groups.apply(name, this, options);
|
411
|
+
},
|
412
|
+
render: function render() {
|
413
|
+
this.inputs.each(function (input) {
|
414
|
+
this.$el.append(input.render().el);
|
415
|
+
}, this);
|
416
|
+
return this;
|
417
|
+
},
|
418
|
+
onClose: function onClose() {
|
419
|
+
if (this.inputs) {
|
420
|
+
this.inputs.call('close');
|
421
|
+
}
|
422
|
+
}
|
423
|
+
});
|
424
|
+
|
425
|
+
ConfigurationEditorTabView.Groups = function () {
|
426
|
+
var groups = {};
|
427
|
+
|
428
|
+
this.define = function (name, fn) {
|
429
|
+
if (typeof fn !== 'function') {
|
430
|
+
throw 'Group has to be function.';
|
431
|
+
}
|
432
|
+
|
433
|
+
groups[name] = fn;
|
434
|
+
};
|
435
|
+
|
436
|
+
this.apply = function (name, context, options) {
|
437
|
+
if (!(name in groups)) {
|
438
|
+
throw 'Undefined group named "' + name + '".';
|
439
|
+
}
|
440
|
+
|
441
|
+
groups[name].call(context, options || {});
|
442
|
+
};
|
443
|
+
};
|
444
|
+
|
445
|
+
ConfigurationEditorTabView.groups = new ConfigurationEditorTabView.Groups();
|
446
|
+
|
447
|
+
function template(data) {
|
448
|
+
var __p = '';
|
449
|
+
__p += '<div class="tabs_view-scroller">\n <ul class="tabs_view-headers"></ul>\n</div>\n<div class="tabs_view-container"></div>\n';
|
450
|
+
return __p
|
451
|
+
}
|
452
|
+
|
453
|
+
/*global pageflow*/
|
454
|
+
/**
|
455
|
+
* Switch between different views using tabs.
|
456
|
+
*
|
457
|
+
* @param {Object} [options]
|
458
|
+
*
|
459
|
+
* @param {string} [options.defaultTab]
|
460
|
+
* Name of the tab to enable by default.
|
461
|
+
*
|
462
|
+
* @param {string[]} [options.translationKeyPrefixes]
|
463
|
+
* List of prefixes to append tab name to. First exisiting translation is used as label.
|
464
|
+
*
|
465
|
+
* @param {string} [options.fallbackTranslationKeyPrefix]
|
466
|
+
* Translation key prefix to use if non of the `translationKeyPrefixes` result in an
|
467
|
+
* existing translation for a tab name.
|
468
|
+
*
|
469
|
+
* @param {string} [options.i18n]
|
470
|
+
* Legacy alias for `fallbackTranslationKeyPrefix`.
|
471
|
+
*
|
472
|
+
* @class
|
473
|
+
*/
|
474
|
+
|
475
|
+
var TabsView = Marionette.Layout.extend(
|
476
|
+
/* @lends TabView.prototype */
|
477
|
+
{
|
478
|
+
template: template,
|
479
|
+
className: 'tabs_view',
|
480
|
+
ui: {
|
481
|
+
headers: '.tabs_view-headers',
|
482
|
+
scroller: '.tabs_view-scroller'
|
483
|
+
},
|
484
|
+
regions: {
|
485
|
+
container: '.tabs_view-container'
|
486
|
+
},
|
487
|
+
events: {
|
488
|
+
'click .tabs_view-headers > li': function clickTabs_viewHeadersLi(event) {
|
489
|
+
this.changeTab($(event.target).data('tab-name'));
|
490
|
+
}
|
491
|
+
},
|
492
|
+
initialize: function initialize() {
|
493
|
+
this.tabFactoryFns = {};
|
494
|
+
this.tabNames = [];
|
495
|
+
this.currentTabName = null;
|
496
|
+
|
497
|
+
this._refreshScrollerOnSideBarResize();
|
498
|
+
},
|
499
|
+
tab: function tab(name, factoryFn) {
|
500
|
+
this.tabFactoryFns[name] = factoryFn;
|
501
|
+
this.tabNames.push(name);
|
502
|
+
},
|
503
|
+
onRender: function onRender() {
|
504
|
+
_.each(this.tabNames, function (name) {
|
505
|
+
var label = findTranslation(this._labelTranslationKeys(name));
|
506
|
+
this.ui.headers.append($('<li />').attr('data-tab-name', name).text(label));
|
507
|
+
}, this);
|
508
|
+
|
509
|
+
this.scroller = new IScroll(this.ui.scroller[0], {
|
510
|
+
scrollX: true,
|
511
|
+
scrollY: false,
|
512
|
+
bounce: false,
|
513
|
+
mouseWheel: true,
|
514
|
+
preventDefault: false
|
515
|
+
});
|
516
|
+
this.changeTab(this.defaultTab());
|
517
|
+
},
|
518
|
+
changeTab: function changeTab(name) {
|
519
|
+
this.container.show(this.tabFactoryFns[name]());
|
520
|
+
|
521
|
+
this._updateActiveHeader(name);
|
522
|
+
|
523
|
+
this.currentTabName = name;
|
524
|
+
},
|
525
|
+
defaultTab: function defaultTab() {
|
526
|
+
if (_.include(this.tabNames, this.options.defaultTab)) {
|
527
|
+
return this.options.defaultTab;
|
528
|
+
} else {
|
529
|
+
return _.first(this.tabNames);
|
530
|
+
}
|
531
|
+
},
|
532
|
+
|
533
|
+
/**
|
534
|
+
* Rerender current tab.
|
535
|
+
*/
|
536
|
+
refresh: function refresh() {
|
537
|
+
this.changeTab(this.currentTabName);
|
538
|
+
},
|
539
|
+
|
540
|
+
/**
|
541
|
+
* Adjust tabs scroller to changed width of view.
|
542
|
+
*/
|
543
|
+
refreshScroller: function refreshScroller() {
|
544
|
+
this.scroller.refresh();
|
545
|
+
},
|
546
|
+
toggleSpinnerOnTab: function toggleSpinnerOnTab(name, visible) {
|
547
|
+
this.$('[data-tab-name=' + name + ']').toggleClass('spinner', visible);
|
548
|
+
},
|
549
|
+
_labelTranslationKeys: function _labelTranslationKeys(name) {
|
550
|
+
var result = _.map(this.options.translationKeyPrefixes, function (prefix) {
|
551
|
+
return prefix + '.' + name;
|
552
|
+
});
|
553
|
+
|
554
|
+
if (this.options.i18n) {
|
555
|
+
result.push(this.options.i18n + '.' + name);
|
556
|
+
}
|
557
|
+
|
558
|
+
if (this.options.fallbackTranslationKeyPrefix) {
|
559
|
+
result.push(this.options.fallbackTranslationKeyPrefix + '.' + name);
|
560
|
+
}
|
561
|
+
|
562
|
+
return result;
|
563
|
+
},
|
564
|
+
_updateActiveHeader: function _updateActiveHeader(activeTabName) {
|
565
|
+
var scroller = this.scroller;
|
566
|
+
this.ui.headers.children().each(function () {
|
567
|
+
if ($(this).data('tab-name') === activeTabName) {
|
568
|
+
scroller.scrollToElement(this, 200, true);
|
569
|
+
$(this).addClass('active');
|
570
|
+
} else {
|
571
|
+
$(this).removeClass('active');
|
572
|
+
}
|
573
|
+
});
|
574
|
+
},
|
575
|
+
_refreshScrollerOnSideBarResize: function _refreshScrollerOnSideBarResize() {
|
576
|
+
if (pageflow.app) {
|
577
|
+
this.listenTo(pageflow.app, 'resize', function () {
|
578
|
+
this.scroller.refresh();
|
579
|
+
});
|
580
|
+
}
|
581
|
+
}
|
582
|
+
});
|
583
|
+
|
584
|
+
/**
|
585
|
+
* Render a inputs on multiple tabs.
|
586
|
+
*
|
587
|
+
* @param {Object} [options]
|
588
|
+
*
|
589
|
+
* @param {string} [options.model]
|
590
|
+
* Backbone model to use for input views.
|
591
|
+
*
|
592
|
+
* @param {string} [options.placeholderModel]
|
593
|
+
* Backbone model to read placeholder values from.
|
594
|
+
|
595
|
+
* @param {string} [options.tab]
|
596
|
+
* Name of the tab to enable by default.
|
597
|
+
*
|
598
|
+
* @param {string[]} [options.attributeTranslationKeyPrefixes]
|
599
|
+
* List of prefixes to use in input views for attribute based transltions.
|
600
|
+
*
|
601
|
+
* @param {string[]} [options.tabTranslationKeyPrefixes]
|
602
|
+
* List of prefixes to append tab name to. First exisiting translation is used as label.
|
603
|
+
*
|
604
|
+
* @param {string} [options.tabTranslationKeyPrefix]
|
605
|
+
* Prefixes to append tab name to.
|
606
|
+
*
|
607
|
+
* @class
|
608
|
+
*/
|
609
|
+
|
610
|
+
var ConfigurationEditorView = Marionette.View.extend({
|
611
|
+
className: 'configuration_editor',
|
612
|
+
initialize: function initialize() {
|
613
|
+
this.tabsView = new TabsView({
|
614
|
+
translationKeyPrefixes: this.options.tabTranslationKeyPrefixes || [this.options.tabTranslationKeyPrefix],
|
615
|
+
fallbackTranslationKeyPrefix: 'pageflow.ui.configuration_editor.tabs',
|
616
|
+
defaultTab: this.options.tab
|
617
|
+
});
|
618
|
+
this.configure();
|
619
|
+
},
|
620
|
+
configure: function configure() {},
|
621
|
+
tab: function tab(name, callback) {
|
622
|
+
this.tabsView.tab(name, _.bind(function () {
|
623
|
+
var tabView = new ConfigurationEditorTabView({
|
624
|
+
model: this.model,
|
625
|
+
placeholderModel: this.options.placeholderModel,
|
626
|
+
tab: name,
|
627
|
+
attributeTranslationKeyPrefixes: this.options.attributeTranslationKeyPrefixes
|
628
|
+
});
|
629
|
+
callback.call(tabView);
|
630
|
+
return tabView;
|
631
|
+
}, this));
|
632
|
+
},
|
633
|
+
|
634
|
+
/**
|
635
|
+
* Rerender current tab.
|
636
|
+
*/
|
637
|
+
refresh: function refresh() {
|
638
|
+
this.tabsView.refresh();
|
639
|
+
},
|
640
|
+
|
641
|
+
/**
|
642
|
+
* Adjust tabs scroller to changed width of view.
|
643
|
+
*/
|
644
|
+
refreshScroller: function refreshScroller() {
|
645
|
+
this.tabsView.refreshScroller();
|
646
|
+
},
|
647
|
+
render: function render() {
|
648
|
+
this.$el.append(this.subview(this.tabsView).el);
|
649
|
+
return this;
|
650
|
+
}
|
651
|
+
});
|
652
|
+
|
653
|
+
_.extend(ConfigurationEditorView, {
|
654
|
+
repository: {},
|
655
|
+
register: function register(pageTypeName, prototype) {
|
656
|
+
this.repository[pageTypeName] = ConfigurationEditorView.extend(prototype);
|
657
|
+
}
|
658
|
+
});
|
659
|
+
|
660
|
+
function template$1(data) {
|
661
|
+
var __p = '';
|
662
|
+
__p += '';
|
663
|
+
return __p
|
664
|
+
}
|
665
|
+
|
666
|
+
/**
|
667
|
+
* Base class for table cell views.
|
668
|
+
*
|
669
|
+
* Inside sub classes the name of the column options are available as
|
670
|
+
* `this.options.column`. Override the `update` method to populate the
|
671
|
+
* element.
|
672
|
+
*
|
673
|
+
* @param {Object} [options]
|
674
|
+
*
|
675
|
+
* @param {string} [options.className]
|
676
|
+
* Class attribute to apply to the cell element.
|
677
|
+
*
|
678
|
+
* @since 12.0
|
679
|
+
*/
|
680
|
+
|
681
|
+
var TableCellView = Marionette.ItemView.extend({
|
682
|
+
tagName: 'td',
|
683
|
+
template: template$1,
|
684
|
+
className: function className() {
|
685
|
+
return this.options.className;
|
686
|
+
},
|
687
|
+
onRender: function onRender() {
|
688
|
+
this.listenTo(this.getModel(), 'change:' + this.options.column.name, this.update);
|
689
|
+
this.setupContentBinding();
|
690
|
+
this.update();
|
691
|
+
},
|
692
|
+
|
693
|
+
/**
|
694
|
+
* Override in concrete cell view.
|
695
|
+
*/
|
696
|
+
update: function update() {
|
697
|
+
throw 'Not implemented';
|
698
|
+
},
|
699
|
+
|
700
|
+
/**
|
701
|
+
* Returns the column attribute's value in the row model.
|
702
|
+
*/
|
703
|
+
attributeValue: function attributeValue() {
|
704
|
+
if (typeof this.options.column.value == 'function') {
|
705
|
+
return this.options.column.value(this.model);
|
706
|
+
} else {
|
707
|
+
return this.getModel().get(this.options.column.name);
|
708
|
+
}
|
709
|
+
},
|
710
|
+
getModel: function getModel() {
|
711
|
+
if (this.options.column.configurationAttribute) {
|
712
|
+
return this.model.configuration;
|
713
|
+
} else {
|
714
|
+
return this.model;
|
715
|
+
}
|
716
|
+
},
|
717
|
+
|
718
|
+
/**
|
719
|
+
* Look up attribute specific translations based on
|
720
|
+
* `attributeTranslationKeyPrefixes` of the the parent `TableView`.
|
721
|
+
*
|
722
|
+
* @param {Object} [options]
|
723
|
+
* Interpolations to apply to the translation.
|
724
|
+
*
|
725
|
+
* @param {string} [options.defaultValue]
|
726
|
+
* Fallback value if no translation is found.
|
727
|
+
*
|
728
|
+
* @protected
|
729
|
+
*
|
730
|
+
* @example
|
731
|
+
*
|
732
|
+
* this.attribute.attributeTranslation("cell_title");
|
733
|
+
* // Looks for keys of the form:
|
734
|
+
* // <table_view_translation_key_prefix>.<column_attribute>.cell_title
|
735
|
+
*/
|
736
|
+
attributeTranslation: function attributeTranslation(keyName, options) {
|
737
|
+
return findTranslation(this.attributeTranslationKeys(keyName), options);
|
738
|
+
},
|
739
|
+
attributeTranslationKeys: function attributeTranslationKeys(keyName) {
|
740
|
+
return _(this.options.attributeTranslationKeyPrefixes || []).map(function (prefix) {
|
741
|
+
return prefix + '.' + this.options.column.name + '.' + keyName;
|
742
|
+
}, this);
|
743
|
+
},
|
744
|
+
|
745
|
+
/**
|
746
|
+
* Set up content binding to update this view upon change of
|
747
|
+
* specified attribute on this.getModel().
|
748
|
+
*
|
749
|
+
* @param {string} [options.column.contentBinding]
|
750
|
+
* Name of the attribute to which this cell's update is bound
|
751
|
+
*
|
752
|
+
* @protected
|
753
|
+
*/
|
754
|
+
setupContentBinding: function setupContentBinding() {
|
755
|
+
if (this.options.column.contentBinding) {
|
756
|
+
this.listenTo(this.getModel(), 'change:' + this.options.column.contentBinding, this.update);
|
757
|
+
this.update();
|
758
|
+
}
|
759
|
+
}
|
760
|
+
});
|
761
|
+
|
762
|
+
var TableHeaderCellView = TableCellView.extend({
|
763
|
+
tagName: 'th',
|
764
|
+
render: function render() {
|
765
|
+
this.$el.text(this.attributeTranslation('column_header'));
|
766
|
+
this.$el.data('columnName', this.options.column.name);
|
767
|
+
return this;
|
768
|
+
}
|
769
|
+
});
|
770
|
+
|
771
|
+
var TableRowView = Marionette.View.extend({
|
772
|
+
tagName: 'tr',
|
773
|
+
events: {
|
774
|
+
'click': function click() {
|
775
|
+
if (this.options.selection) {
|
776
|
+
this.options.selection.set(this.selectionAttribute(), this.model);
|
777
|
+
}
|
778
|
+
}
|
779
|
+
},
|
780
|
+
initialize: function initialize() {
|
781
|
+
if (this.options.selection) {
|
782
|
+
this.listenTo(this.options.selection, 'change', this.updateClassName);
|
783
|
+
}
|
784
|
+
},
|
785
|
+
render: function render() {
|
786
|
+
_(this.options.columns).each(function (column) {
|
787
|
+
this.appendSubview(new column.cellView(_.extend({
|
788
|
+
model: this.model,
|
789
|
+
column: column,
|
790
|
+
attributeTranslationKeyPrefixes: this.options.attributeTranslationKeyPrefixes
|
791
|
+
}, column.cellViewOptions || {})));
|
792
|
+
}, this);
|
793
|
+
|
794
|
+
this.updateClassName();
|
795
|
+
return this;
|
796
|
+
},
|
797
|
+
updateClassName: function updateClassName() {
|
798
|
+
this.$el.toggleClass('is_selected', this.isSelected());
|
799
|
+
},
|
800
|
+
isSelected: function isSelected() {
|
801
|
+
return this.options.selection && this.options.selection.get(this.selectionAttribute()) === this.model;
|
802
|
+
},
|
803
|
+
selectionAttribute: function selectionAttribute() {
|
804
|
+
return this.options.selectionAttribute || 'current';
|
805
|
+
}
|
806
|
+
});
|
807
|
+
|
808
|
+
function template$2(data) {
|
809
|
+
var __p = '';
|
810
|
+
__p += '<table>\n <thead>\n <tr></tr>\n </thead>\n <tbody>\n </tbody>\n</table>\n';
|
811
|
+
return __p
|
812
|
+
}
|
813
|
+
|
814
|
+
function blankSlateTemplate(data) {
|
815
|
+
var __t, __p = '';
|
816
|
+
__p += '<td colspan="' +
|
817
|
+
((__t = ( data.colSpan )) == null ? '' : __t) +
|
818
|
+
'">\n ' +
|
819
|
+
((__t = ( data.blankSlateText )) == null ? '' : __t) +
|
820
|
+
'\n</td>\n';
|
821
|
+
return __p
|
822
|
+
}
|
823
|
+
|
824
|
+
var TableView = Marionette.ItemView.extend({
|
825
|
+
tagName: 'table',
|
826
|
+
className: 'table_view',
|
827
|
+
template: template$2,
|
828
|
+
ui: {
|
829
|
+
headRow: 'thead tr',
|
830
|
+
body: 'tbody'
|
831
|
+
},
|
832
|
+
onRender: function onRender() {
|
833
|
+
var view = this;
|
834
|
+
|
835
|
+
_(this.options.columns).each(function (column) {
|
836
|
+
this.ui.headRow.append(this.subview(new TableHeaderCellView({
|
837
|
+
column: column,
|
838
|
+
attributeTranslationKeyPrefixes: this.options.attributeTranslationKeyPrefixes
|
839
|
+
})).el);
|
840
|
+
}, this);
|
841
|
+
|
842
|
+
this.subview(new CollectionView({
|
843
|
+
el: this.ui.body,
|
844
|
+
collection: this.collection,
|
845
|
+
itemViewConstructor: TableRowView,
|
846
|
+
itemViewOptions: {
|
847
|
+
columns: this.options.columns,
|
848
|
+
selection: this.options.selection,
|
849
|
+
selectionAttribute: this.options.selectionAttribute,
|
850
|
+
attributeTranslationKeyPrefixes: this.options.attributeTranslationKeyPrefixes
|
851
|
+
},
|
852
|
+
blankSlateViewConstructor: Marionette.ItemView.extend({
|
853
|
+
tagName: 'tr',
|
854
|
+
className: 'blank_slate',
|
855
|
+
template: blankSlateTemplate,
|
856
|
+
serializeData: function serializeData() {
|
857
|
+
return {
|
858
|
+
blankSlateText: view.options.blankSlateText,
|
859
|
+
colSpan: view.options.columns.length
|
860
|
+
};
|
861
|
+
}
|
862
|
+
})
|
863
|
+
}));
|
864
|
+
}
|
865
|
+
});
|
866
|
+
|
867
|
+
function template$3(data) {
|
868
|
+
var __p = '';
|
869
|
+
__p += '<span class="label">\n</span>\n';
|
870
|
+
return __p
|
871
|
+
}
|
872
|
+
|
873
|
+
var TooltipView = Marionette.ItemView.extend({
|
874
|
+
template: template$3,
|
875
|
+
className: 'tooltip',
|
876
|
+
ui: {
|
877
|
+
label: '.label'
|
878
|
+
},
|
879
|
+
hide: function hide() {
|
880
|
+
this.visible = false;
|
881
|
+
clearTimeout(this.timeout);
|
882
|
+
this.$el.removeClass('visible');
|
883
|
+
},
|
884
|
+
show: function show(text, position, options) {
|
885
|
+
options = options || {};
|
886
|
+
this.visible = true;
|
887
|
+
clearTimeout(this.timeout);
|
888
|
+
this.timeout = setTimeout(_.bind(function () {
|
889
|
+
var offsetTop;
|
890
|
+
var offsetLeft;
|
891
|
+
this.ui.label.text(text);
|
892
|
+
this.$el.toggleClass('align_bottom_right', options.align === 'bottom right');
|
893
|
+
this.$el.toggleClass('align_bottom_left', options.align === 'bottom left');
|
894
|
+
|
895
|
+
if (options.align === 'bottom right' || options.align === 'bottom left') {
|
896
|
+
offsetTop = 10;
|
897
|
+
offsetLeft = 0;
|
898
|
+
} else {
|
899
|
+
offsetTop = -17;
|
900
|
+
offsetLeft = 10;
|
901
|
+
}
|
902
|
+
|
903
|
+
this.$el.css({
|
904
|
+
top: position.top + offsetTop + 'px',
|
905
|
+
left: position.left + offsetLeft + 'px'
|
906
|
+
});
|
907
|
+
this.$el.addClass('visible');
|
908
|
+
}, this), 200);
|
909
|
+
}
|
910
|
+
});
|
911
|
+
|
912
|
+
/**
|
913
|
+
* Mixin for input views handling common concerns like labels,
|
914
|
+
* inline help, visiblity and disabling.
|
915
|
+
*
|
916
|
+
* ## Label and Inline Help Translations
|
917
|
+
*
|
918
|
+
* By default `#labelText` and `#inlineHelpText` are defined through
|
919
|
+
* translations. If no `attributeTranslationKeyPrefixes` are given,
|
920
|
+
* translation keys for labels and inline help are constructed from
|
921
|
+
* the `i18nKey` of the model and the given `propertyName`
|
922
|
+
* option. Suppose the model's `i18nKey` is "page" and the
|
923
|
+
* `propertyName` option is "title". Then the key
|
924
|
+
*
|
925
|
+
* activerecord.attributes.page.title
|
926
|
+
*
|
927
|
+
* will be used for the label. And the key
|
928
|
+
*
|
929
|
+
* pageflow.ui.inline_help.page.title_html
|
930
|
+
* pageflow.ui.inline_help.page.title
|
931
|
+
*
|
932
|
+
* will be used for the inline help.
|
933
|
+
*
|
934
|
+
* ### Attribute Translation Key Prefixes
|
935
|
+
*
|
936
|
+
* The `attributeTranslationKeyPrefixes` option can be used to supply
|
937
|
+
* an array of scopes in which label and inline help translations
|
938
|
+
* shall be looked up based on the `propertyName` option.
|
939
|
+
*
|
940
|
+
* Suppose the array `['some.attributes', 'fallback.attributes']` is
|
941
|
+
* given as `attributeTranslationKeyPrefixes` option. Then, in the
|
942
|
+
* example above, the first existing translation key is used as label:
|
943
|
+
*
|
944
|
+
* some.attributes.title.label
|
945
|
+
* fallback.attributes.title.label
|
946
|
+
* activerecord.attributes.post.title
|
947
|
+
*
|
948
|
+
* Accordingly, for the inline help:
|
949
|
+
*
|
950
|
+
* some.attributes.title.inline_help_html
|
951
|
+
* some.attributes.title.inline_help
|
952
|
+
* fallback.attributes.title.inline_help_html
|
953
|
+
* fallback.attributes.title.inline_help
|
954
|
+
* pageflow.ui.inline_help.post.title_html
|
955
|
+
* pageflow.ui.inline_help.post.title
|
956
|
+
*
|
957
|
+
* This setup allows to keep all translation keys for an attribute
|
958
|
+
* to share a common prefix:
|
959
|
+
*
|
960
|
+
* some:
|
961
|
+
* attributes:
|
962
|
+
* title:
|
963
|
+
* label: "Label"
|
964
|
+
* inline_help: "..."
|
965
|
+
* inline_help_disabled: "..."
|
966
|
+
*
|
967
|
+
* ### Inline Help for Disabled Inputs
|
968
|
+
*
|
969
|
+
* For each inline help translation key, a separate key with an
|
970
|
+
* `"_disabled"` suffix can be supplied, which provides a help string
|
971
|
+
* that shall be displayed when the input is disabled. More specific
|
972
|
+
* attribute translation key prefixes take precedence over suffixed
|
973
|
+
* keys:
|
974
|
+
*
|
975
|
+
* some.attributes.title.inline_help_html
|
976
|
+
* some.attributes.title.inline_help
|
977
|
+
* some.attributes.title.inline_help_disabled_html
|
978
|
+
* some.attributes.title.inline_help_disabled
|
979
|
+
* fallback.attributes.title.inline_help_html
|
980
|
+
* fallback.attributes.title.inline_help
|
981
|
+
* fallback.attributes.title.inline_help_disabled_html
|
982
|
+
* fallback.attributes.title.inline_help_disabled
|
983
|
+
* pageflow.ui.inline_help.post.title_html
|
984
|
+
* pageflow.ui.inline_help.post.title
|
985
|
+
* pageflow.ui.inline_help.post.title_disabled_html
|
986
|
+
* pageflow.ui.inline_help.post.title_disabled
|
987
|
+
*
|
988
|
+
* @param {string} options
|
989
|
+
* Common constructor options for all views that include this mixin.
|
990
|
+
*
|
991
|
+
* @param {string} options.propertyName
|
992
|
+
* Name of the attribute on the model to display and edit.
|
993
|
+
*
|
994
|
+
* @param {string} [options.label]
|
995
|
+
* Label text for the input.
|
996
|
+
*
|
997
|
+
* @param {string[]} [options.attributeTranslationKeyPrefixes]
|
998
|
+
* An array of prefixes to lookup translations for labels and
|
999
|
+
* inline help texts based on attribute names.
|
1000
|
+
*
|
1001
|
+
* @param {string} [options.additionalInlineHelpText]
|
1002
|
+
* A text that will be appended to the translation based inline
|
1003
|
+
* text.
|
1004
|
+
*
|
1005
|
+
* @param {boolean} [options.disabled]
|
1006
|
+
* Render input as disabled.
|
1007
|
+
*
|
1008
|
+
* @param {string} [options.visibleBinding]
|
1009
|
+
* Name of an attribute to control whether the input is visible. If
|
1010
|
+
* the `visible` and `visibleBindingValue` options are not set,
|
1011
|
+
* input will be visible whenever this attribute has a truthy value.
|
1012
|
+
*
|
1013
|
+
* @param {function|boolean} [options.visible]
|
1014
|
+
* A Function taking the value of the `visibleBinding` attribute as
|
1015
|
+
* parameter. Input will be visible only if function returns `true`.
|
1016
|
+
*
|
1017
|
+
* @param {any} [options.visibleBindingValue]
|
1018
|
+
* Input will be visible whenever the value of the `visibleBinding`
|
1019
|
+
* attribute equals the value of this option.
|
1020
|
+
*
|
1021
|
+
* @mixin
|
1022
|
+
*/
|
1023
|
+
|
1024
|
+
var inputView = {
|
1025
|
+
ui: {
|
1026
|
+
labelText: 'label .name',
|
1027
|
+
inlineHelp: 'label .inline_help'
|
1028
|
+
},
|
1029
|
+
|
1030
|
+
/**
|
1031
|
+
* Returns an array of translation keys based on the
|
1032
|
+
* `attributeTranslationKeyPrefixes` option and the given keyName.
|
1033
|
+
*
|
1034
|
+
* Combined with {@link #i18nutils
|
1035
|
+
* i18nUtils.findTranslation}, this can be used inside input views
|
1036
|
+
* to obtain additional translations with the same logic as for
|
1037
|
+
* labels and inline help texts.
|
1038
|
+
*
|
1039
|
+
* findTranslation(this.attributeTranslationKeys('default_value'));
|
1040
|
+
*
|
1041
|
+
* @param {string} keyName
|
1042
|
+
* Suffix to append to prefixes.
|
1043
|
+
*
|
1044
|
+
* @param {string} [options.fallbackPrefix]
|
1045
|
+
* Optional additional prefix to form a model based translation
|
1046
|
+
* key of the form `prefix.modelI18nKey.propertyName.keyName
|
1047
|
+
*
|
1048
|
+
* @return {string[]}
|
1049
|
+
* @since 0.9
|
1050
|
+
* @member
|
1051
|
+
*/
|
1052
|
+
attributeTranslationKeys: function attributeTranslationKeys$1(keyName, options) {
|
1053
|
+
return attributeTranslationKeys(this.options.propertyName, keyName, _.extend({
|
1054
|
+
prefixes: this.options.attributeTranslationKeyPrefixes,
|
1055
|
+
fallbackModelI18nKey: this.model.i18nKey
|
1056
|
+
}, options || {}));
|
1057
|
+
},
|
1058
|
+
onRender: function onRender() {
|
1059
|
+
this.$el.addClass('input');
|
1060
|
+
this.$el.addClass(this.model.modelName + '_' + this.options.propertyName);
|
1061
|
+
this.$el.data('inputPropertyName', this.options.propertyName);
|
1062
|
+
this.ui.labelText.text(this.labelText());
|
1063
|
+
this.ui.inlineHelp.html(this.inlineHelpText());
|
1064
|
+
|
1065
|
+
if (!this.inlineHelpText()) {
|
1066
|
+
this.ui.inlineHelp.hide();
|
1067
|
+
}
|
1068
|
+
|
1069
|
+
this.updateDisabled();
|
1070
|
+
this.setupVisibleBinding();
|
1071
|
+
},
|
1072
|
+
|
1073
|
+
/**
|
1074
|
+
* The label to display in the form.
|
1075
|
+
* @return {string}
|
1076
|
+
*/
|
1077
|
+
labelText: function labelText() {
|
1078
|
+
return this.options.label || this.localizedAttributeName();
|
1079
|
+
},
|
1080
|
+
localizedAttributeName: function localizedAttributeName() {
|
1081
|
+
return findTranslation(this.attributeTranslationKeys('label', {
|
1082
|
+
fallbackPrefix: 'activerecord.attributes'
|
1083
|
+
}));
|
1084
|
+
},
|
1085
|
+
|
1086
|
+
/**
|
1087
|
+
* The inline help text for the form field.
|
1088
|
+
* @return {string}
|
1089
|
+
*/
|
1090
|
+
inlineHelpText: function inlineHelpText() {
|
1091
|
+
var keys = this.attributeTranslationKeys('inline_help', {
|
1092
|
+
fallbackPrefix: 'pageflow.ui.inline_help'
|
1093
|
+
});
|
1094
|
+
|
1095
|
+
if (this.options.disabled) {
|
1096
|
+
keys = translationKeysWithSuffix(keys, 'disabled');
|
1097
|
+
}
|
1098
|
+
|
1099
|
+
return _.compact([findTranslation(keys, {
|
1100
|
+
defaultValue: '',
|
1101
|
+
html: true
|
1102
|
+
}), this.options.additionalInlineHelpText]).join(' ');
|
1103
|
+
},
|
1104
|
+
updateDisabled: function updateDisabled() {
|
1105
|
+
if (this.ui.input) {
|
1106
|
+
this.updateDisabledAttribute(this.ui.input);
|
1107
|
+
}
|
1108
|
+
},
|
1109
|
+
updateDisabledAttribute: function updateDisabledAttribute(element) {
|
1110
|
+
if (this.options.disabled) {
|
1111
|
+
element.attr('disabled', true);
|
1112
|
+
} else {
|
1113
|
+
element.removeAttr('disabled');
|
1114
|
+
}
|
1115
|
+
},
|
1116
|
+
setupVisibleBinding: function setupVisibleBinding() {
|
1117
|
+
var view = this;
|
1118
|
+
|
1119
|
+
if (this.options.visibleBinding) {
|
1120
|
+
this.listenTo(this.model, 'change:' + this.options.visibleBinding, updateVisible);
|
1121
|
+
updateVisible(this.model, this.model.get(this.options.visibleBinding));
|
1122
|
+
}
|
1123
|
+
|
1124
|
+
function updateVisible(model, value) {
|
1125
|
+
view.$el.toggleClass('input-hidden_via_binding', !isVisible(value));
|
1126
|
+
}
|
1127
|
+
|
1128
|
+
function isVisible(value) {
|
1129
|
+
if ('visibleBindingValue' in view.options) {
|
1130
|
+
return value === view.options.visibleBindingValue;
|
1131
|
+
} else if (typeof view.options.visible === 'function') {
|
1132
|
+
return !!view.options.visible(value);
|
1133
|
+
} else if ('visible' in view.options) {
|
1134
|
+
return !!view.options.visible;
|
1135
|
+
} else {
|
1136
|
+
return !!value;
|
1137
|
+
}
|
1138
|
+
}
|
1139
|
+
}
|
1140
|
+
};
|
1141
|
+
|
1142
|
+
function template$4(data) {
|
1143
|
+
var __p = '';
|
1144
|
+
__p += '<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>\n<div class="check_boxes_container" />\n';
|
1145
|
+
return __p
|
1146
|
+
}
|
1147
|
+
|
1148
|
+
/**
|
1149
|
+
* Input view for attributes storing configuration hashes with boolean values.
|
1150
|
+
* See {@link inputView} for further options.
|
1151
|
+
*
|
1152
|
+
* @param {Object} [options]
|
1153
|
+
*
|
1154
|
+
* @class
|
1155
|
+
*/
|
1156
|
+
|
1157
|
+
var CheckBoxGroupInputView = Marionette.ItemView.extend({
|
1158
|
+
mixins: [inputView],
|
1159
|
+
template: template$4,
|
1160
|
+
className: 'check_box_group_input',
|
1161
|
+
events: {
|
1162
|
+
'change': 'save'
|
1163
|
+
},
|
1164
|
+
ui: {
|
1165
|
+
label: 'label',
|
1166
|
+
container: '.check_boxes_container'
|
1167
|
+
},
|
1168
|
+
initialize: function initialize() {
|
1169
|
+
if (!this.options.texts) {
|
1170
|
+
if (!this.options.translationKeys) {
|
1171
|
+
var translationKeyPrefix = this.options.translationKeyPrefix || findKeyWithTranslation(this.attributeTranslationKeys('values', {
|
1172
|
+
fallbackPrefix: 'activerecord.values'
|
1173
|
+
}));
|
1174
|
+
this.options.translationKeys = _.map(this.options.values, function (value) {
|
1175
|
+
return translationKeyPrefix + '.' + value;
|
1176
|
+
}, this);
|
1177
|
+
}
|
1178
|
+
|
1179
|
+
this.options.texts = _.map(this.options.translationKeys, function (key) {
|
1180
|
+
return I18n$1.t(key);
|
1181
|
+
});
|
1182
|
+
}
|
1183
|
+
},
|
1184
|
+
onRender: function onRender() {
|
1185
|
+
this.ui.label.attr('for', this.cid);
|
1186
|
+
this.appendOptions();
|
1187
|
+
this.load();
|
1188
|
+
this.listenTo(this.model, 'change:' + this.options.propertyName, this.load);
|
1189
|
+
},
|
1190
|
+
appendOptions: function appendOptions() {
|
1191
|
+
_.each(this.options.values, function (value, index) {
|
1192
|
+
var option = '<div class="check_box">' + '<label><input type="checkbox" name="' + value + '" />' + this.options.texts[index] + '</label></div>';
|
1193
|
+
this.ui.container.append($(option));
|
1194
|
+
}, this);
|
1195
|
+
},
|
1196
|
+
save: function save() {
|
1197
|
+
var configured = {};
|
1198
|
+
|
1199
|
+
_.each(this.ui.container.find('input'), function (input) {
|
1200
|
+
configured[$(input).attr('name')] = $(input).prop('checked');
|
1201
|
+
});
|
1202
|
+
|
1203
|
+
this.model.set(this.options.propertyName, configured);
|
1204
|
+
},
|
1205
|
+
load: function load() {
|
1206
|
+
if (!this.isClosed) {
|
1207
|
+
_.each(this.options.values, function (value) {
|
1208
|
+
this.ui.container.find('input[name="' + value + '"]').prop('checked', this.model.get(this.options.propertyName)[value]);
|
1209
|
+
}, this);
|
1210
|
+
}
|
1211
|
+
}
|
1212
|
+
});
|
1213
|
+
|
1214
|
+
function template$5(data) {
|
1215
|
+
var __t, __p = '';
|
1216
|
+
__p += '<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>\n<a class="original" href="#" download target="_blank">\n ' +
|
1217
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.url_display.link_text') )) == null ? '' : __t) +
|
1218
|
+
'\n</a>\n';
|
1219
|
+
return __p
|
1220
|
+
}
|
1221
|
+
|
1222
|
+
/**
|
1223
|
+
* Display view for a link to a URL, to be used like an input view.
|
1224
|
+
* See {@link inputView} for further options
|
1225
|
+
*
|
1226
|
+
* @param {Object} [options]
|
1227
|
+
*
|
1228
|
+
* @param {string} [options.propertyName]
|
1229
|
+
* Target URL for link
|
1230
|
+
*
|
1231
|
+
* @class
|
1232
|
+
*/
|
1233
|
+
|
1234
|
+
var UrlDisplayView = Marionette.ItemView.extend({
|
1235
|
+
mixins: [inputView],
|
1236
|
+
template: template$5,
|
1237
|
+
ui: {
|
1238
|
+
link: 'a'
|
1239
|
+
},
|
1240
|
+
modelEvents: {
|
1241
|
+
'change': 'update'
|
1242
|
+
},
|
1243
|
+
events: {
|
1244
|
+
'click a': function clickA(event) {
|
1245
|
+
// Ensure default is not prevented by parent event listener.
|
1246
|
+
event.stopPropagation();
|
1247
|
+
}
|
1248
|
+
},
|
1249
|
+
onRender: function onRender() {
|
1250
|
+
this.update();
|
1251
|
+
},
|
1252
|
+
update: function update() {
|
1253
|
+
var url = this.model.get('original_url');
|
1254
|
+
this.$el.toggle(this.model.isUploaded() && !_.isEmpty(url));
|
1255
|
+
this.ui.link.attr('href', url);
|
1256
|
+
}
|
1257
|
+
});
|
1258
|
+
|
1259
|
+
/**
|
1260
|
+
* Text based input view that can display a placeholder.
|
1261
|
+
*
|
1262
|
+
* @param {Object} [options]
|
1263
|
+
*
|
1264
|
+
* @param {string|function} [options.placeholder]
|
1265
|
+
* Display a placeholder string if the input is blank. Either a
|
1266
|
+
* string or a function taking the model as a first parameter and
|
1267
|
+
* returning a string.
|
1268
|
+
*
|
1269
|
+
* @param {string} [options.placeholderBinding]
|
1270
|
+
* Name of an attribute. Recompute the placeholder function whenever
|
1271
|
+
* this attribute changes.
|
1272
|
+
*
|
1273
|
+
* @param {boolean} [options.hidePlaceholderIfDisabled]
|
1274
|
+
* Do not display the placeholder if the input is disabled.
|
1275
|
+
*
|
1276
|
+
* @param {Backbone.Model} [options.placeholderModel]
|
1277
|
+
* Obtain placeholder by looking up the configured `propertyName`
|
1278
|
+
* inside a given model.
|
1279
|
+
*/
|
1280
|
+
var inputWithPlaceholderText = {
|
1281
|
+
onRender: function onRender() {
|
1282
|
+
this.updatePlaceholder();
|
1283
|
+
|
1284
|
+
if (this.options.placeholderBinding) {
|
1285
|
+
this.listenTo(this.model, 'change:' + this.options.placeholderBinding, this.updatePlaceholder);
|
1286
|
+
}
|
1287
|
+
},
|
1288
|
+
updatePlaceholder: function updatePlaceholder() {
|
1289
|
+
this.ui.input.attr('placeholder', this.placeholderText());
|
1290
|
+
},
|
1291
|
+
placeholderText: function placeholderText() {
|
1292
|
+
if (!this.options.disabled || !this.options.hidePlaceholderIfDisabled) {
|
1293
|
+
if (this.options.placeholder) {
|
1294
|
+
if (typeof this.options.placeholder == 'function') {
|
1295
|
+
return this.options.placeholder(this.model);
|
1296
|
+
} else {
|
1297
|
+
return this.options.placeholder;
|
1298
|
+
}
|
1299
|
+
} else {
|
1300
|
+
return this.placeholderModelValue();
|
1301
|
+
}
|
1302
|
+
}
|
1303
|
+
},
|
1304
|
+
placeholderModelValue: function placeholderModelValue() {
|
1305
|
+
return this.options.placeholderModel && this.options.placeholderModel.get(this.options.propertyName);
|
1306
|
+
}
|
1307
|
+
};
|
1308
|
+
|
1309
|
+
function template$6(data) {
|
1310
|
+
var __p = '';
|
1311
|
+
__p += '<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>\n<input type="text" dir="auto" />\n';
|
1312
|
+
return __p
|
1313
|
+
}
|
1314
|
+
|
1315
|
+
/**
|
1316
|
+
* Input view for a single line of text.
|
1317
|
+
*
|
1318
|
+
* See {@link inputWithPlaceholderText} for placeholder related
|
1319
|
+
* further options. See {@link inputView} for further options.
|
1320
|
+
*
|
1321
|
+
* @param {Object} [options]
|
1322
|
+
*
|
1323
|
+
* @param {boolean} [options.required=false]
|
1324
|
+
* Display an error if the input is blank.
|
1325
|
+
*
|
1326
|
+
* @param {number} [options.maxLength=255]
|
1327
|
+
* Maximum length of characters for this input. To support legacy
|
1328
|
+
* data which consists of more characters than the specified
|
1329
|
+
* maxLength, the option will only take effect for data which is
|
1330
|
+
* shorter than the specified maxLength.
|
1331
|
+
*
|
1332
|
+
* @class
|
1333
|
+
*/
|
1334
|
+
|
1335
|
+
var TextInputView = Marionette.ItemView.extend({
|
1336
|
+
mixins: [inputView, inputWithPlaceholderText],
|
1337
|
+
template: template$6,
|
1338
|
+
ui: {
|
1339
|
+
input: 'input'
|
1340
|
+
},
|
1341
|
+
events: {
|
1342
|
+
'change': 'onChange'
|
1343
|
+
},
|
1344
|
+
onRender: function onRender() {
|
1345
|
+
this.load();
|
1346
|
+
this.validate();
|
1347
|
+
this.listenTo(this.model, 'change:' + this.options.propertyName, this.load);
|
1348
|
+
},
|
1349
|
+
onChange: function onChange() {
|
1350
|
+
if (this.validate()) {
|
1351
|
+
this.save();
|
1352
|
+
}
|
1353
|
+
},
|
1354
|
+
onClose: function onClose() {
|
1355
|
+
if (this.validate()) {
|
1356
|
+
this.save();
|
1357
|
+
}
|
1358
|
+
},
|
1359
|
+
save: function save() {
|
1360
|
+
this.model.set(this.options.propertyName, this.ui.input.val());
|
1361
|
+
},
|
1362
|
+
load: function load() {
|
1363
|
+
var input = this.ui.input;
|
1364
|
+
input.val(this.model.get(this.options.propertyName)); // set mysql varchar length as default for non-legacy data
|
1365
|
+
|
1366
|
+
this.options.maxLength = this.options.maxLength || 255; // do not validate legacy data which length exceeds the specified maximum
|
1367
|
+
// for new and maxLength-conforming data: add validation
|
1368
|
+
|
1369
|
+
this.validateMaxLength = input.val().length <= this.options.maxLength;
|
1370
|
+
},
|
1371
|
+
validate: function validate() {
|
1372
|
+
var input = this.ui.input;
|
1373
|
+
|
1374
|
+
if (this.options.required && !input.val()) {
|
1375
|
+
this.displayValidationError(I18n$1.t('pageflow.ui.views.inputs.text_input_view.required_field'));
|
1376
|
+
return false;
|
1377
|
+
}
|
1378
|
+
|
1379
|
+
if (this.validateMaxLength && input.val().length > this.options.maxLength) {
|
1380
|
+
this.displayValidationError(I18n$1.t('pageflow.ui.views.inputs.text_input_view.max_characters_exceeded', {
|
1381
|
+
max_length: this.options.maxLength
|
1382
|
+
}));
|
1383
|
+
return false;
|
1384
|
+
} else {
|
1385
|
+
this.resetValidationError();
|
1386
|
+
return true;
|
1387
|
+
}
|
1388
|
+
},
|
1389
|
+
displayValidationError: function displayValidationError(message) {
|
1390
|
+
this.$el.addClass('invalid');
|
1391
|
+
this.ui.input.attr('title', message);
|
1392
|
+
},
|
1393
|
+
resetValidationError: function resetValidationError(message) {
|
1394
|
+
this.$el.removeClass('invalid');
|
1395
|
+
this.ui.input.attr('title', '');
|
1396
|
+
}
|
1397
|
+
});
|
1398
|
+
|
1399
|
+
/**
|
1400
|
+
* Input view for a color value in hex representation.
|
1401
|
+
* See {@link inputView} for further options
|
1402
|
+
*
|
1403
|
+
* @param {Object} [options]
|
1404
|
+
*
|
1405
|
+
* @param {string|function} [options.defaultValue]
|
1406
|
+
* Color value to display by default. The corresponding value is not
|
1407
|
+
* stored in the model. Selecting the default value when a different
|
1408
|
+
* value was set before, unsets the attribute in the model.
|
1409
|
+
*
|
1410
|
+
* @param {string} [options.defaultValueBinding]
|
1411
|
+
* Name of an attribute the default value depends on. If a function
|
1412
|
+
* is used as defaultValue option, it will be passed the value of the
|
1413
|
+
* defaultValueBinding attribute each time it changes. If no
|
1414
|
+
* defaultValue option is set, the value of the defaultValueBinding
|
1415
|
+
* attribute will be used as default value.
|
1416
|
+
*
|
1417
|
+
* @param {string[]} [options.swatches]
|
1418
|
+
* Preset color values to be displayed inside the picker drop
|
1419
|
+
* down. The default value, if present, is always used as the
|
1420
|
+
* first swatch automatically.
|
1421
|
+
*
|
1422
|
+
* @class
|
1423
|
+
*/
|
1424
|
+
|
1425
|
+
var ColorInputView = Marionette.ItemView.extend({
|
1426
|
+
mixins: [inputView],
|
1427
|
+
template: template$6,
|
1428
|
+
className: 'color_input',
|
1429
|
+
ui: {
|
1430
|
+
input: 'input'
|
1431
|
+
},
|
1432
|
+
events: {
|
1433
|
+
'mousedown': 'refreshPicker'
|
1434
|
+
},
|
1435
|
+
onRender: function onRender() {
|
1436
|
+
this.ui.input.minicolors({
|
1437
|
+
changeDelay: 200,
|
1438
|
+
change: _.bind(function (color) {
|
1439
|
+
if (color === this.defaultValue()) {
|
1440
|
+
this.model.unset(this.options.propertyName);
|
1441
|
+
} else {
|
1442
|
+
this.model.set(this.options.propertyName, color);
|
1443
|
+
}
|
1444
|
+
}, this)
|
1445
|
+
});
|
1446
|
+
this.listenTo(this.model, 'change:' + this.options.propertyName, this.load);
|
1447
|
+
|
1448
|
+
if (this.options.defaultValueBinding) {
|
1449
|
+
this.listenTo(this.model, 'change:' + this.options.defaultValueBinding, this.updateSettings);
|
1450
|
+
}
|
1451
|
+
|
1452
|
+
this.updateSettings();
|
1453
|
+
},
|
1454
|
+
updateSettings: function updateSettings() {
|
1455
|
+
this.ui.input.minicolors('settings', {
|
1456
|
+
defaultValue: this.defaultValue(),
|
1457
|
+
swatches: this.getSwatches()
|
1458
|
+
});
|
1459
|
+
this.load();
|
1460
|
+
},
|
1461
|
+
load: function load() {
|
1462
|
+
this.ui.input.minicolors('value', this.model.get(this.options.propertyName) || this.defaultValue());
|
1463
|
+
this.$el.toggleClass('is_default', !this.model.has(this.options.propertyName));
|
1464
|
+
},
|
1465
|
+
refreshPicker: function refreshPicker() {
|
1466
|
+
this.ui.input.minicolors('value', {});
|
1467
|
+
},
|
1468
|
+
getSwatches: function getSwatches() {
|
1469
|
+
return _.chain([this.defaultValue(), this.options.swatches]).flatten().uniq().compact().value();
|
1470
|
+
},
|
1471
|
+
defaultValue: function defaultValue() {
|
1472
|
+
var bindingValue;
|
1473
|
+
|
1474
|
+
if (this.options.defaultValueBinding) {
|
1475
|
+
bindingValue = this.model.get(this.options.defaultValueBinding);
|
1476
|
+
}
|
1477
|
+
|
1478
|
+
if (typeof this.options.defaultValue === 'function') {
|
1479
|
+
return this.options.defaultValue(bindingValue);
|
1480
|
+
} else if ('defaultValue' in this.options) {
|
1481
|
+
return this.options.defaultValue;
|
1482
|
+
} else {
|
1483
|
+
return bindingValue;
|
1484
|
+
}
|
1485
|
+
}
|
1486
|
+
});
|
1487
|
+
|
1488
|
+
function template$7(data) {
|
1489
|
+
var __p = '';
|
1490
|
+
__p += '<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>\n<select></select>';
|
1491
|
+
return __p
|
1492
|
+
}
|
1493
|
+
|
1494
|
+
/**
|
1495
|
+
* A drop down with support for grouped items.
|
1496
|
+
* See {@link inputView} for further options
|
1497
|
+
*
|
1498
|
+
* @param {Object} [options]
|
1499
|
+
*
|
1500
|
+
* @param {string[]} [options.values]
|
1501
|
+
* List of possible values to persist in the attribute.
|
1502
|
+
*
|
1503
|
+
* @param {string[]} [options.texts]
|
1504
|
+
* List of display texts for drop down items.
|
1505
|
+
*
|
1506
|
+
* @param {string[]} [options.translationKeys]
|
1507
|
+
* Translation keys to obtain item texts from.
|
1508
|
+
*
|
1509
|
+
* @param {string[]} [options.translationKeyPrefix]
|
1510
|
+
* Obtain texts for items from translations by appending the item
|
1511
|
+
* value to this prefix separated by a dot. By default the
|
1512
|
+
* [`attributeTranslationKeyPrefixes` option]{@link inputView}
|
1513
|
+
* is used by appending the suffix `.values` to each candidate.
|
1514
|
+
*
|
1515
|
+
* @param {string[]} [options.groups]
|
1516
|
+
* Array of same length as `values` array, containing the display
|
1517
|
+
* name of a group header each item shall be grouped under.
|
1518
|
+
*
|
1519
|
+
* @param {Backbone.Model[]} [options.collection]
|
1520
|
+
* Create items for each model in the collection. Use the
|
1521
|
+
* `*Property` options to extract values and texts for each items
|
1522
|
+
* from the models.
|
1523
|
+
*
|
1524
|
+
* @param {string} [options.valueProperty]
|
1525
|
+
* Attribute to use as item value.
|
1526
|
+
*
|
1527
|
+
* @param {string} [options.textProperty]
|
1528
|
+
* Attribute to use as item display text.
|
1529
|
+
*
|
1530
|
+
* @param {string} [options.groupProperty]
|
1531
|
+
* Attribute to use as item group name.
|
1532
|
+
*
|
1533
|
+
* @param {string} [options.translationKeyProperty]
|
1534
|
+
* Attribute to use as translation key to obtain display text.
|
1535
|
+
*
|
1536
|
+
* @param {string} [options.groupTranslationKeyProperty]
|
1537
|
+
* Attribute to use as translation key to obtain group name.
|
1538
|
+
*
|
1539
|
+
* @param {boolean} [options.ensureValueDefined]
|
1540
|
+
* Set the attribute to the first value on view creation.
|
1541
|
+
*
|
1542
|
+
* @param {boolean} [options.includeBlank]
|
1543
|
+
* Include an item that sets the value of the attribute to a blank
|
1544
|
+
* string.
|
1545
|
+
*
|
1546
|
+
* @param {string} [options.blankText]
|
1547
|
+
* Display text for the blank item.
|
1548
|
+
*
|
1549
|
+
* @param {string} [options.blankTranslationKey]
|
1550
|
+
* Translation key to obtain display text for blank item.
|
1551
|
+
*
|
1552
|
+
* @param {string} [options.placeholderValue]
|
1553
|
+
* Include an item that sets the value of the attribute to a blank
|
1554
|
+
* string and indicate that the attribute is set to a default
|
1555
|
+
* value. Include the display name of the given value, in the
|
1556
|
+
* text. This option can be used if a fallback to the
|
1557
|
+
* `placeholderValue` occurs whenever the attribute is blank.
|
1558
|
+
*
|
1559
|
+
* @param {Backbone.Model} [options.placeholderModel]
|
1560
|
+
* Behaves like `placeholderValue`, but obtains the value by looking
|
1561
|
+
* up the `propertyName` attribute inside the given model. This
|
1562
|
+
* option can be used if a fallback to the corresponding attribute
|
1563
|
+
* value of the `placeholderModel` occurs whenever the attribute is
|
1564
|
+
* blank.
|
1565
|
+
*
|
1566
|
+
* @class
|
1567
|
+
*/
|
1568
|
+
|
1569
|
+
var SelectInputView = Marionette.ItemView.extend({
|
1570
|
+
mixins: [inputView],
|
1571
|
+
template: template$7,
|
1572
|
+
events: {
|
1573
|
+
'change': 'save'
|
1574
|
+
},
|
1575
|
+
ui: {
|
1576
|
+
select: 'select',
|
1577
|
+
input: 'select'
|
1578
|
+
},
|
1579
|
+
initialize: function initialize() {
|
1580
|
+
if (this.options.collection) {
|
1581
|
+
this.options.values = _.pluck(this.options.collection, this.options.valueProperty);
|
1582
|
+
|
1583
|
+
if (this.options.textProperty) {
|
1584
|
+
this.options.texts = _.pluck(this.options.collection, this.options.textProperty);
|
1585
|
+
} else if (this.options.translationKeyProperty) {
|
1586
|
+
this.options.translationKeys = _.pluck(this.options.collection, this.options.translationKeyProperty);
|
1587
|
+
}
|
1588
|
+
|
1589
|
+
if (this.options.groupProperty) {
|
1590
|
+
this.options.groups = _.pluck(this.options.collection, this.options.groupProperty);
|
1591
|
+
} else if (this.options.groupTranslationKeyProperty) {
|
1592
|
+
this.options.groupTanslationKeys = _.pluck(this.options.collection, this.options.groupTranslationKeyProperty);
|
1593
|
+
}
|
1594
|
+
}
|
1595
|
+
|
1596
|
+
if (!this.options.texts) {
|
1597
|
+
if (!this.options.translationKeys) {
|
1598
|
+
var translationKeyPrefix = this.options.translationKeyPrefix || findKeyWithTranslation(this.attributeTranslationKeys('values', {
|
1599
|
+
fallbackPrefix: 'activerecord.values'
|
1600
|
+
}));
|
1601
|
+
this.options.translationKeys = _.map(this.options.values, function (value) {
|
1602
|
+
return translationKeyPrefix + '.' + value;
|
1603
|
+
}, this);
|
1604
|
+
}
|
1605
|
+
|
1606
|
+
this.options.texts = _.map(this.options.translationKeys, function (key) {
|
1607
|
+
return I18n$1.t(key);
|
1608
|
+
});
|
1609
|
+
}
|
1610
|
+
|
1611
|
+
if (!this.options.groups) {
|
1612
|
+
this.options.groups = _.map(this.options.groupTanslationKeys, function (key) {
|
1613
|
+
return I18n$1.t(key);
|
1614
|
+
});
|
1615
|
+
}
|
1616
|
+
|
1617
|
+
this.optGroups = {};
|
1618
|
+
},
|
1619
|
+
onRender: function onRender() {
|
1620
|
+
this.appendBlank();
|
1621
|
+
this.appendPlaceholder();
|
1622
|
+
this.appendOptions();
|
1623
|
+
this.load();
|
1624
|
+
this.listenTo(this.model, 'change:' + this.options.propertyName, this.load);
|
1625
|
+
|
1626
|
+
if (this.options.ensureValueDefined && !this.model.has(this.options.propertyName)) {
|
1627
|
+
this.save();
|
1628
|
+
}
|
1629
|
+
},
|
1630
|
+
appendBlank: function appendBlank() {
|
1631
|
+
if (!this.options.includeBlank) {
|
1632
|
+
return;
|
1633
|
+
}
|
1634
|
+
|
1635
|
+
if (this.options.blankTranslationKey) {
|
1636
|
+
this.options.blankText = I18n$1.t(this.options.blankTranslationKey);
|
1637
|
+
}
|
1638
|
+
|
1639
|
+
var option = document.createElement('option');
|
1640
|
+
option.value = '';
|
1641
|
+
option.text = this.options.blankText || I18n$1.t('pageflow.ui.views.inputs.select_input_view.none');
|
1642
|
+
this.ui.select.append(option);
|
1643
|
+
},
|
1644
|
+
appendPlaceholder: function appendPlaceholder() {
|
1645
|
+
if (!this.options.placeholderModel && !this.options.placeholderValue) {
|
1646
|
+
return;
|
1647
|
+
}
|
1648
|
+
|
1649
|
+
var placeholderValue = this.options.placeholderValue || this.options.placeholderModel.get(this.options.propertyName);
|
1650
|
+
var placeholderIndex = this.options.values.indexOf(placeholderValue);
|
1651
|
+
|
1652
|
+
if (placeholderIndex >= 0) {
|
1653
|
+
var option = document.createElement('option');
|
1654
|
+
option.value = '';
|
1655
|
+
option.text = I18n$1.t('pageflow.ui.views.inputs.select_input_view.placeholder', {
|
1656
|
+
text: this.options.texts[placeholderIndex]
|
1657
|
+
});
|
1658
|
+
this.ui.select.append(option);
|
1659
|
+
}
|
1660
|
+
},
|
1661
|
+
appendOptions: function appendOptions() {
|
1662
|
+
_.each(this.options.values, function (value, index) {
|
1663
|
+
var option = document.createElement('option');
|
1664
|
+
var group = this.options.groups[index];
|
1665
|
+
option.value = value;
|
1666
|
+
option.text = this.options.texts[index];
|
1667
|
+
|
1668
|
+
if (group) {
|
1669
|
+
option.setAttribute('data-group', group);
|
1670
|
+
this.findOrCreateOptGroup(group).append(option);
|
1671
|
+
} else {
|
1672
|
+
this.ui.select.append(option);
|
1673
|
+
}
|
1674
|
+
}, this);
|
1675
|
+
},
|
1676
|
+
findOrCreateOptGroup: function findOrCreateOptGroup(label) {
|
1677
|
+
if (!this.optGroups[label]) {
|
1678
|
+
this.optGroups[label] = $('<optgroup />', {
|
1679
|
+
label: label
|
1680
|
+
}).appendTo(this.ui.select);
|
1681
|
+
}
|
1682
|
+
|
1683
|
+
return this.optGroups[label];
|
1684
|
+
},
|
1685
|
+
save: function save() {
|
1686
|
+
this.model.set(this.options.propertyName, this.ui.select.val());
|
1687
|
+
},
|
1688
|
+
load: function load() {
|
1689
|
+
if (!this.isClosed) {
|
1690
|
+
var value = this.model.get(this.options.propertyName);
|
1691
|
+
|
1692
|
+
if (this.model.has(this.options.propertyName) && this.ui.select.find('option[value="' + value + '"]').length) {
|
1693
|
+
this.ui.select.val(value);
|
1694
|
+
} else {
|
1695
|
+
this.ui.select.val(this.ui.select.find('option:first').val());
|
1696
|
+
}
|
1697
|
+
}
|
1698
|
+
}
|
1699
|
+
});
|
1700
|
+
|
1701
|
+
var ExtendedSelectInputView = SelectInputView.extend({
|
1702
|
+
className: 'extended_select_input',
|
1703
|
+
initialize: function initialize() {
|
1704
|
+
SelectInputView.prototype.initialize.apply(this, arguments);
|
1705
|
+
|
1706
|
+
if (this.options.collection) {
|
1707
|
+
if (this.options.descriptionProperty) {
|
1708
|
+
this.options.descriptions = _.pluck(this.options.collection, this.options.descriptionProperty);
|
1709
|
+
} else if (this.options.descriptionTranslationKeyProperty) {
|
1710
|
+
this.options.descriptionTanslationKeys = _.pluck(this.options.collection, this.options.descriptionTranslationKeyProperty);
|
1711
|
+
}
|
1712
|
+
}
|
1713
|
+
|
1714
|
+
if (!this.options.descriptions) {
|
1715
|
+
this.options.descriptions = _.map(this.options.descriptionTanslationKeys, function (key) {
|
1716
|
+
return I18n$1.t(key);
|
1717
|
+
});
|
1718
|
+
}
|
1719
|
+
},
|
1720
|
+
onRender: function onRender() {
|
1721
|
+
var view = this,
|
1722
|
+
options = this.options;
|
1723
|
+
SelectInputView.prototype.onRender.apply(this, arguments);
|
1724
|
+
$.widget("custom.extendedselectmenu", $.ui.selectmenu, {
|
1725
|
+
_renderItem: function _renderItem(ul, item) {
|
1726
|
+
var widget = this;
|
1727
|
+
var li = $('<li>', {
|
1728
|
+
"class": item.value
|
1729
|
+
});
|
1730
|
+
var container = $('<div>', {
|
1731
|
+
"class": 'text-container'
|
1732
|
+
}).appendTo(li);
|
1733
|
+
var index = options.values.indexOf(item.value);
|
1734
|
+
|
1735
|
+
if (item.disabled) {
|
1736
|
+
li.addClass('ui-state-disabled');
|
1737
|
+
}
|
1738
|
+
|
1739
|
+
if (options.pictogramClass) {
|
1740
|
+
$('<span>', {
|
1741
|
+
"class": options.pictogramClass
|
1742
|
+
}).prependTo(li);
|
1743
|
+
}
|
1744
|
+
|
1745
|
+
$('<p>', {
|
1746
|
+
text: item.label,
|
1747
|
+
"class": 'item-text'
|
1748
|
+
}).appendTo(container);
|
1749
|
+
$('<p>', {
|
1750
|
+
text: options.descriptions[index],
|
1751
|
+
"class": 'item-description'
|
1752
|
+
}).appendTo(container);
|
1753
|
+
|
1754
|
+
if (options.helpLinkClicked) {
|
1755
|
+
$('<a>', {
|
1756
|
+
href: '#',
|
1757
|
+
title: I18n$1.t('pageflow.ui.views.extended_select_input_view.display_help')
|
1758
|
+
}).on('click', function () {
|
1759
|
+
widget.close();
|
1760
|
+
options.helpLinkClicked(item.value);
|
1761
|
+
return false;
|
1762
|
+
}).appendTo(li);
|
1763
|
+
}
|
1764
|
+
|
1765
|
+
return li.appendTo(ul);
|
1766
|
+
},
|
1767
|
+
_resizeMenu: function _resizeMenu() {
|
1768
|
+
this.menuWrap.addClass('extended_select_input_menu');
|
1769
|
+
var menuHeight = this.menu.height(),
|
1770
|
+
menuOffset = this.button.offset().top + this.button.outerHeight(),
|
1771
|
+
bodyHeight = $('body').height();
|
1772
|
+
|
1773
|
+
if (menuHeight + menuOffset > bodyHeight) {
|
1774
|
+
this.menuWrap.outerHeight(bodyHeight - menuOffset - 5).css({
|
1775
|
+
'overflow-y': 'scroll'
|
1776
|
+
});
|
1777
|
+
} else {
|
1778
|
+
this.menuWrap.css({
|
1779
|
+
height: 'initial',
|
1780
|
+
'overflow-y': 'initial'
|
1781
|
+
});
|
1782
|
+
}
|
1783
|
+
}
|
1784
|
+
});
|
1785
|
+
this.ui.select.extendedselectmenu({
|
1786
|
+
select: view.select.bind(view),
|
1787
|
+
width: '100%',
|
1788
|
+
position: {
|
1789
|
+
my: 'right top',
|
1790
|
+
at: 'right bottom'
|
1791
|
+
}
|
1792
|
+
});
|
1793
|
+
},
|
1794
|
+
select: function select(event, ui) {
|
1795
|
+
this.ui.select.val(ui.item.value);
|
1796
|
+
this.save();
|
1797
|
+
}
|
1798
|
+
});
|
1799
|
+
|
1800
|
+
function template$8(data) {
|
1801
|
+
var __t, __p = '';
|
1802
|
+
__p += '<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>\n\n<!-- inline style for wysihtml5 to pick up -->\n<textarea style="width: 100%;" dir="auto"></textarea>\n\n<div class="toolbar">\n <a data-wysihtml5-command="bold" title="' +
|
1803
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.bold') )) == null ? '' : __t) +
|
1804
|
+
'"></a>\n <a data-wysihtml5-command="italic" title="' +
|
1805
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.italic') )) == null ? '' : __t) +
|
1806
|
+
'"></a>\n <a data-wysihtml5-command="underline" title="' +
|
1807
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.underline') )) == null ? '' : __t) +
|
1808
|
+
'"></a>\n <a data-wysihtml5-command="createLink" class="link_button" title="' +
|
1809
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.create_link') )) == null ? '' : __t) +
|
1810
|
+
'"></a>\n <a data-wysihtml5-command="insertOrderedList" title="' +
|
1811
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.insert_ordered_list') )) == null ? '' : __t) +
|
1812
|
+
'"></a>\n <a data-wysihtml5-command="insertUnorderedList" title="' +
|
1813
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.insert_unordered_list') )) == null ? '' : __t) +
|
1814
|
+
'"></a>\n\n <div data-wysihtml5-dialog="createLink" class="dialog link_dialog" style="display: none;">\n <div class="link_type_select">\n <label>\n <input type="radio" name="link_type" class="url_link_radio_button">\n ' +
|
1815
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.link_type.url') )) == null ? '' : __t) +
|
1816
|
+
'\n </label>\n <label>\n <input type="radio" name="link_type" class="fragment_link_radio_button">\n ' +
|
1817
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.link_type.page_link') )) == null ? '' : __t) +
|
1818
|
+
'\n </label>\n </div>\n <div class="url_link_panel">\n <label>\n ' +
|
1819
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.target') )) == null ? '' : __t) +
|
1820
|
+
'\n </label>\n <input type="text" class="display_url">\n <div class="open_in_new_tab_section">\n <label>\n <input type="checkbox" class="open_in_new_tab">\n ' +
|
1821
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.open_in_new_tab') )) == null ? '' : __t) +
|
1822
|
+
'\n </label>\n <span class="inline_help">\n ' +
|
1823
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.open_in_new_tab_help') )) == null ? '' : __t) +
|
1824
|
+
'\n </span>\n </div>\n </div>\n <div class="fragment_link_panel">\n <!-- LinkInputView is inserted here -->\n </div>\n\n <!-- wysihtml5 does not handle hidden fields correctly -->\n <div class="internal">\n <input type="text" data-wysihtml5-dialog-field="href" class="current_url" value="http://">\n <input type="text" data-wysihtml5-dialog-field="target" class="current_target" value="_blank">\n </div>\n\n <a class="button" data-wysihtml5-dialog-action="save">\n ' +
|
1825
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.save') )) == null ? '' : __t) +
|
1826
|
+
'\n </a>\n <a class="button" data-wysihtml5-dialog-action="cancel">\n ' +
|
1827
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.cancel') )) == null ? '' : __t) +
|
1828
|
+
'\n </a>\n\n <a data-wysihtml5-command="removeLink">' +
|
1829
|
+
((__t = ( I18n.t('pageflow.ui.templates.inputs.text_area_input.remove_link') )) == null ? '' : __t) +
|
1830
|
+
'</a>\n </div>\n</div>\n';
|
1831
|
+
return __p
|
1832
|
+
}
|
1833
|
+
|
1834
|
+
/**
|
1835
|
+
* Input view for multi line text with simple formatting options.
|
1836
|
+
* See {@link inputWithPlaceholderText} for placeholder related options.
|
1837
|
+
* See {@link inputView} for further options.
|
1838
|
+
*
|
1839
|
+
* @param {Object} [options]
|
1840
|
+
*
|
1841
|
+
* @param {string} [options.size="normal"]
|
1842
|
+
* Pass `"short"` to reduce the text area height.
|
1843
|
+
*
|
1844
|
+
* @param {boolean} [options.disableLinks=false]
|
1845
|
+
* Do not allow links inside the text.
|
1846
|
+
*
|
1847
|
+
* @param {boolean} [options.disableRichtext=false]
|
1848
|
+
* Do not provide text formatting options.
|
1849
|
+
*
|
1850
|
+
* @param {Backbone.View} [options.fragmentLinkInputView]
|
1851
|
+
* A view to select an id to use in links which only consist
|
1852
|
+
* of a url fragment. Will receive a model with a `linkId`
|
1853
|
+
* attribute.
|
1854
|
+
*
|
1855
|
+
* @class
|
1856
|
+
*/
|
1857
|
+
|
1858
|
+
var TextAreaInputView = Marionette.ItemView.extend({
|
1859
|
+
mixins: [inputView, inputWithPlaceholderText],
|
1860
|
+
template: template$8,
|
1861
|
+
ui: {
|
1862
|
+
input: 'textarea',
|
1863
|
+
toolbar: '.toolbar',
|
1864
|
+
linkButton: '.link_button',
|
1865
|
+
linkDialog: '.link_dialog',
|
1866
|
+
urlInput: '.current_url',
|
1867
|
+
targetInput: '.current_target',
|
1868
|
+
linkTypeSelection: '.link_type_select',
|
1869
|
+
urlLinkRadioButton: '.url_link_radio_button',
|
1870
|
+
fragmentLinkRadioButton: '.fragment_link_radio_button',
|
1871
|
+
urlLinkPanel: '.url_link_panel',
|
1872
|
+
displayUrlInput: '.display_url',
|
1873
|
+
openInNewTabCheckBox: '.open_in_new_tab',
|
1874
|
+
fragmentLinkPanel: '.fragment_link_panel'
|
1875
|
+
},
|
1876
|
+
events: {
|
1877
|
+
'change textarea': 'save',
|
1878
|
+
'click .url_link_radio_button': 'showUrlLinkPanel',
|
1879
|
+
'click .fragment_link_radio_button': 'showFragmentLinkPanel',
|
1880
|
+
'change .open_in_new_tab': 'setTargetFromOpenInNewTabCheckBox',
|
1881
|
+
'change .display_url': 'setUrlFromDisplayUrl'
|
1882
|
+
},
|
1883
|
+
onRender: function onRender() {
|
1884
|
+
this.ui.input.addClass(this.options.size);
|
1885
|
+
this.load();
|
1886
|
+
this.updatePlaceholder();
|
1887
|
+
this.editor = new wysihtml5.Editor(this.ui.input[0], {
|
1888
|
+
toolbar: this.ui.toolbar[0],
|
1889
|
+
autoLink: this.options.disableLinks ? 0 : 1,
|
1890
|
+
parserRules: {
|
1891
|
+
tags: {
|
1892
|
+
em: {
|
1893
|
+
unwrap: this.options.disableRichtext ? 1 : 0,
|
1894
|
+
rename_tag: "i"
|
1895
|
+
},
|
1896
|
+
strong: {
|
1897
|
+
unwrap: this.options.disableRichtext ? 1 : 0,
|
1898
|
+
rename_tag: "b"
|
1899
|
+
},
|
1900
|
+
u: {
|
1901
|
+
unwrap: this.options.disableRichtext ? 1 : 0
|
1902
|
+
},
|
1903
|
+
b: {
|
1904
|
+
unwrap: this.options.disableRichtext ? 1 : 0
|
1905
|
+
},
|
1906
|
+
i: {
|
1907
|
+
unwrap: this.options.disableRichtext ? 1 : 0
|
1908
|
+
},
|
1909
|
+
ol: {
|
1910
|
+
unwrap: this.options.enableLists ? 0 : 1
|
1911
|
+
},
|
1912
|
+
ul: {
|
1913
|
+
unwrap: this.options.enableLists ? 0 : 1
|
1914
|
+
},
|
1915
|
+
li: {
|
1916
|
+
unwrap: this.options.enableLists ? 0 : 1
|
1917
|
+
},
|
1918
|
+
br: {},
|
1919
|
+
a: {
|
1920
|
+
unwrap: this.options.disableLinks ? 1 : 0,
|
1921
|
+
check_attributes: {
|
1922
|
+
href: 'href',
|
1923
|
+
target: 'any'
|
1924
|
+
},
|
1925
|
+
set_attributes: {
|
1926
|
+
rel: 'nofollow'
|
1927
|
+
}
|
1928
|
+
}
|
1929
|
+
}
|
1930
|
+
}
|
1931
|
+
});
|
1932
|
+
|
1933
|
+
if (this.options.disableRichtext) {
|
1934
|
+
this.ui.toolbar.find('a[data-wysihtml5-command="bold"]').hide();
|
1935
|
+
this.ui.toolbar.find('a[data-wysihtml5-command="italic"]').hide();
|
1936
|
+
this.ui.toolbar.find('a[data-wysihtml5-command="underline"]').hide();
|
1937
|
+
this.ui.toolbar.find('a[data-wysihtml5-command="insertOrderedList"]').hide();
|
1938
|
+
this.ui.toolbar.find('a[data-wysihtml5-command="insertUnorderedList"]').hide();
|
1939
|
+
}
|
1940
|
+
|
1941
|
+
if (!this.options.enableLists) {
|
1942
|
+
this.ui.toolbar.find('a[data-wysihtml5-command="insertOrderedList"]').hide();
|
1943
|
+
this.ui.toolbar.find('a[data-wysihtml5-command="insertUnorderedList"]').hide();
|
1944
|
+
}
|
1945
|
+
|
1946
|
+
if (this.options.disableLinks) {
|
1947
|
+
this.ui.toolbar.find('a[data-wysihtml5-command="createLink"]').hide();
|
1948
|
+
} else {
|
1949
|
+
this.setupUrlLinkPanel();
|
1950
|
+
this.setupFragmentLinkPanel();
|
1951
|
+
}
|
1952
|
+
|
1953
|
+
this.editor.on('change', _.bind(this.save, this));
|
1954
|
+
this.editor.on('aftercommand:composer', _.bind(this.save, this));
|
1955
|
+
},
|
1956
|
+
onClose: function onClose() {
|
1957
|
+
this.editor.fire('destroy:composer');
|
1958
|
+
},
|
1959
|
+
save: function save() {
|
1960
|
+
this.model.set(this.options.propertyName, this.editor.getValue());
|
1961
|
+
},
|
1962
|
+
load: function load() {
|
1963
|
+
this.ui.input.val(this.model.get(this.options.propertyName));
|
1964
|
+
},
|
1965
|
+
setupUrlLinkPanel: function setupUrlLinkPanel() {
|
1966
|
+
this.editor.on('show:dialog', _.bind(function () {
|
1967
|
+
this.ui.linkDialog.toggleClass('for_existing_link', this.ui.linkButton.hasClass('wysihtml5-command-active'));
|
1968
|
+
var currentUrl = this.ui.urlInput.val();
|
1969
|
+
|
1970
|
+
if (currentUrl.startsWith('#')) {
|
1971
|
+
this.ui.displayUrlInput.val('http://');
|
1972
|
+
this.ui.openInNewTabCheckBox.prop('checked', true);
|
1973
|
+
} else {
|
1974
|
+
this.ui.displayUrlInput.val(currentUrl);
|
1975
|
+
this.ui.openInNewTabCheckBox.prop('checked', this.ui.targetInput.val() !== '_self');
|
1976
|
+
}
|
1977
|
+
}, this));
|
1978
|
+
},
|
1979
|
+
setupFragmentLinkPanel: function setupFragmentLinkPanel() {
|
1980
|
+
if (this.options.fragmentLinkInputView) {
|
1981
|
+
this.fragmentLinkModel = new Backbone.Model();
|
1982
|
+
this.listenTo(this.fragmentLinkModel, 'change', function (model, options) {
|
1983
|
+
if (!options.skipCurrentUrlUpdate) {
|
1984
|
+
this.setInputsFromFragmentLinkModel();
|
1985
|
+
}
|
1986
|
+
});
|
1987
|
+
this.editor.on('show:dialog', _.bind(function () {
|
1988
|
+
var currentUrl = this.ui.urlInput.val();
|
1989
|
+
var id = currentUrl.startsWith('#') ? currentUrl.substr(1) : null;
|
1990
|
+
this.fragmentLinkModel.set('linkId', id, {
|
1991
|
+
skipCurrentUrlUpdate: true
|
1992
|
+
});
|
1993
|
+
this.initLinkTypePanels(!id);
|
1994
|
+
}, this));
|
1995
|
+
var fragmentLinkInput = new this.options.fragmentLinkInputView({
|
1996
|
+
model: this.fragmentLinkModel,
|
1997
|
+
propertyName: 'linkId',
|
1998
|
+
label: I18n$1.t('pageflow.ui.templates.inputs.text_area_input.target'),
|
1999
|
+
hideUnsetButton: true
|
2000
|
+
});
|
2001
|
+
this.ui.fragmentLinkPanel.append(fragmentLinkInput.render().el);
|
2002
|
+
} else {
|
2003
|
+
this.ui.linkTypeSelection.hide();
|
2004
|
+
this.ui.fragmentLinkPanel.hide();
|
2005
|
+
}
|
2006
|
+
},
|
2007
|
+
initLinkTypePanels: function initLinkTypePanels(isUrlLink) {
|
2008
|
+
if (isUrlLink) {
|
2009
|
+
this.ui.urlLinkRadioButton.prop('checked', true);
|
2010
|
+
} else {
|
2011
|
+
this.ui.fragmentLinkRadioButton.prop('checked', true);
|
2012
|
+
}
|
2013
|
+
|
2014
|
+
this.ui.toolbar.toggleClass('fragment_link_panel_active', !isUrlLink);
|
2015
|
+
},
|
2016
|
+
showUrlLinkPanel: function showUrlLinkPanel() {
|
2017
|
+
this.ui.toolbar.removeClass('fragment_link_panel_active');
|
2018
|
+
this.setUrlFromDisplayUrl();
|
2019
|
+
this.setTargetFromOpenInNewTabCheckBox();
|
2020
|
+
},
|
2021
|
+
showFragmentLinkPanel: function showFragmentLinkPanel() {
|
2022
|
+
this.ui.toolbar.addClass('fragment_link_panel_active');
|
2023
|
+
this.setInputsFromFragmentLinkModel();
|
2024
|
+
},
|
2025
|
+
setInputsFromFragmentLinkModel: function setInputsFromFragmentLinkModel() {
|
2026
|
+
this.ui.urlInput.val('#' + (this.fragmentLinkModel.get('linkId') || ''));
|
2027
|
+
this.ui.targetInput.val('_self');
|
2028
|
+
},
|
2029
|
+
setUrlFromDisplayUrl: function setUrlFromDisplayUrl() {
|
2030
|
+
this.ui.urlInput.val(this.ui.displayUrlInput.val());
|
2031
|
+
},
|
2032
|
+
setTargetFromOpenInNewTabCheckBox: function setTargetFromOpenInNewTabCheckBox() {
|
2033
|
+
this.ui.targetInput.val(this.ui.openInNewTabCheckBox.is(':checked') ? '_blank' : '_self');
|
2034
|
+
}
|
2035
|
+
});
|
2036
|
+
|
2037
|
+
function template$9(data) {
|
2038
|
+
var __p = '';
|
2039
|
+
__p += '<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>\n<input type="text" />\n<div class="validation"></div>\n';
|
2040
|
+
return __p
|
2041
|
+
}
|
2042
|
+
|
2043
|
+
/**
|
2044
|
+
* Input view for URLs.
|
2045
|
+
* See {@link inputView} for further options
|
2046
|
+
*
|
2047
|
+
* @param {Object} [options]
|
2048
|
+
*
|
2049
|
+
* @param {string[]} options.supportedHosts
|
2050
|
+
* List of allowed url prefixes.
|
2051
|
+
*
|
2052
|
+
* @param {boolean} [options.required=false]
|
2053
|
+
* Display an error if the url is blank.
|
2054
|
+
*
|
2055
|
+
* @param {boolean} [options.permitHttps=false]
|
2056
|
+
* Allow urls with https protocol.
|
2057
|
+
*
|
2058
|
+
* @class
|
2059
|
+
*/
|
2060
|
+
|
2061
|
+
var UrlInputView = Marionette.Layout.extend(
|
2062
|
+
/** @lends UrlInputView.prototype */
|
2063
|
+
{
|
2064
|
+
mixins: [inputView],
|
2065
|
+
template: template$9,
|
2066
|
+
ui: {
|
2067
|
+
input: 'input',
|
2068
|
+
validation: '.validation'
|
2069
|
+
},
|
2070
|
+
events: {
|
2071
|
+
'change': 'onChange'
|
2072
|
+
},
|
2073
|
+
onRender: function onRender() {
|
2074
|
+
this.ui.validation.hide();
|
2075
|
+
this.load();
|
2076
|
+
this.validate();
|
2077
|
+
},
|
2078
|
+
onChange: function onChange() {
|
2079
|
+
var view = this;
|
2080
|
+
this.saveDisplayProperty();
|
2081
|
+
this.validate().done(function () {
|
2082
|
+
view.save();
|
2083
|
+
});
|
2084
|
+
},
|
2085
|
+
saveDisplayProperty: function saveDisplayProperty() {
|
2086
|
+
this.model.set(this.options.displayPropertyName, this.ui.input.val());
|
2087
|
+
this.model.unset(this.options.propertyName);
|
2088
|
+
},
|
2089
|
+
save: function save() {
|
2090
|
+
var view = this;
|
2091
|
+
$.when(this.transformPropertyValue(this.ui.input.val())).then(function (value) {
|
2092
|
+
view.model.set(view.options.propertyName, value);
|
2093
|
+
});
|
2094
|
+
},
|
2095
|
+
load: function load() {
|
2096
|
+
this.ui.input.val(this.model.get(this.options.displayPropertyName));
|
2097
|
+
this.onLoad();
|
2098
|
+
},
|
2099
|
+
|
2100
|
+
/**
|
2101
|
+
* Override to be notified when the input has been loaded.
|
2102
|
+
*/
|
2103
|
+
onLoad: function onLoad() {},
|
2104
|
+
|
2105
|
+
/**
|
2106
|
+
* Override to validate the untransformed url. Validation error
|
2107
|
+
* message can be passed as rejected promise. Progress notifications
|
2108
|
+
* are displayed. Only valid urls are stored in the configuration.
|
2109
|
+
*
|
2110
|
+
* @return Promise
|
2111
|
+
*/
|
2112
|
+
validateUrl: function validateUrl(url) {
|
2113
|
+
return $.Deferred().resolve().promise();
|
2114
|
+
},
|
2115
|
+
|
2116
|
+
/**
|
2117
|
+
* Override to transform the property value before it is stored.
|
2118
|
+
*
|
2119
|
+
* @return Promise | String
|
2120
|
+
*/
|
2121
|
+
transformPropertyValue: function transformPropertyValue(value) {
|
2122
|
+
return value;
|
2123
|
+
},
|
2124
|
+
|
2125
|
+
/**
|
2126
|
+
* Override to change the list of supported host names.
|
2127
|
+
*/
|
2128
|
+
supportedHosts: function supportedHosts() {
|
2129
|
+
return this.options.supportedHosts;
|
2130
|
+
},
|
2131
|
+
validate: function validate(success) {
|
2132
|
+
var view = this;
|
2133
|
+
var options = this.options;
|
2134
|
+
var value = this.ui.input.val();
|
2135
|
+
|
2136
|
+
if (options.required && !value) {
|
2137
|
+
displayValidationError(I18n$1.t('pageflow.ui.views.inputs.url_input_view.required_field'));
|
2138
|
+
} else if (value && !isValidUrl(value)) {
|
2139
|
+
var errorMessage = I18n$1.t('pageflow.ui.views.inputs.url_input_view.url_hint');
|
2140
|
+
|
2141
|
+
if (options.permitHttps) {
|
2142
|
+
errorMessage = I18n$1.t('pageflow.ui.views.inputs.url_input_view.url_hint_https');
|
2143
|
+
}
|
2144
|
+
|
2145
|
+
displayValidationError(errorMessage);
|
2146
|
+
} else if (value && !hasSupportedHost(value)) {
|
2147
|
+
displayValidationError(I18n$1.t('pageflow.ui.views.inputs.url_input_view.supported_vendors') + _.map(view.supportedHosts(), function (url) {
|
2148
|
+
return '<li>' + url + '</li>';
|
2149
|
+
}).join(''));
|
2150
|
+
} else {
|
2151
|
+
return view.validateUrl(value).progress(function (message) {
|
2152
|
+
displayValidationPending(message);
|
2153
|
+
}).done(function () {
|
2154
|
+
resetValidationError();
|
2155
|
+
}).fail(function (error) {
|
2156
|
+
displayValidationError(error);
|
2157
|
+
});
|
2158
|
+
}
|
2159
|
+
|
2160
|
+
return $.Deferred().reject().promise();
|
2161
|
+
|
2162
|
+
function isValidUrl(url) {
|
2163
|
+
return options.permitHttps ? url.match(/^https?:\/\//i) : url.match(/^http:\/\//i);
|
2164
|
+
}
|
2165
|
+
|
2166
|
+
function hasSupportedHost(url) {
|
2167
|
+
return _.any(view.supportedHosts(), function (host) {
|
2168
|
+
return url.match(new RegExp('^' + host));
|
2169
|
+
});
|
2170
|
+
}
|
2171
|
+
|
2172
|
+
function displayValidationError(message) {
|
2173
|
+
view.$el.addClass('invalid');
|
2174
|
+
view.ui.validation.removeClass('pending').addClass('failed').html(message).show();
|
2175
|
+
}
|
2176
|
+
|
2177
|
+
function displayValidationPending(message) {
|
2178
|
+
view.$el.removeClass('invalid');
|
2179
|
+
view.ui.validation.removeClass('failed').addClass('pending').html(message).show();
|
2180
|
+
}
|
2181
|
+
|
2182
|
+
function resetValidationError(message) {
|
2183
|
+
view.$el.removeClass('invalid');
|
2184
|
+
view.ui.validation.hide();
|
2185
|
+
}
|
2186
|
+
}
|
2187
|
+
});
|
2188
|
+
|
2189
|
+
/**
|
2190
|
+
* Input view that verifies that a certain URL is reachable via a
|
2191
|
+
* proxy. To conform with same origin restrictions, this input view
|
2192
|
+
* lets the user enter some url and saves a rewritten url where the
|
2193
|
+
* domain is replaced with some path segment.
|
2194
|
+
*
|
2195
|
+
* That way, when `/example` is setup to proxy requests to
|
2196
|
+
* `http://example.com`, the user can enter an url of the form
|
2197
|
+
* `http://example.com/some/path` but the string `/example/some/path`
|
2198
|
+
* is persisited to the database.
|
2199
|
+
*
|
2200
|
+
* See {@link inputView} for further options
|
2201
|
+
*
|
2202
|
+
* @param {Object} options
|
2203
|
+
*
|
2204
|
+
* @param {string} options.displayPropertyName
|
2205
|
+
* Attribute name to store the url entered by the user.
|
2206
|
+
*
|
2207
|
+
* @param {Object[]} options.proxies
|
2208
|
+
* List of supported proxies.
|
2209
|
+
*
|
2210
|
+
* @param {string} options.proxies[].url
|
2211
|
+
* Supported prefix of an url that can be entered by the user.
|
2212
|
+
*
|
2213
|
+
* @param {string} options.proxies[].base_path
|
2214
|
+
* Path to replace the url prefix with.
|
2215
|
+
*
|
2216
|
+
* @param {boolean} [options.required=false]
|
2217
|
+
* Display an error if the url is blank.
|
2218
|
+
*
|
2219
|
+
* @param {boolean} [options.permitHttps=false]
|
2220
|
+
* Allow urls with https protocol.
|
2221
|
+
*
|
2222
|
+
* @example
|
2223
|
+
*
|
2224
|
+
* this.input('url, ProxyUrlInputView, {
|
2225
|
+
* proxies: [
|
2226
|
+
* {
|
2227
|
+
* url: 'http://example.com',
|
2228
|
+
* base_path: '/example'
|
2229
|
+
* }
|
2230
|
+
* ]
|
2231
|
+
* });
|
2232
|
+
*
|
2233
|
+
* @class
|
2234
|
+
*/
|
2235
|
+
|
2236
|
+
var ProxyUrlInputView = UrlInputView.extend(
|
2237
|
+
/** @lends ProxyUrlInputView.prototype */
|
2238
|
+
{
|
2239
|
+
// @override
|
2240
|
+
validateUrl: function validateUrl(url) {
|
2241
|
+
var view = this;
|
2242
|
+
return $.Deferred(function (deferred) {
|
2243
|
+
deferred.notify(I18n$1.t('pageflow.ui.views.inputs.proxy_url_input_view.url_validation'));
|
2244
|
+
$.ajax({
|
2245
|
+
url: view.rewriteUrl(url),
|
2246
|
+
dataType: 'html'
|
2247
|
+
}).done(deferred.resolve).fail(function (xhr) {
|
2248
|
+
deferred.reject(I18n$1.t('pageflow.ui.views.inputs.proxy_url_input_view.http_error', {
|
2249
|
+
status: xhr.status
|
2250
|
+
}));
|
2251
|
+
});
|
2252
|
+
}).promise();
|
2253
|
+
},
|
2254
|
+
// override
|
2255
|
+
transformPropertyValue: function transformPropertyValue(url) {
|
2256
|
+
return this.rewriteUrl(url);
|
2257
|
+
},
|
2258
|
+
// override
|
2259
|
+
supportedHosts: function supportedHosts() {
|
2260
|
+
return _.pluck(this.options.proxies, 'url');
|
2261
|
+
},
|
2262
|
+
rewriteUrl: function rewriteUrl(url) {
|
2263
|
+
_.each(this.options.proxies, function (proxy) {
|
2264
|
+
url = url.replace(new RegExp('^' + proxy.url + '/?'), proxy.base_path + '/');
|
2265
|
+
});
|
2266
|
+
|
2267
|
+
return url;
|
2268
|
+
}
|
2269
|
+
});
|
2270
|
+
|
2271
|
+
function template$a(data) {
|
2272
|
+
var __p = '';
|
2273
|
+
__p += '<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>\n<div class="value"></div>\n<div class="slider"></div>\n';
|
2274
|
+
return __p
|
2275
|
+
}
|
2276
|
+
|
2277
|
+
/**
|
2278
|
+
* A slider for numeric inputs.
|
2279
|
+
* See {@link inputView} for options
|
2280
|
+
*
|
2281
|
+
* @param {Object} [options]
|
2282
|
+
*
|
2283
|
+
* @class
|
2284
|
+
*/
|
2285
|
+
|
2286
|
+
var SliderInputView = Marionette.ItemView.extend({
|
2287
|
+
mixins: [inputView],
|
2288
|
+
className: 'slider_input',
|
2289
|
+
template: template$a,
|
2290
|
+
ui: {
|
2291
|
+
widget: '.slider',
|
2292
|
+
value: '.value'
|
2293
|
+
},
|
2294
|
+
events: {
|
2295
|
+
'slidechange': 'save'
|
2296
|
+
},
|
2297
|
+
onRender: function onRender() {
|
2298
|
+
this.ui.widget.slider({
|
2299
|
+
animate: 'fast',
|
2300
|
+
min: 'minValue' in this.options ? this.options.minValue : 0,
|
2301
|
+
max: 'maxValue' in this.options ? this.options.maxValue : 100
|
2302
|
+
});
|
2303
|
+
this.load();
|
2304
|
+
},
|
2305
|
+
save: function save() {
|
2306
|
+
var value = this.ui.widget.slider('option', 'value');
|
2307
|
+
var unit = 'unit' in this.options ? this.options.unit : '%';
|
2308
|
+
this.ui.value.text(value + unit);
|
2309
|
+
this.model.set(this.options.propertyName, value);
|
2310
|
+
},
|
2311
|
+
load: function load() {
|
2312
|
+
var value;
|
2313
|
+
|
2314
|
+
if (this.model.has(this.options.propertyName)) {
|
2315
|
+
value = this.model.get(this.options.propertyName);
|
2316
|
+
} else {
|
2317
|
+
value = 'defaultValue' in this.options ? this.options.defaultValue : 0;
|
2318
|
+
}
|
2319
|
+
|
2320
|
+
this.ui.widget.slider('option', 'value', value);
|
2321
|
+
}
|
2322
|
+
});
|
2323
|
+
|
2324
|
+
function template$b(data) {
|
2325
|
+
var __p = '';
|
2326
|
+
__p += '<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>\n\n<textarea></textarea>\n';
|
2327
|
+
return __p
|
2328
|
+
}
|
2329
|
+
|
2330
|
+
var JsonInputView = Marionette.ItemView.extend({
|
2331
|
+
mixins: [inputView],
|
2332
|
+
template: template$b,
|
2333
|
+
className: 'json_input',
|
2334
|
+
ui: {
|
2335
|
+
input: 'textarea'
|
2336
|
+
},
|
2337
|
+
events: {
|
2338
|
+
'change': 'onChange',
|
2339
|
+
'keyup': 'validate'
|
2340
|
+
},
|
2341
|
+
onRender: function onRender() {
|
2342
|
+
this.load();
|
2343
|
+
this.validate();
|
2344
|
+
this.listenTo(this.model, 'change:' + this.options.propertyName, this.load);
|
2345
|
+
},
|
2346
|
+
onChange: function onChange() {
|
2347
|
+
if (this.validate()) {
|
2348
|
+
this.save();
|
2349
|
+
}
|
2350
|
+
},
|
2351
|
+
onClose: function onClose() {
|
2352
|
+
if (this.validate()) {
|
2353
|
+
this.save();
|
2354
|
+
}
|
2355
|
+
},
|
2356
|
+
save: function save() {
|
2357
|
+
this.model.set(this.options.propertyName, this.ui.input.val() ? JSON.parse(this.ui.input.val()) : null);
|
2358
|
+
},
|
2359
|
+
load: function load() {
|
2360
|
+
var input = this.ui.input;
|
2361
|
+
var value = this.model.get(this.options.propertyName);
|
2362
|
+
input.val(value ? JSON.stringify(value, null, 2) : '');
|
2363
|
+
},
|
2364
|
+
validate: function validate() {
|
2365
|
+
var input = this.ui.input;
|
2366
|
+
|
2367
|
+
if (input.val() && !this.isValidJson(input.val())) {
|
2368
|
+
this.displayValidationError(I18n$1.t('pageflow.ui.views.inputs.json_input_view.invalid'));
|
2369
|
+
return false;
|
2370
|
+
} else {
|
2371
|
+
this.resetValidationError();
|
2372
|
+
return true;
|
2373
|
+
}
|
2374
|
+
},
|
2375
|
+
displayValidationError: function displayValidationError(message) {
|
2376
|
+
this.$el.addClass('invalid');
|
2377
|
+
this.ui.input.attr('title', message);
|
2378
|
+
},
|
2379
|
+
resetValidationError: function resetValidationError(message) {
|
2380
|
+
this.$el.removeClass('invalid');
|
2381
|
+
this.ui.input.attr('title', '');
|
2382
|
+
},
|
2383
|
+
isValidJson: function isValidJson(text) {
|
2384
|
+
try {
|
2385
|
+
JSON.parse(text);
|
2386
|
+
return true;
|
2387
|
+
} catch (e) {
|
2388
|
+
return false;
|
2389
|
+
}
|
2390
|
+
}
|
2391
|
+
});
|
2392
|
+
|
2393
|
+
function template$c(data) {
|
2394
|
+
var __p = '';
|
2395
|
+
__p += '<input type="checkbox" />\n<label>\n <span class="name"></span>\n <span class="inline_help"></span>\n</label>';
|
2396
|
+
return __p
|
2397
|
+
}
|
2398
|
+
|
2399
|
+
/**
|
2400
|
+
* Input view for boolean values.
|
2401
|
+
* See {@link inputView} for further options
|
2402
|
+
*
|
2403
|
+
* @param {Object} [options]
|
2404
|
+
*
|
2405
|
+
* @param {boolean} [options.displayUncheckedIfDisabled=false]
|
2406
|
+
* Ignore the attribute value if the input is disabled and display
|
2407
|
+
* an unchecked check box.
|
2408
|
+
*
|
2409
|
+
* @class
|
2410
|
+
*/
|
2411
|
+
|
2412
|
+
var CheckBoxInputView = Marionette.ItemView.extend({
|
2413
|
+
mixins: [inputView],
|
2414
|
+
template: template$c,
|
2415
|
+
className: 'check_box_input',
|
2416
|
+
events: {
|
2417
|
+
'change': 'save'
|
2418
|
+
},
|
2419
|
+
ui: {
|
2420
|
+
input: 'input',
|
2421
|
+
label: 'label'
|
2422
|
+
},
|
2423
|
+
onRender: function onRender() {
|
2424
|
+
this.ui.label.attr('for', this.cid);
|
2425
|
+
this.ui.input.attr('id', this.cid);
|
2426
|
+
this.load();
|
2427
|
+
this.listenTo(this.model, 'change:' + this.options.propertyName, this.load);
|
2428
|
+
},
|
2429
|
+
save: function save() {
|
2430
|
+
if (!this.options.disabled) {
|
2431
|
+
this.model.set(this.options.propertyName, this.ui.input.is(':checked'));
|
2432
|
+
}
|
2433
|
+
},
|
2434
|
+
load: function load() {
|
2435
|
+
if (!this.isClosed) {
|
2436
|
+
this.ui.input.prop('checked', this.displayValue());
|
2437
|
+
}
|
2438
|
+
},
|
2439
|
+
displayValue: function displayValue() {
|
2440
|
+
if (this.options.disabled && this.options.displayUncheckedIfDisabled) {
|
2441
|
+
return false;
|
2442
|
+
} else {
|
2443
|
+
return this.model.get(this.options.propertyName);
|
2444
|
+
}
|
2445
|
+
}
|
2446
|
+
});
|
2447
|
+
|
2448
|
+
/**
|
2449
|
+
* A table cell mapping column attribute values to a list of
|
2450
|
+
* translations.
|
2451
|
+
*
|
2452
|
+
* ## Attribute Translations
|
2453
|
+
*
|
2454
|
+
* The following attribute translations are used:
|
2455
|
+
*
|
2456
|
+
* - `.cell_text.<attribute_value>` - Used as cell content.
|
2457
|
+
* - `.cell_text.blank` - Used as cell content if attribute is blank.
|
2458
|
+
* - `.cell_title.<attribute_value>` - Used as title attribute.
|
2459
|
+
* - `.cell_title.blank` - Used as title attribute if attribute is blank.
|
2460
|
+
*
|
2461
|
+
* @since 12.0
|
2462
|
+
*/
|
2463
|
+
|
2464
|
+
var EnumTableCellView = TableCellView.extend({
|
2465
|
+
className: 'enum_table_cell',
|
2466
|
+
update: function update() {
|
2467
|
+
this.$el.text(this.attributeTranslation('cell_text.' + (this.attributeValue() || 'blank')));
|
2468
|
+
this.$el.attr('title', this.attributeTranslation('cell_title.' + (this.attributeValue() || 'blank'), {
|
2469
|
+
defaultValue: ''
|
2470
|
+
}));
|
2471
|
+
}
|
2472
|
+
});
|
2473
|
+
|
2474
|
+
function template$d(data) {
|
2475
|
+
var __t, __p = '';
|
2476
|
+
__p += '<a class="remove" title="' +
|
2477
|
+
((__t = ( I18n.t('pageflow.editor.templates.row.destroy') )) == null ? '' : __t) +
|
2478
|
+
'"></a>\n';
|
2479
|
+
return __p
|
2480
|
+
}
|
2481
|
+
|
2482
|
+
/**
|
2483
|
+
* A table cell providing a button which destroys the model that the
|
2484
|
+
* current row refers to.
|
2485
|
+
*
|
2486
|
+
* ## Attribute Translations
|
2487
|
+
*
|
2488
|
+
* The following attribute translation is used:
|
2489
|
+
*
|
2490
|
+
* - `.cell_title` - Used as title attribute.
|
2491
|
+
*
|
2492
|
+
* @param {Object} [options]
|
2493
|
+
*
|
2494
|
+
* @param {function} [options.toggleDeleteButton]
|
2495
|
+
* A function with boolean return value to be called on
|
2496
|
+
* this.getModel(). Delete button will be visible only if the
|
2497
|
+
* function returns a truthy value.
|
2498
|
+
*
|
2499
|
+
* @param {boolean} [options.invertToggleDeleteButton]
|
2500
|
+
* Invert the return value of `toggleDeleteButton`?
|
2501
|
+
*
|
2502
|
+
* @since 12.0
|
2503
|
+
*/
|
2504
|
+
|
2505
|
+
var DeleteRowTableCellView = TableCellView.extend({
|
2506
|
+
className: 'delete_row_table_cell',
|
2507
|
+
template: template$d,
|
2508
|
+
ui: {
|
2509
|
+
removeButton: '.remove'
|
2510
|
+
},
|
2511
|
+
events: {
|
2512
|
+
'click .remove': 'destroy',
|
2513
|
+
'click': function click() {
|
2514
|
+
return false;
|
2515
|
+
}
|
2516
|
+
},
|
2517
|
+
showButton: function showButton() {
|
2518
|
+
if (this.options.toggleDeleteButton) {
|
2519
|
+
var context = this.getModel();
|
2520
|
+
var toggle = context[this.options.toggleDeleteButton].apply(context);
|
2521
|
+
|
2522
|
+
if (this.options.invertToggleDeleteButton) {
|
2523
|
+
return !toggle;
|
2524
|
+
} else {
|
2525
|
+
return !!toggle;
|
2526
|
+
}
|
2527
|
+
} else {
|
2528
|
+
return true;
|
2529
|
+
}
|
2530
|
+
},
|
2531
|
+
update: function update() {
|
2532
|
+
this.ui.removeButton.toggleClass('remove', this.showButton());
|
2533
|
+
this.ui.removeButton.attr('title', this.attributeTranslation('cell_title'));
|
2534
|
+
},
|
2535
|
+
destroy: function destroy() {
|
2536
|
+
this.getModel().destroy();
|
2537
|
+
}
|
2538
|
+
});
|
2539
|
+
|
2540
|
+
/**
|
2541
|
+
* A table cell representing whether the column attribute is present
|
2542
|
+
* on the row model.
|
2543
|
+
*
|
2544
|
+
* ## Attribute Translations
|
2545
|
+
*
|
2546
|
+
* The following attribute translations are used:
|
2547
|
+
*
|
2548
|
+
* - `.cell_title.present` - Used as title attribute if the attribute
|
2549
|
+
* is present. The current attribute value is provided as
|
2550
|
+
* interpolation `%{value}`.
|
2551
|
+
* - `.cell_title.blank` - Used as title attribute if the
|
2552
|
+
* attribute is blank.
|
2553
|
+
*
|
2554
|
+
* @since 12.0
|
2555
|
+
*/
|
2556
|
+
|
2557
|
+
var PresenceTableCellView = TableCellView.extend({
|
2558
|
+
className: 'presence_table_cell',
|
2559
|
+
update: function update() {
|
2560
|
+
var isPresent = !!this.attributeValue();
|
2561
|
+
this.$el.attr('title', isPresent ? this.attributeTranslation('cell_title.present', {
|
2562
|
+
value: this.attributeValue()
|
2563
|
+
}) : this.attributeTranslation('cell_title.blank'));
|
2564
|
+
this.$el.toggleClass('is_present', isPresent);
|
2565
|
+
}
|
2566
|
+
});
|
2567
|
+
|
2568
|
+
/**
|
2569
|
+
* A table cell mapping column attribute values to icons.
|
2570
|
+
*
|
2571
|
+
* ## Attribute Translations
|
2572
|
+
*
|
2573
|
+
* The following attribute translations are used:
|
2574
|
+
*
|
2575
|
+
* - `.cell_title.<attribute_value>` - Used as title attribute.
|
2576
|
+
* - `.cell_title.blank` - Used as title attribute if attribute is blank.
|
2577
|
+
*
|
2578
|
+
* @param {Object} [options]
|
2579
|
+
*
|
2580
|
+
* @param {string[]} [options.icons]
|
2581
|
+
* An array of all possible attribute values to be mapped to HTML
|
2582
|
+
* classes of the same name. A global mapping from those classes to
|
2583
|
+
* icon mixins is provided in
|
2584
|
+
* pageflow/ui/table_cells/icon_table_cell.scss.
|
2585
|
+
*
|
2586
|
+
* @since 12.0
|
2587
|
+
*/
|
2588
|
+
|
2589
|
+
var IconTableCellView = TableCellView.extend({
|
2590
|
+
className: 'icon_table_cell',
|
2591
|
+
update: function update() {
|
2592
|
+
var icon = this.attributeValue();
|
2593
|
+
var isPresent = !!this.attributeValue();
|
2594
|
+
this.removeExistingIcons();
|
2595
|
+
this.$el.attr('title', isPresent ? this.attributeTranslation('cell_title.' + icon, {
|
2596
|
+
value: this.attributeValue()
|
2597
|
+
}) : this.attributeTranslation('cell_title.blank'));
|
2598
|
+
this.$el.addClass(icon);
|
2599
|
+
},
|
2600
|
+
removeExistingIcons: function removeExistingIcons() {
|
2601
|
+
this.$el.removeClass(this.options.icons.join(' '));
|
2602
|
+
}
|
2603
|
+
});
|
2604
|
+
|
2605
|
+
/**
|
2606
|
+
* A table cell using the row model's value of the column attribute as
|
2607
|
+
* text. If attribute value is empty, use most specific default
|
2608
|
+
* available.
|
2609
|
+
*
|
2610
|
+
* @param {Object} [options]
|
2611
|
+
*
|
2612
|
+
* @param {function|string} [options.column.default]
|
2613
|
+
* A function returning a default value for display if attribute
|
2614
|
+
* value is empty.
|
2615
|
+
*
|
2616
|
+
* @param {string} [options.column.contentBinding]
|
2617
|
+
* If this is provided, the function `options.column.default`
|
2618
|
+
* receives the values of `options.column.contentBinding` and of
|
2619
|
+
* this.getModel() via its options hash. No-op if
|
2620
|
+
* `options.column.default` is not a function.
|
2621
|
+
*
|
2622
|
+
* @since 12.0
|
2623
|
+
*/
|
2624
|
+
|
2625
|
+
var TextTableCellView = TableCellView.extend({
|
2626
|
+
className: 'text_table_cell',
|
2627
|
+
update: function update() {
|
2628
|
+
this.$el.text(this._updateText());
|
2629
|
+
},
|
2630
|
+
_updateText: function _updateText() {
|
2631
|
+
if (this.attributeValue()) {
|
2632
|
+
return this.attributeValue();
|
2633
|
+
} else if (typeof this.options.column["default"] === 'function') {
|
2634
|
+
var options = {};
|
2635
|
+
|
2636
|
+
if (this.options.column.contentBinding) {
|
2637
|
+
options = {
|
2638
|
+
contentBinding: this.options.column.contentBinding,
|
2639
|
+
model: this.getModel()
|
2640
|
+
};
|
2641
|
+
}
|
2642
|
+
|
2643
|
+
return this.options.column["default"](options);
|
2644
|
+
} else if ('default' in this.options.column) {
|
2645
|
+
return this.options.column["default"];
|
2646
|
+
} else {
|
2647
|
+
return I18n$1.t('pageflow.ui.text_table_cell_view.empty');
|
2648
|
+
}
|
2649
|
+
}
|
2650
|
+
});
|
2651
|
+
|
2652
|
+
var subviewContainer = {
|
2653
|
+
subview: function subview(view) {
|
2654
|
+
this.subviews = this.subviews || new ChildViewContainer();
|
2655
|
+
this.subviews.add(view.render());
|
2656
|
+
return view;
|
2657
|
+
},
|
2658
|
+
appendSubview: function appendSubview(view) {
|
2659
|
+
return this.$el.append(this.subview(view).el);
|
2660
|
+
},
|
2661
|
+
onClose: function onClose() {
|
2662
|
+
if (this.subviews) {
|
2663
|
+
this.subviews.call('close');
|
2664
|
+
}
|
2665
|
+
}
|
2666
|
+
};
|
2667
|
+
Cocktail.mixin(Marionette.View, subviewContainer);
|
2668
|
+
|
2669
|
+
var tooltipContainer = {
|
2670
|
+
events: {
|
2671
|
+
'mouseover [data-tooltip]': function mouseoverDataTooltip(event) {
|
2672
|
+
if (!this.tooltip.visible) {
|
2673
|
+
var target = $(event.target);
|
2674
|
+
var key = target.data('tooltip');
|
2675
|
+
var position;
|
2676
|
+
|
2677
|
+
if (target.data('tooltipAlign') === 'bottom left') {
|
2678
|
+
position = {
|
2679
|
+
left: target.position().left,
|
2680
|
+
top: target.position().top + target.outerHeight()
|
2681
|
+
};
|
2682
|
+
} else if (target.data('tooltipAlign') === 'bottom right') {
|
2683
|
+
position = {
|
2684
|
+
left: target.position().left + target.outerWidth(),
|
2685
|
+
top: target.position().top + target.outerHeight()
|
2686
|
+
};
|
2687
|
+
} else {
|
2688
|
+
position = {
|
2689
|
+
left: target.position().left + target.outerWidth(),
|
2690
|
+
top: target.position().top + target.outerHeight() / 2
|
2691
|
+
};
|
2692
|
+
}
|
2693
|
+
|
2694
|
+
this.tooltip.show(I18n$1.t(key), position, {
|
2695
|
+
align: target.data('tooltipAlign')
|
2696
|
+
});
|
2697
|
+
}
|
2698
|
+
},
|
2699
|
+
'mouseout [data-tooltip]': function mouseoutDataTooltip() {
|
2700
|
+
this.tooltip.hide();
|
2701
|
+
}
|
2702
|
+
},
|
2703
|
+
onRender: function onRender() {
|
2704
|
+
this.appendSubview(this.tooltip = new TooltipView());
|
2705
|
+
}
|
2706
|
+
};
|
2707
|
+
|
2708
|
+
export { CheckBoxGroupInputView, CheckBoxInputView, CollectionView, ColorInputView, ConfigurationEditorTabView, ConfigurationEditorView, DeleteRowTableCellView, EnumTableCellView, ExtendedSelectInputView, IconTableCellView, JsonInputView, BaseObject as Object, PresenceTableCellView, ProxyUrlInputView, SelectInputView, SliderInputView, SortableCollectionView, TableCellView, TableHeaderCellView, TableRowView, TableView, TabsView, TextAreaInputView, TextInputView, TextTableCellView, TooltipView, UrlDisplayView, UrlInputView, cssModulesUtils, i18nUtils, inputView, inputWithPlaceholderText, subviewContainer, tooltipContainer };
|