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.

@@ -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 };