From 1f44a1001f360427ade9d8733df61e0aba9cba82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Wed, 24 Apr 2013 23:43:22 -0400 Subject: [PATCH] Issue #1678002-79 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit fc7e7b6a03bb32d4ef892ab1e8a35ac0ef9115de Author: J. Renée Beach Date: Wed Apr 24 23:35:52 2013 -0400 Positioning is now much better Signed-off-by: J. Renée Beach commit a09f4b09ff33f4943d4f4f45aa8861578455458e Author: J. Renée Beach Date: Wed Apr 24 23:24:57 2013 -0400 Deleted EntityView Signed-off-by: J. Renée Beach commit c46abca01bdc5d974f0c9327293f834079301955 Author: J. Renée Beach Date: Wed Apr 24 23:08:34 2013 -0400 Updated the EntityModel Signed-off-by: J. Renée Beach commit 31e0065fb96e252f041f63bc5a58b1e46fbd0609 Author: J. Renée Beach Date: Wed Apr 24 23:05:40 2013 -0400 More cleanups of EntityToolbarView. Signed-off-by: J. Renée Beach commit 7dc0be2615a1106ecd279838795ad794683af6cb Author: J. Renée Beach Date: Wed Apr 24 22:55:53 2013 -0400 Cleaning up the entity toolbar Signed-off-by: J. Renée Beach commit 16dd9fe61dfc95c2ba07e9ececbcdd96b8a5feb1 Author: J. Renée Beach Date: Wed Apr 24 22:51:35 2013 -0400 Deleting a lot of files that came back. Signed-off-by: J. Renée Beach commit aee06c8264ec843950ea0fb3ea2e016be02292ee Author: J. Renée Beach Date: Wed Apr 24 22:47:23 2013 -0400 Cleaned up some more EntityToolbarView stuff Signed-off-by: J. Renée Beach commit 75e707ff590fece508ce259900cd5940ae11bda4 Author: J. Renée Beach Date: Wed Apr 24 17:23:51 2013 -0400 Turned the Toolbar's el into the toolbar element itself. Signed-off-by: J. Renée Beach commit 5e96d6a674cc15ccd072ae590aba0995fabc4d8d Author: J. Renée Beach Date: Wed Apr 24 17:04:38 2013 -0400 Save and cancel buttons show up. Signed-off-by: J. Renée Beach commit 148af7ddec3f20d8fc39a3c99b6088ee15cc72e5 Author: J. Renée Beach Date: Wed Apr 24 15:26:52 2013 -0400 EntityToolbar is now listening to changes on the active entity. Signed-off-by: J. Renée Beach Conflicts: core/modules/edit/js/edit.js core/modules/edit/js/models/EntityModel.js core/modules/edit/js/views/ToolbarView.js commit b612f8355d0627d9d2c7b13bc2e66b0d4ead210e Author: J. Renée Beach Date: Wed Apr 24 14:45:25 2013 -0400 Entity toolbar now knows about its fields. Signed-off-by: J. Renée Beach Conflicts: core/modules/edit/js/edit.js core/modules/edit/js/models/EntityModel.js core/modules/edit/js/models/FieldModel.js commit 814eaf8a8568d1aaed1bf9069822f57d7306f2ce Author: J. Renée Beach Date: Wed Apr 24 12:17:56 2013 -0400 We've got a zippy entity toolbar. Signed-off-by: J. Renée Beach commit fd5a0487282905f40caf16db30e4e60e000d38da Author: J. Renée Beach Date: Wed Apr 24 12:03:58 2013 -0400 First steps to an entity Toolbar Signed-off-by: J. Renée Beach Conflicts: core/modules/edit/js/edit.js commit 334a5d9b7a2fcafce9354a4ca79e60f4d7774748 Author: J. Renée Beach Date: Wed Apr 24 16:24:04 2013 -0400 1678002-77 Signed-off-by: J. Renée Beach --- core/misc/create/create-editonly.js | 1651 -------------------- core/modules/edit/css/edit.css | 13 +- core/modules/edit/edit.module | 31 +- core/modules/edit/js/app.js | 391 ----- core/modules/edit/js/backbone.drupalform.js | 2 +- core/modules/edit/js/createjs/editable.js | 30 - .../editingWidgets/drupalcontenteditablewidget.js | 83 - .../edit/js/createjs/editingWidgets/formwidget.js | 152 -- core/modules/edit/js/createjs/storage.js | 11 - core/modules/edit/js/edit.js | 270 +++- core/modules/edit/js/editors/directEditor.js | 89 ++ core/modules/edit/js/editors/formEditor.js | 220 +++ core/modules/edit/js/models/AppModel.js | 23 + core/modules/edit/js/models/EntityModel.js | 74 + core/modules/edit/js/models/FieldModel.js | 119 ++ core/modules/edit/js/models/edit-app-model.js | 21 - core/modules/edit/js/storage.js | 518 ++++++ core/modules/edit/js/theme.js | 17 + core/modules/edit/js/viejs/EditService.js | 289 ---- core/modules/edit/js/views/AppView.js | 292 ++++ core/modules/edit/js/views/ContextualLinkView.js | 75 + core/modules/edit/js/views/EntityToolbarView.js | 333 ++++ core/modules/edit/js/views/ModalView.js | 80 + .../edit/js/views/PropertyEditorDecorationView.js | 393 +++++ core/modules/edit/js/views/ToolbarView.js | 194 +++ core/modules/edit/js/views/contextuallink-view.js | 109 -- core/modules/edit/js/views/modal-view.js | 83 - .../edit/js/views/propertyeditordecoration-view.js | 363 ----- core/modules/edit/js/views/toolbar-view.js | 490 ------ core/modules/editor/js/editor.createjs.js | 64 +- core/modules/editor/js/editor.js | 8 +- core/modules/system/system.module | 14 - 32 files changed, 2718 insertions(+), 3784 deletions(-) delete mode 100644 core/misc/create/create-editonly.js delete mode 100644 core/modules/edit/js/app.js delete mode 100644 core/modules/edit/js/createjs/editable.js delete mode 100644 core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js delete mode 100644 core/modules/edit/js/createjs/editingWidgets/formwidget.js delete mode 100644 core/modules/edit/js/createjs/storage.js create mode 100644 core/modules/edit/js/editors/directEditor.js create mode 100644 core/modules/edit/js/editors/formEditor.js create mode 100644 core/modules/edit/js/models/AppModel.js create mode 100644 core/modules/edit/js/models/EntityModel.js create mode 100644 core/modules/edit/js/models/FieldModel.js delete mode 100644 core/modules/edit/js/models/edit-app-model.js create mode 100644 core/modules/edit/js/storage.js delete mode 100644 core/modules/edit/js/viejs/EditService.js create mode 100644 core/modules/edit/js/views/AppView.js create mode 100644 core/modules/edit/js/views/ContextualLinkView.js create mode 100644 core/modules/edit/js/views/EntityToolbarView.js create mode 100644 core/modules/edit/js/views/ModalView.js create mode 100644 core/modules/edit/js/views/PropertyEditorDecorationView.js create mode 100644 core/modules/edit/js/views/ToolbarView.js delete mode 100644 core/modules/edit/js/views/contextuallink-view.js delete mode 100644 core/modules/edit/js/views/modal-view.js delete mode 100644 core/modules/edit/js/views/propertyeditordecoration-view.js delete mode 100644 core/modules/edit/js/views/toolbar-view.js diff --git a/core/misc/create/create-editonly.js b/core/misc/create/create-editonly.js deleted file mode 100644 index aed84a4..0000000 --- a/core/misc/create/create-editonly.js +++ /dev/null @@ -1,1651 +0,0 @@ -// Create.js - On-site web editing interface -// (c) 2011-2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false window:false console:false */ - 'use strict'; - - // # Widget for adding items to a collection - jQuery.widget('Midgard.midgardCollectionAdd', { - options: { - editingWidgets: null, - collection: null, - model: null, - definition: null, - view: null, - disabled: false, - vie: null, - editableOptions: null, - templates: { - button: '' - } - }, - - _create: function () { - this.addButtons = []; - var widget = this; - if (!widget.options.collection.localStorage) { - try { - widget.options.collection.url = widget.options.model.url(); - } catch (e) { - if (window.console) { - console.log(e); - } - } - } - - widget.options.collection.on('add', function (model) { - model.primaryCollection = widget.options.collection; - widget.options.vie.entities.add(model); - model.collection = widget.options.collection; - }); - - // Re-check collection constraints - widget.options.collection.on('add remove reset', widget.checkCollectionConstraints, widget); - - widget._bindCollectionView(widget.options.view); - }, - - _bindCollectionView: function (view) { - var widget = this; - view.on('add', function (itemView) { - itemView.$el.effect('slide', function () { - widget._makeEditable(itemView); - }); - }); - }, - - _makeEditable: function (itemView) { - this.options.editableOptions.disabled = this.options.disabled; - this.options.editableOptions.model = itemView.model; - itemView.$el.midgardEditable(this.options.editableOptions); - }, - - _init: function () { - if (this.options.disabled) { - this.disable(); - return; - } - this.enable(); - }, - - hideButtons: function () { - _.each(this.addButtons, function (button) { - button.hide(); - }); - }, - - showButtons: function () { - _.each(this.addButtons, function (button) { - button.show(); - }); - }, - - checkCollectionConstraints: function () { - if (this.options.disabled) { - return; - } - - if (!this.options.view.canAdd()) { - this.hideButtons(); - return; - } - - if (!this.options.definition) { - // We have now information on the constraints applying to this collection - this.showButtons(); - return; - } - - if (!this.options.definition.max || this.options.definition.max === -1) { - // No maximum constraint - this.showButtons(); - return; - } - - if (this.options.collection.length < this.options.definition.max) { - this.showButtons(); - return; - } - // Collection is already full by its definition - this.hideButtons(); - }, - - enable: function () { - var widget = this; - - var addButton = jQuery(_.template(this.options.templates.button, { - icon: 'plus', - label: this.options.editableOptions.localize('Add', this.options.editableOptions.language) - })).button(); - addButton.addClass('midgard-create-add'); - addButton.click(function () { - widget.addItem(addButton); - }); - jQuery(widget.options.view.el).after(addButton); - - widget.addButtons.push(addButton); - widget.checkCollectionConstraints(); - }, - - disable: function () { - _.each(this.addButtons, function (button) { - button.remove(); - }); - this.addButtons = []; - }, - - _getTypeActions: function (options) { - var widget = this; - var actions = []; - _.each(this.options.definition.range, function (type) { - var nsType = widget.options.collection.vie.namespaces.uri(type); - if (!widget.options.view.canAdd(nsType)) { - return; - } - actions.push({ - name: type, - label: type, - cb: function () { - widget.options.collection.add({ - '@type': type - }, options); - }, - className: 'create-ui-btn' - }); - }); - return actions; - }, - - addItem: function (button, options) { - if (options === undefined) { - options = {}; - } - var addOptions = _.extend({}, options, { validate: false }); - - var itemData = {}; - if (this.options.definition && this.options.definition.range) { - if (this.options.definition.range.length === 1) { - // Items can be of single type, add that - itemData['@type'] = this.options.definition.range[0]; - } else { - // Ask user which type to add - jQuery('body').midgardNotifications('create', { - bindTo: button, - gravity: 'L', - body: this.options.editableOptions.localize('Choose type to add', this.options.editableOptions.language), - timeout: 0, - actions: this._getTypeActions(addOptions) - }); - return; - } - } else { - // Check the view templates for possible non-Thing type to use - var keys = _.keys(this.options.view.templates); - if (keys.length == 2) { - itemData['@type'] = keys[0]; - } - } - this.options.collection.add(itemData, addOptions); - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2011-2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false window:false console:false */ - 'use strict'; - - // # Widget for adding items anywhere inside a collection - jQuery.widget('Midgard.midgardCollectionAddBetween', jQuery.Midgard.midgardCollectionAdd, { - _bindCollectionView: function (view) { - var widget = this; - view.on('add', function (itemView) { - //itemView.el.effect('slide'); - widget._makeEditable(itemView); - widget._refreshButtons(); - }); - view.on('remove', function () { - widget._refreshButtons(); - }); - }, - - _refreshButtons: function () { - var widget = this; - window.setTimeout(function () { - widget.disable(); - widget.enable(); - }, 1); - }, - - prepareButton: function (index) { - var widget = this; - var addButton = jQuery(_.template(this.options.templates.button, { - icon: 'plus', - label: '' - })).button(); - addButton.addClass('midgard-create-add'); - addButton.click(function () { - widget.addItem(addButton, { - at: index - }); - }); - return addButton; - }, - - enable: function () { - var widget = this; - - var firstAddButton = widget.prepareButton(0); - jQuery(widget.options.view.el).prepend(firstAddButton); - widget.addButtons.push(firstAddButton); - jQuery.each(widget.options.view.entityViews, function (cid, view) { - var index = widget.options.collection.indexOf(view.model); - var addButton = widget.prepareButton(index + 1); - jQuery(view.el).append(addButton); - widget.addButtons.push(addButton); - }); - - this.checkCollectionConstraints(); - }, - - disable: function () { - var widget = this; - jQuery.each(widget.addButtons, function (idx, button) { - button.remove(); - }); - widget.addButtons = []; - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2011-2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false window:false VIE:false */ - 'use strict'; - - // Define Create's EditableEntity widget. - jQuery.widget('Midgard.midgardEditable', { - options: { - propertyEditors: {}, - collections: [], - model: null, - // the configuration (mapping and options) of property editor widgets - propertyEditorWidgetsConfiguration: { - hallo: { - widget: 'halloWidget', - options: {} - } - }, - // the available property editor widgets by data type - propertyEditorWidgets: { - 'default': 'hallo' - }, - collectionWidgets: { - 'default': 'midgardCollectionAdd' - }, - toolbarState: 'full', - vie: null, - domService: 'rdfa', - predicateSelector: '[property]', - disabled: false, - localize: function (id, language) { - return window.midgardCreate.localize(id, language); - }, - language: null, - // Current state of the Editable - state: null, - // Callback function for validating changes between states. Receives the previous state, new state, possibly property, and a callback - acceptStateChange: true, - // Callback function for listening (and reacting) to state changes. - stateChange: null, - // Callback function for decorating the full editable. Will be called on instantiation - decorateEditableEntity: null, - // Callback function for decorating a single property editor widget. Will - // be called on editing widget instantiation. - decoratePropertyEditor: null, - - // Deprecated. - editables: [], // Now `propertyEditors`. - editors: {}, // Now `propertyEditorWidgetsConfiguration`. - widgets: {} // Now `propertyEditorW - }, - - // Aids in consistently passing parameters to events and callbacks. - _params: function(predicate, extended) { - var entityParams = { - entity: this.options.model, - editableEntity: this, - entityElement: this.element, - - // Deprecated. - editable: this, - element: this.element, - instance: this.options.model - }; - var propertyParams = (predicate) ? { - predicate: predicate, - propertyEditor: this.options.propertyEditors[predicate], - propertyElement: this.options.propertyEditors[predicate].element, - - // Deprecated. - property: predicate, - element: this.options.propertyEditors[predicate].element - } : {}; - - return _.extend(entityParams, propertyParams, extended); - }, - - _create: function () { - // Backwards compatibility: - // - this.options.propertyEditorWidgets used to be this.options.widgets - // - this.options.propertyEditorWidgetsConfiguration used to be - // this.options.editors - if (this.options.widgets) { - this.options.propertyEditorWidgets = _.extend(this.options.propertyEditorWidgets, this.options.widgets); - } - if (this.options.editors) { - this.options.propertyEditorWidgetsConfiguration = _.extend(this.options.propertyEditorWidgetsConfiguration, this.options.editors); - } - - this.vie = this.options.vie; - this.domService = this.vie.service(this.options.domService); - if (!this.options.model) { - var widget = this; - this.vie.load({ - element: this.element - }).from(this.options.domService).execute().done(function (entities) { - widget.options.model = entities[0]; - }); - } - if (_.isFunction(this.options.decorateEditableEntity)) { - this.options.decorateEditableEntity(this._params()); - } - }, - - _init: function () { - // Backwards compatibility: - // - this.options.propertyEditorWidgets used to be this.options.widgets - // - this.options.propertyEditorWidgetsConfiguration used to be - // this.options.editors - if (this.options.widgets) { - this.options.propertyEditorWidgets = _.extend(this.options.propertyEditorWidgets, this.options.widgets); - } - if (this.options.editors) { - this.options.propertyEditorWidgetsConfiguration = _.extend(this.options.propertyEditorWidgetsConfiguration, this.options.editors); - } - - // Old way of setting the widget inactive - if (this.options.disabled === true) { - this.setState('inactive'); - return; - } - - if (this.options.disabled === false && this.options.state === 'inactive') { - this.setState('candidate'); - return; - } - this.options.disabled = false; - - if (this.options.state) { - this.setState(this.options.state); - return; - } - this.setState('candidate'); - }, - - // Method used for cycling between the different states of the Editable widget: - // - // * Inactive: editable is loaded but disabled - // * Candidate: editable is enabled but not activated - // * Highlight: user is hovering over the editable (not set by Editable widget directly) - // * Activating: an editor widget is being activated for user to edit with it (skipped for editors that activate instantly) - // * Active: user is actually editing something inside the editable - // * Changed: user has made changes to the editable - // * Invalid: the contents of the editable have validation errors - // - // In situations where state changes are triggered for a particular property editor, the `predicate` - // argument will provide the name of that property. - // - // State changes may carry optional context information in a JavaScript object. The payload of these context objects is not - // standardized, and is meant to be set and used by the application controller - // - // The callback parameter is optional and will be invoked after a state change has been accepted (after the 'statechange' - // event) or rejected. - setState: function (state, predicate, context, callback) { - var previous = this.options.state; - var current = state; - if (current === previous) { - return; - } - - if (this.options.acceptStateChange === undefined || !_.isFunction(this.options.acceptStateChange)) { - // Skip state transition validation - this._doSetState(previous, current, predicate, context); - if (_.isFunction(callback)) { - callback(true); - } - return; - } - - var widget = this; - this.options.acceptStateChange(previous, current, predicate, context, function (accepted) { - if (accepted) { - widget._doSetState(previous, current, predicate, context); - } - if (_.isFunction(callback)) { - callback(accepted); - } - return; - }); - }, - - getState: function () { - return this.options.state; - }, - - _doSetState: function (previous, current, predicate, context) { - this.options.state = current; - if (current === 'inactive') { - this.disable(); - } else if ((previous === null || previous === 'inactive') && current !== 'inactive') { - this.enable(); - } - - this._trigger('statechange', null, this._params(predicate, { - previous: previous, - current: current, - context: context - })); - }, - - findEditablePredicateElements: function (callback) { - this.domService.findPredicateElements(this.options.model.id, jQuery(this.options.predicateSelector, this.element), false).each(callback); - }, - - getElementPredicate: function (element) { - return this.domService.getElementPredicate(element); - }, - - enable: function () { - var editableEntity = this; - if (!this.options.model) { - return; - } - - this.findEditablePredicateElements(function () { - editableEntity._enablePropertyEditor(jQuery(this)); - }); - - this._trigger('enable', null, this._params()); - - if (!this.vie.view || !this.vie.view.Collection) { - return; - } - - _.each(this.domService.views, function (view) { - if (view instanceof this.vie.view.Collection && this.options.model === view.owner) { - var predicate = view.collection.predicate; - var editableOptions = _.clone(this.options); - editableOptions.state = null; - var collection = this.enableCollection({ - model: this.options.model, - collection: view.collection, - property: predicate, - definition: this.getAttributeDefinition(predicate), - view: view, - element: view.el, - vie: editableEntity.vie, - editableOptions: editableOptions - }); - editableEntity.options.collections.push(collection); - } - }, this); - }, - - disable: function () { - _.each(this.options.propertyEditors, function (editable) { - this.disablePropertyEditor({ - widget: this, - editable: editable, - entity: this.options.model, - element: editable.element - }); - }, this); - this.options.propertyEditors = {}; - - // Deprecated. - this.options.editables = []; - - _.each(this.options.collections, function (collectionWidget) { - var editableOptions = _.clone(this.options); - editableOptions.state = 'inactive'; - this.disableCollection({ - widget: this, - model: this.options.model, - element: collectionWidget, - vie: this.vie, - editableOptions: editableOptions - }); - }, this); - this.options.collections = []; - - this._trigger('disable', null, this._params()); - }, - - _enablePropertyEditor: function (element) { - var widget = this; - var predicate = this.getElementPredicate(element); - if (!predicate) { - return true; - } - if (this.options.model.get(predicate) instanceof Array) { - // For now we don't deal with multivalued properties in the editable - return true; - } - - var propertyElement = this.enablePropertyEditor({ - widget: this, - element: element, - entity: this.options.model, - property: predicate, - vie: this.vie, - decorate: this.options.decoratePropertyEditor, - decorateParams: _.bind(this._params, this), - changed: function (content) { - widget.setState('changed', predicate); - - var changedProperties = {}; - changedProperties[predicate] = content; - widget.options.model.set(changedProperties, { - silent: true - }); - - widget._trigger('changed', null, widget._params(predicate)); - }, - activating: function () { - widget.setState('activating', predicate); - }, - activated: function () { - widget.setState('active', predicate); - widget._trigger('activated', null, widget._params(predicate)); - }, - deactivated: function () { - widget.setState('candidate', predicate); - widget._trigger('deactivated', null, widget._params(predicate)); - } - }); - - if (!propertyElement) { - return; - } - var widgetType = propertyElement.data('createWidgetName'); - this.options.propertyEditors[predicate] = propertyElement.data('Midgard-' + widgetType); - - // Deprecated. - this.options.editables.push(propertyElement); - - this._trigger('enableproperty', null, this._params(predicate)); - }, - - // returns the name of the property editor widget to use for the given property - _propertyEditorName: function (data) { - if (this.options.propertyEditorWidgets[data.property] !== undefined) { - // Property editor widget configuration set for specific RDF predicate - return this.options.propertyEditorWidgets[data.property]; - } - - // Load the property editor widget configuration for the data type - var propertyType = 'default'; - var attributeDefinition = this.getAttributeDefinition(data.property); - if (attributeDefinition) { - propertyType = attributeDefinition.range[0]; - } - if (this.options.propertyEditorWidgets[propertyType] !== undefined) { - return this.options.propertyEditorWidgets[propertyType]; - } - return this.options.propertyEditorWidgets['default']; - }, - - _propertyEditorWidget: function (editor) { - return this.options.propertyEditorWidgetsConfiguration[editor].widget; - }, - - _propertyEditorOptions: function (editor) { - return this.options.propertyEditorWidgetsConfiguration[editor].options; - }, - - getAttributeDefinition: function (property) { - var type = this.options.model.get('@type'); - if (!type) { - return; - } - if (!type.attributes) { - return; - } - return type.attributes.get(property); - }, - - // Deprecated. - enableEditor: function (data) { - return this.enablePropertyEditor(data); - }, - - enablePropertyEditor: function (data) { - var editorName = this._propertyEditorName(data); - if (editorName === null) { - return; - } - - var editorWidget = this._propertyEditorWidget(editorName); - - data.editorOptions = this._propertyEditorOptions(editorName); - data.toolbarState = this.options.toolbarState; - data.disabled = false; - // Pass metadata that could be useful for some implementations. - data.editorName = editorName; - data.editorWidget = editorWidget; - - if (typeof jQuery(data.element)[editorWidget] !== 'function') { - throw new Error(editorWidget + ' widget is not available'); - } - - jQuery(data.element)[editorWidget](data); - jQuery(data.element).data('createWidgetName', editorWidget); - return jQuery(data.element); - }, - - // Deprecated. - disableEditor: function (data) { - return this.disablePropertyEditor(data); - }, - - disablePropertyEditor: function (data) { - data.element[data.editable.widgetName]({ - disabled: true - }); - jQuery(data.element).removeClass('ui-state-disabled'); - - if (data.element.is(':focus')) { - data.element.blur(); - } - }, - - collectionWidgetName: function (data) { - if (this.options.collectionWidgets[data.property] !== undefined) { - // Widget configuration set for specific RDF predicate - return this.options.collectionWidgets[data.property]; - } - - var propertyType = 'default'; - var attributeDefinition = this.getAttributeDefinition(data.property); - if (attributeDefinition) { - propertyType = attributeDefinition.range[0]; - } - if (this.options.collectionWidgets[propertyType] !== undefined) { - return this.options.collectionWidgets[propertyType]; - } - return this.options.collectionWidgets['default']; - }, - - enableCollection: function (data) { - var widgetName = this.collectionWidgetName(data); - if (widgetName === null) { - return; - } - data.disabled = false; - if (typeof jQuery(data.element)[widgetName] !== 'function') { - throw new Error(widgetName + ' widget is not available'); - } - jQuery(data.element)[widgetName](data); - jQuery(data.element).data('createCollectionWidgetName', widgetName); - return jQuery(data.element); - }, - - disableCollection: function (data) { - var widgetName = jQuery(data.element).data('createCollectionWidgetName'); - if (widgetName === null) { - return; - } - data.disabled = true; - if (widgetName) { - // only if there has been an editing widget registered - jQuery(data.element)[widgetName](data); - jQuery(data.element).removeClass('ui-state-disabled'); - } - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Tobias Herrmann, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false */ - 'use strict'; - - // # Base property editor widget - // - // This property editor widget provides a very simplistic `contentEditable` - // property editor that can be used as standalone, but should more usually be - // used as the base class for other property editor widgets. - // This property editor widget is only useful for textual properties! - // - // Subclassing this base property editor widget is easy: - // - // jQuery.widget('Namespace.MyWidget', jQuery.Create.editWidget, { - // // override any properties - // }); - jQuery.widget('Midgard.editWidget', { - options: { - disabled: false, - vie: null - }, - // override to enable the widget - enable: function () { - this.element.attr('contenteditable', 'true'); - }, - // override to disable the widget - disable: function (disable) { - this.element.attr('contenteditable', 'false'); - }, - // called by the jQuery UI plugin factory when creating the property editor - // widget instance - _create: function () { - this._registerWidget(); - this._initialize(); - - if (_.isFunction(this.options.decorate) && _.isFunction(this.options.decorateParams)) { - // TRICKY: we can't use this.options.decorateParams()'s 'propertyName' - // parameter just yet, because it will only be available after this - // object has been created, but we're currently in the constructor! - // Hence we have to duplicate part of its logic here. - this.options.decorate(this.options.decorateParams(null, { - propertyName: this.options.property, - propertyEditor: this, - propertyElement: this.element, - // Deprecated. - editor: this, - predicate: this.options.property, - element: this.element - })); - } - }, - // called every time the property editor widget is called - _init: function () { - if (this.options.disabled) { - this.disable(); - return; - } - this.enable(); - }, - // override this function to initialize the property editor widget functions - _initialize: function () { - var self = this; - this.element.on('focus', function () { - if (self.options.disabled) { - return; - } - self.options.activated(); - }); - this.element.on('blur', function () { - if (self.options.disabled) { - return; - } - self.options.deactivated(); - }); - var before = this.element.html(); - this.element.on('keyup paste', function (event) { - if (self.options.disabled) { - return; - } - var current = jQuery(this).html(); - if (before !== current) { - before = current; - self.options.changed(current); - } - }); - }, - // used to register the property editor widget name with the DOM element - _registerWidget: function () { - this.element.data("createWidgetName", this.widgetName); - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Tobias Herrmann, IKS Consortium -// (c) 2011 Rene Kapusta, Evo42 -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false Aloha:false */ - 'use strict'; - - // # Aloha editing widget - // - // This widget allows editing textual contents using the - // [Aloha](http://aloha-editor.org) rich text editor. - // - // Due to licensing incompatibilities, Aloha Editor needs to be installed - // and configured separately. - jQuery.widget('Midgard.alohaWidget', jQuery.Midgard.editWidget, { - _initialize: function () {}, - enable: function () { - var options = this.options; - var editable; - var currentElement = Aloha.jQuery(options.element.get(0)).aloha(); - _.each(Aloha.editables, function (aloha) { - // Find the actual editable instance so we can hook to the events - // correctly - if (aloha.obj.get(0) === currentElement.get(0)) { - editable = aloha; - } - }); - if (!editable) { - return; - } - editable.vieEntity = options.entity; - - // Subscribe to activation and deactivation events - Aloha.bind('aloha-editable-activated', function (event, data) { - if (data.editable !== editable) { - return; - } - options.activated(); - }); - Aloha.bind('aloha-editable-deactivated', function (event, data) { - if (data.editable !== editable) { - return; - } - options.deactivated(); - }); - - Aloha.bind('aloha-smart-content-changed', function (event, data) { - if (data.editable !== editable) { - return; - } - if (!data.editable.isModified()) { - return true; - } - options.changed(data.editable.getContents()); - data.editable.setUnmodified(); - }); - this.options.disabled = false; - }, - disable: function () { - Aloha.jQuery(this.options.element.get(0)).mahalo(); - this.options.disabled = true; - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Tobias Herrmann, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false CKEDITOR:false */ - 'use strict'; - - // # CKEditor editing widget - // - // This widget allows editing textual content areas with the - // [CKEditor](http://ckeditor.com/) rich text editor. - jQuery.widget('Midgard.ckeditorWidget', jQuery.Midgard.editWidget, { - enable: function () { - this.element.attr('contentEditable', 'true'); - this.editor = CKEDITOR.inline(this.element.get(0)); - this.options.disabled = false; - - var widget = this; - this.editor.on('focus', function () { - widget.options.activated(); - }); - this.editor.on('blur', function () { - widget.options.activated(); - widget.options.changed(widget.editor.getData()); - }); - this.editor.on('key', function () { - widget.options.changed(widget.editor.getData()); - }); - this.editor.on('paste', function () { - widget.options.changed(widget.editor.getData()); - }); - this.editor.on('afterCommandExec', function () { - widget.options.changed(widget.editor.getData()); - }); - this.editor.on('configLoaded', function() { - jQuery.each(widget.options.editorOptions, function(optionName, option) { - widget.editor.config[optionName] = option; - }); - }); - }, - - disable: function () { - if (!this.editor) { - return; - } - this.element.attr('contentEditable', 'false'); - this.editor.destroy(); - this.editor = null; - }, - - _initialize: function () { - CKEDITOR.disableAutoInline = true; - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Tobias Herrmann, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false */ - 'use strict'; - - // # Hallo editing widget - // - // This widget allows editing textual content areas with the - // [Hallo](http://hallojs.org) rich text editor. - jQuery.widget('Midgard.halloWidget', jQuery.Midgard.editWidget, { - options: { - editorOptions: {}, - disabled: true, - toolbarState: 'full', - vie: null, - entity: null - }, - enable: function () { - jQuery(this.element).hallo({ - editable: true - }); - this.options.disabled = false; - }, - - disable: function () { - jQuery(this.element).hallo({ - editable: false - }); - this.options.disabled = true; - }, - - _initialize: function () { - jQuery(this.element).hallo(this.getHalloOptions()); - var self = this; - jQuery(this.element).on('halloactivated', function (event, data) { - self.options.activated(); - }); - jQuery(this.element).on('hallodeactivated', function (event, data) { - self.options.deactivated(); - }); - jQuery(this.element).on('hallomodified', function (event, data) { - self.options.changed(data.content); - data.editable.setUnmodified(); - }); - - jQuery(document).on('midgardtoolbarstatechange', function(event, data) { - // Switch between Hallo configurations when toolbar state changes - if (data.display === self.options.toolbarState) { - return; - } - self.options.toolbarState = data.display; - if (!self.element.data('IKS-hallo')) { - // Hallo not yet instantiated - return; - } - var newOptions = self.getHalloOptions(); - self.element.hallo('changeToolbar', newOptions.parentElement, newOptions.toolbar, true); - }); - }, - - getHalloOptions: function() { - var defaults = { - plugins: { - halloformat: {}, - halloblock: {}, - hallolists: {}, - hallolink: {}, - halloimage: { - entity: this.options.entity - } - }, - buttonCssClass: 'create-ui-btn-small', - placeholder: '[' + this.options.property + ']' - }; - - if (typeof this.element.annotate === 'function' && this.options.vie.services.stanbol) { - // Enable Hallo Annotate plugin by default if user has annotate.js - // loaded and VIE has Stanbol enabled - defaults.plugins.halloannotate = { - vie: this.options.vie - }; - } - - if (this.options.toolbarState === 'full') { - // Use fixed toolbar in the Create tools area - defaults.parentElement = jQuery('.create-ui-toolbar-dynamictoolarea .create-ui-tool-freearea'); - defaults.toolbar = 'halloToolbarFixed'; - } else { - // Tools area minimized, use floating toolbar - defaults.parentElement = 'body'; - defaults.toolbar = 'halloToolbarContextual'; - } - return _.extend(defaults, this.options.editorOptions); - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false */ - 'use strict'; - - // # Redactor editing widget - // - // This widget allows editing textual content areas with the - // [Redactor](http://redactorjs.com/) rich text editor. - jQuery.widget('Midgard.redactorWidget', jQuery.Midgard.editWidget, { - editor: null, - - options: { - editorOptions: {}, - disabled: true - }, - - enable: function () { - jQuery(this.element).redactor(this.getRedactorOptions()); - this.options.disabled = false; - }, - - disable: function () { - jQuery(this.element).destroyEditor(); - this.options.disabled = true; - }, - - _initialize: function () { - var self = this; - jQuery(this.element).on('focus', function (event) { - self.options.activated(); - }); - /* - jQuery(this.element).on('blur', function (event) { - self.options.deactivated(); - }); - */ - }, - - getRedactorOptions: function () { - var self = this; - var overrides = { - keyupCallback: function (obj, event) { - self.options.changed(jQuery(self.element).getCode()); - }, - execCommandCallback: function (obj, command) { - self.options.changed(jQuery(self.element).getCode()); - } - }; - - return _.extend(self.options.editorOptions, overrides); - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2011-2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false window:false */ - 'use strict'; - - jQuery.widget('Midgard.midgardStorage', { - saveEnabled: true, - options: { - // Whether to use localstorage - localStorage: false, - removeLocalstorageOnIgnore: true, - // VIE instance to use for storage handling - vie: null, - // URL callback for Backbone.sync - url: '', - // Whether to enable automatic saving - autoSave: false, - // How often to autosave in milliseconds - autoSaveInterval: 5000, - // Whether to save entities that are referenced by entities - // we're saving to the server. - saveReferencedNew: false, - saveReferencedChanged: false, - // Namespace used for events from midgardEditable-derived widget - editableNs: 'midgardeditable', - // CSS selector for the Edit button, leave to null to not bind - // notifications to any element - editSelector: '#midgardcreate-edit a', - localize: function (id, language) { - return window.midgardCreate.localize(id, language); - }, - language: null - }, - - _create: function () { - var widget = this; - this.changedModels = []; - - if (window.localStorage) { - this.options.localStorage = true; - } - - this.vie = this.options.vie; - - this.vie.entities.on('add', function (model) { - // Add the back-end URL used by Backbone.sync - model.url = widget.options.url; - model.toJSON = model.toJSONLD; - }); - - widget._bindEditables(); - if (widget.options.autoSave) { - widget._autoSave(); - } - }, - - _autoSave: function () { - var widget = this; - widget.saveEnabled = true; - - var doAutoSave = function () { - if (!widget.saveEnabled) { - return; - } - - if (widget.changedModels.length === 0) { - return; - } - - widget.saveRemoteAll({ - // We make autosaves silent so that potential changes from server - // don't disrupt user while writing. - silent: true - }); - }; - - var timeout = window.setInterval(doAutoSave, widget.options.autoSaveInterval); - - this.element.on('startPreventSave', function () { - if (timeout) { - window.clearInterval(timeout); - timeout = null; - } - widget.disableAutoSave(); - }); - this.element.on('stopPreventSave', function () { - if (!timeout) { - timeout = window.setInterval(doAutoSave, widget.options.autoSaveInterval); - } - widget.enableAutoSave(); - }); - - }, - - enableAutoSave: function () { - this.saveEnabled = true; - }, - - disableAutoSave: function () { - this.saveEnabled = false; - }, - - _bindEditables: function () { - var widget = this; - this.restorables = []; - var restorer; - - widget.element.on(widget.options.editableNs + 'changed', function (event, options) { - if (_.indexOf(widget.changedModels, options.instance) === -1) { - widget.changedModels.push(options.instance); - } - widget._saveLocal(options.instance); - }); - - widget.element.on(widget.options.editableNs + 'disable', function (event, options) { - widget.revertChanges(options.instance); - }); - - widget.element.on(widget.options.editableNs + 'enable', function (event, options) { - if (!options.instance._originalAttributes) { - options.instance._originalAttributes = _.clone(options.instance.attributes); - } - - if (!options.instance.isNew() && widget._checkLocal(options.instance)) { - // We have locally-stored modifications, user needs to be asked - widget.restorables.push(options.instance); - } - - /*_.each(options.instance.attributes, function (attributeValue, property) { - if (attributeValue instanceof widget.vie.Collection) { - widget._readLocalReferences(options.instance, property, attributeValue); - } - });*/ - }); - - widget.element.on('midgardcreatestatechange', function (event, options) { - if (options.state === 'browse' || widget.restorables.length === 0) { - widget.restorables = []; - if (restorer) { - restorer.close(); - } - return; - } - restorer = widget.checkRestore(); - }); - - widget.element.on('midgardstorageloaded', function (event, options) { - if (_.indexOf(widget.changedModels, options.instance) === -1) { - widget.changedModels.push(options.instance); - } - }); - }, - - checkRestore: function () { - var widget = this; - if (widget.restorables.length === 0) { - return; - } - - var message; - var restorer; - if (widget.restorables.length === 1) { - message = _.template(widget.options.localize('localModification', widget.options.language), { - label: widget.restorables[0].getSubjectUri() - }); - } else { - message = _.template(widget.options.localize('localModifications', widget.options.language), { - number: widget.restorables.length - }); - } - - var doRestore = function (event, notification) { - widget.restoreLocalAll(); - restorer.close(); - }; - - var doIgnore = function (event, notification) { - widget.ignoreLocal(); - restorer.close(); - }; - - restorer = jQuery('body').midgardNotifications('create', { - bindTo: widget.options.editSelector, - gravity: 'TR', - body: message, - timeout: 0, - actions: [ - { - name: 'restore', - label: widget.options.localize('Restore', widget.options.language), - cb: doRestore, - className: 'create-ui-btn' - }, - { - name: 'ignore', - label: widget.options.localize('Ignore', widget.options.language), - cb: doIgnore, - className: 'create-ui-btn' - } - ], - callbacks: { - beforeShow: function () { - if (!window.Mousetrap) { - return; - } - window.Mousetrap.bind(['command+shift+r', 'ctrl+shift+r'], function (event) { - event.preventDefault(); - doRestore(); - }); - window.Mousetrap.bind(['command+shift+i', 'ctrl+shift+i'], function (event) { - event.preventDefault(); - doIgnore(); - }); - }, - afterClose: function () { - if (!window.Mousetrap) { - return; - } - window.Mousetrap.unbind(['command+shift+r', 'ctrl+shift+r']); - window.Mousetrap.unbind(['command+shift+i', 'ctrl+shift+i']); - } - } - }); - return restorer; - }, - - restoreLocalAll: function () { - _.each(this.restorables, function (instance) { - this.readLocal(instance); - }, this); - this.restorables = []; - }, - - ignoreLocal: function () { - if (this.options.removeLocalstorageOnIgnore) { - _.each(this.restorables, function (instance) { - this._removeLocal(instance); - }, this); - } - this.restorables = []; - }, - - saveReferences: function (model) { - _.each(model.attributes, function (value, property) { - if (!value || !value.isCollection) { - return; - } - - value.each(function (referencedModel) { - if (this.changedModels.indexOf(referencedModel) !== -1) { - // The referenced model is already in the save queue - return; - } - - if (referencedModel.isNew() && this.options.saveReferencedNew) { - return referencedModel.save(); - } - - if (referencedModel.hasChanged() && this.options.saveReferencedChanged) { - return referencedModel.save(); - } - }, this); - }, this); - }, - - saveRemote: function (model, options) { - // Optionally handle entities referenced in this model first - this.saveReferences(model); - - this._trigger('saveentity', null, { - entity: model, - options: options - }); - - var widget = this; - model.save(null, _.extend({}, options, { - success: function (m, response) { - // From now on we're going with the values we have on server - model._originalAttributes = _.clone(model.attributes); - widget._removeLocal(model); - window.setTimeout(function () { - // Remove the model from the list of changed models after saving - widget.changedModels.splice(widget.changedModels.indexOf(model), 1); - }, 0); - if (_.isFunction(options.success)) { - options.success(m, response); - } - widget._trigger('savedentity', null, { - entity: model, - options: options - }); - }, - error: function (m, response) { - if (_.isFunction(options.error)) { - options.error(m, response); - } - } - })); - }, - - saveRemoteAll: function (options) { - var widget = this; - if (widget.changedModels.length === 0) { - return; - } - - widget._trigger('save', null, { - entities: widget.changedModels, - options: options, - // Deprecated - models: widget.changedModels - }); - - var notification_msg; - var needed = widget.changedModels.length; - if (needed > 1) { - notification_msg = _.template(widget.options.localize('saveSuccessMultiple', widget.options.language), { - number: needed - }); - } else { - notification_msg = _.template(widget.options.localize('saveSuccess', widget.options.language), { - label: widget.changedModels[0].getSubjectUri() - }); - } - - widget.disableAutoSave(); - _.each(widget.changedModels, function (model) { - this.saveRemote(model, { - success: function (m, response) { - needed--; - if (needed <= 0) { - // All models were happily saved - widget._trigger('saved', null, { - options: options - }); - if (options && _.isFunction(options.success)) { - options.success(m, response); - } - jQuery('body').midgardNotifications('create', { - body: notification_msg - }); - widget.enableAutoSave(); - } - }, - error: function (m, err) { - if (options && _.isFunction(options.error)) { - options.error(m, err); - } - jQuery('body').midgardNotifications('create', { - body: _.template(widget.options.localize('saveError', widget.options.language), { - error: err.responseText || '' - }), - timeout: 0 - }); - - widget._trigger('error', null, { - instance: model - }); - } - }); - }, this); - }, - - _saveLocal: function (model) { - if (!this.options.localStorage) { - return; - } - - if (model.isNew()) { - // Anonymous object, save as refs instead - if (!model.primaryCollection) { - return; - } - return this._saveLocalReferences(model.primaryCollection.subject, model.primaryCollection.predicate, model); - } - window.localStorage.setItem(model.getSubjectUri(), JSON.stringify(model.toJSONLD())); - }, - - _getReferenceId: function (model, property) { - return model.id + ':' + property; - }, - - _saveLocalReferences: function (subject, predicate, model) { - if (!this.options.localStorage) { - return; - } - - if (!subject || !predicate) { - return; - } - - var widget = this; - var identifier = subject + ':' + predicate; - var json = model.toJSONLD(); - if (window.localStorage.getItem(identifier)) { - var referenceList = JSON.parse(window.localStorage.getItem(identifier)); - var index = _.pluck(referenceList, '@').indexOf(json['@']); - if (index !== -1) { - referenceList[index] = json; - } else { - referenceList.push(json); - } - window.localStorage.setItem(identifier, JSON.stringify(referenceList)); - return; - } - window.localStorage.setItem(identifier, JSON.stringify([json])); - }, - - _checkLocal: function (model) { - if (!this.options.localStorage) { - return false; - } - - var local = window.localStorage.getItem(model.getSubjectUri()); - if (!local) { - return false; - } - - return true; - }, - - hasLocal: function (model) { - if (!this.options.localStorage) { - return false; - } - - if (!window.localStorage.getItem(model.getSubjectUri())) { - return false; - } - return true; - }, - - readLocal: function (model) { - if (!this.options.localStorage) { - return; - } - - var local = window.localStorage.getItem(model.getSubjectUri()); - if (!local) { - return; - } - if (!model._originalAttributes) { - model._originalAttributes = _.clone(model.attributes); - } - var parsed = JSON.parse(local); - var entity = this.vie.entities.addOrUpdate(parsed, { - overrideAttributes: true - }); - - this._trigger('loaded', null, { - instance: entity - }); - }, - - _readLocalReferences: function (model, property, collection) { - if (!this.options.localStorage) { - return; - } - - var identifier = this._getReferenceId(model, property); - var local = window.localStorage.getItem(identifier); - if (!local) { - return; - } - collection.add(JSON.parse(local)); - }, - - revertChanges: function (model) { - var widget = this; - - // Remove unsaved collection members - if (!model) { return; } - _.each(model.attributes, function (attributeValue, property) { - if (attributeValue instanceof widget.vie.Collection) { - var removables = []; - attributeValue.forEach(function (model) { - if (model.isNew()) { - removables.push(model); - } - }); - attributeValue.remove(removables); - } - }); - - // Restore original object properties - if (!model.changedAttributes()) { - if (model._originalAttributes) { - model.set(model._originalAttributes); - } - return; - } - - model.set(model.previousAttributes()); - }, - - _removeLocal: function (model) { - if (!this.options.localStorage) { - return; - } - - window.localStorage.removeItem(model.getSubjectUri()); - } - }); -})(jQuery); -if (window.midgardCreate === undefined) { - window.midgardCreate = {}; -} -if (window.midgardCreate.locale === undefined) { - window.midgardCreate.locale = {}; -} - -window.midgardCreate.locale.en = { - // Session-state buttons for the main toolbar - 'Save': 'Save', - 'Saving': 'Saving', - 'Cancel': 'Cancel', - 'Edit': 'Edit', - // Storage status messages - 'localModification': 'Item "<%= label %>" has local modifications', - 'localModifications': '<%= number %> items on this page have local modifications', - 'Restore': 'Restore', - 'Ignore': 'Ignore', - 'saveSuccess': 'Item "<%= label %>" saved successfully', - 'saveSuccessMultiple': '<%= number %> items saved successfully', - 'saveError': 'Error occurred while saving
<%= error %>', - // Tagging - 'Item tags': 'Item tags', - 'Suggested tags': 'Suggested tags', - 'Tags': 'Tags', - 'add a tag': 'add a tag', - // Collection widgets - 'Add': 'Add', - 'Choose type to add': 'Choose type to add' -}; diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css index 6a5ac83..af5cb0e 100644 --- a/core/modules/edit/css/edit.css +++ b/core/modules/edit/css/edit.css @@ -69,8 +69,17 @@ transition: background, padding .2s ease; } - - +/** + * Entity toolbar. + */ +.edit-entity-toolbar-container { + background-color: red; + position:absolute; + -webkit-transition: all 0.2s; + transition: all 0.2s; + width: 20em; + z-index: 350; +} /** * Candidate editables + editables being edited. diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 0125d45..c16990f 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -65,7 +65,6 @@ function edit_library_info() { $path = drupal_get_path('module', 'edit'); $options = array( 'scope' => 'footer', - 'attributes' => array('defer' => TRUE), ); $libraries['edit'] = array( 'title' => 'Edit: in-place editing', @@ -74,21 +73,21 @@ function edit_library_info() { 'js' => array( // Core. $path . '/js/edit.js' => $options, - $path . '/js/app.js' => $options, // Models. - $path . '/js/models/edit-app-model.js' => $options, + $path . '/js/models/AppModel.js' => $options, + $path . '/js/models/EntityModel.js' => $options, + $path . '/js/models/FieldModel.js' => $options, // Views. - $path . '/js/views/propertyeditordecoration-view.js' => $options, - $path . '/js/views/contextuallink-view.js' => $options, - $path . '/js/views/modal-view.js' => $options, - $path . '/js/views/toolbar-view.js' => $options, + $path . '/js/views/AppView.js' => $options, + $path . '/js/views/PropertyEditorDecorationView.js' => $options, + $path . '/js/views/ContextualLinkView.js' => $options, + $path . '/js/views/ModalView.js' => $options, + $path . '/js/views/ToolbarView.js' => $options, + $path . '/js/views/EntityToolbarView.js' => $options, // Backbone.sync implementation on top of Drupal forms. $path . '/js/backbone.drupalform.js' => $options, - // VIE service. - $path . '/js/viejs/EditService.js' => $options, - // Create.js subclasses. - $path . '/js/createjs/editable.js' => $options, - $path . '/js/createjs/storage.js' => $options, + // Storage manager. + $path . '/js/storage.js' => $options, // Other. $path . '/js/util.js' => $options, $path . '/js/theme.js' => $options, @@ -109,11 +108,11 @@ function edit_library_info() { array('system', 'jquery'), array('system', 'underscore'), array('system', 'backbone'), - array('system', 'vie.core'), - array('system', 'create.editonly'), array('system', 'jquery.form'), + array('system', 'jquery.ui.position'), array('system', 'drupal.form'), array('system', 'drupal.ajax'), + array('system', 'drupal.debounce'), array('system', 'drupalSettings'), ), ); @@ -121,7 +120,7 @@ function edit_library_info() { 'title' => '"Form" Create.js PropertyEditor widget', 'version' => VERSION, 'js' => array( - $path . '/js/createjs/editingWidgets/formwidget.js' => $options, + $path . '/js/editors/formEditor.js' => $options, ), 'dependencies' => array( array('edit', 'edit'), @@ -131,7 +130,7 @@ function edit_library_info() { 'title' => '"Direct" Create.js PropertyEditor widget', 'version' => VERSION, 'js' => array( - $path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => $options, + $path . '/js/editors/directEditor.js' => $options, ), 'dependencies' => array( array('edit', 'edit'), diff --git a/core/modules/edit/js/app.js b/core/modules/edit/js/app.js deleted file mode 100644 index 14d76a0..0000000 --- a/core/modules/edit/js/app.js +++ /dev/null @@ -1,391 +0,0 @@ -/** - * @file - * A Backbone View that is the central app controller. - */ -(function ($, _, Backbone, Drupal, VIE) { - -"use strict"; - - Drupal.edit = Drupal.edit || {}; - Drupal.edit.EditAppView = Backbone.View.extend({ - vie: null, - domService: null, - - // Configuration for state handling. - states: [], - activeEditorStates: [], - singleEditorStates: [], - - // State. - $entityElements: null, - - /** - * Implements Backbone Views' initialize() function. - */ - initialize: function() { - _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange'); - - // VIE instance for Edit. - this.vie = new VIE(); - // Use our custom DOM parsing service until RDFa is available. - this.vie.use(new this.vie.EditService()); - this.domService = this.vie.service('edit'); - - // Instantiate configuration for state handling. - this.states = [ - null, 'inactive', 'candidate', 'highlighted', - 'activating', 'active', 'changed', 'saving', 'saved', 'invalid' - ]; - this.activeEditorStates = ['activating', 'active']; - this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates); - - this.$entityElements = $([]); - - // Use Create's Storage widget. - this.$el.createStorage({ - vie: this.vie, - editableNs: 'createeditable' - }); - - // When view/edit mode is toggled in the menu, update the editor widgets. - this.model.on('change:activeEntity', this.appStateChange); - }, - - /** - * Finds editable properties within a given context. - * - * Finds editable properties, registers them with the app, updates their - * state to match the current app state. - * - * @param $context - * A jQuery-wrapped context DOM element within which will be searched. - */ - findEditableProperties: function($context) { - var that = this; - var activeEntity = this.model.get('activeEntity'); - - this.domService.findSubjectElements($context).each(function() { - var $element = $(this); - - // Ignore editable properties for which we've already set up Create.js. - if (that.$entityElements.index($element) !== -1) { - return; - } - - $element - // Instantiate an EditableEntity widget. - .createEditable({ - vie: that.vie, - disabled: true, - state: 'inactive', - acceptStateChange: that.acceptEditorStateChange, - statechange: function(event, data) { - that.editorStateChange(data.previous, data.current, data.propertyEditor); - }, - decoratePropertyEditor: function(data) { - that.decorateEditor(data.propertyEditor); - } - }) - // This event is triggered just before Edit removes an EditableEntity - // widget, so that we can do proper clean-up. - .on('destroyedPropertyEditor.edit', function(event, editor) { - that.undecorateEditor(editor); - that.$entityElements = that.$entityElements.not($(this)); - }) - // Transition the new PropertyEditor into the default state. - .createEditable('setState', 'inactive'); - - // If the new PropertyEditor is for the entity that's currently being - // edited, then transition it to the 'candidate' state. - // (This happens when a field was modified and is re-rendered.) - var entityOfProperty = $element.createEditable('option', 'model'); - if (entityOfProperty.getSubjectUri() === activeEntity) { - $element.createEditable('setState', 'candidate'); - } - - // Add this new EditableEntity widget element to the list. - that.$entityElements = that.$entityElements.add($element); - }); - }, - - /** - * Sets the state of PropertyEditor widgets when edit mode begins or ends. - * - * Should be called whenever EditAppModel's "activeEntity" changes. - */ - appStateChange: function() { - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133, https://github.com/bergie/create/issues/140) - // We're currently setting the state on EditableEntity widgets instead of - // PropertyEditor widgets, because of - // https://github.com/bergie/create/issues/133. - - var activeEntity = this.model.get('activeEntity'); - var $editableFieldsForEntity = $('[data-edit-id^="' + activeEntity + '/"]'); - - // First, change the status of all PropertyEditor widgets to 'inactive'. - this.$entityElements.each(function() { - $(this).createEditable('setState', 'inactive', null, {reason: 'stop'}); - }); - - // Then, change the status of PropertyEditor widgets of the currently - // active entity to 'candidate'. - $editableFieldsForEntity.each(function() { - $(this).createEditable('setState', 'candidate'); - }); - - // Manage the page's tab indexes. - }, - - /** - * Accepts or reject editor (PropertyEditor) state changes. - * - * This is what ensures that the app is in control of what happens. - * - * @param from - * The previous state. - * @param to - * The new state. - * @param predicate - * The predicate of the property for which the state change is happening. - * @param context - * The context that is trying to trigger the state change. - * @param callback - * The callback function that should receive the state acceptance result. - */ - acceptEditorStateChange: function(from, to, predicate, context, callback) { - var accept = true; - - // If the app is in view mode, then reject all state changes except for - // those to 'inactive'. - if (context && context.reason === 'stop') { - if (from === 'candidate' && to === 'inactive') { - accept = true; - } - } - // Handling of edit mode state changes is more granular. - else { - // In general, enforce the states sequence. Disallow going back from a - // "later" state to an "earlier" state, except in explicitly allowed - // cases. - if (_.indexOf(this.states, from) > _.indexOf(this.states, to)) { - accept = false; - // Allow: activating/active -> candidate. - // Necessary to stop editing a property. - if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { - accept = true; - } - // Allow: changed/invalid -> candidate. - // Necessary to stop editing a property when it is changed or invalid. - else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { - accept = true; - } - // Allow: highlighted -> candidate. - // Necessary to stop highlighting a property. - else if (from === 'highlighted' && to === 'candidate') { - accept = true; - } - // Allow: saved -> candidate. - // Necessary when successfully saved a property. - else if (from === 'saved' && to === 'candidate') { - accept = true; - } - // Allow: invalid -> saving. - // Necessary to be able to save a corrected, invalid property. - else if (from === 'invalid' && to === 'saving') { - accept = true; - } - } - - // If it's not against the general principle, then here are more - // disallowed cases to check. - if (accept) { - // Ensure only one editor (field) at a time may be higlighted or active. - if (from === 'candidate' && _.indexOf(this.singleEditorStates, to) !== -1) { - if (this.model.get('highlightedEditor') || this.model.get('activeEditor')) { - accept = false; - } - } - // Reject going from activating/active to candidate because of a - // mouseleave. - else if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { - if (context && context.reason === 'mouseleave') { - accept = false; - } - } - // When attempting to stop editing a changed/invalid property, ask for - // confirmation. - else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { - if (context && context.reason === 'mouseleave') { - accept = false; - } - else { - // Check whether the transition has been confirmed? - if (context && context.confirmed) { - accept = true; - } - // Confirm this transition. - else { - // The callback will be called from the helper function. - this._confirmStopEditing(callback); - return; - } - } - } - } - } - - callback(accept); - }, - - /** - * Asks the user to confirm whether he wants to stop editing via a modal. - * - * @param acceptCallback - * The callback function as passed to acceptEditorStateChange(). This - * callback function will be called with the user's choice. - * - * @see acceptEditorStateChange() - */ - _confirmStopEditing: function(acceptCallback) { - // Only instantiate if there isn't a modal instance visible yet. - if (!this.model.get('activeModal')) { - var that = this; - var modal = new Drupal.edit.views.ModalView({ - model: this.model, - message: Drupal.t('You have unsaved changes'), - buttons: [ - { action: 'discard', classes: 'gray-button', label: Drupal.t('Discard changes') }, - { action: 'save', type: 'submit', classes: 'blue-button', label: Drupal.t('Save') } - ], - callback: function(action) { - // The active modal has been removed. - that.model.set('activeModal', null); - if (action === 'discard') { - acceptCallback(true); - } - else { - acceptCallback(false); - var editor = that.model.get('activeEditor'); - editor.options.widget.setState('saving', editor.options.property); - } - } - }); - this.model.set('activeModal', modal); - // The modal will set the activeModal property on the model when rendering - // to prevent multiple modals from being instantiated. - modal.render(); - } - else { - // Reject as there is still an open transition waiting for confirmation. - acceptCallback(false); - } - }, - - /** - * Reacts to editor (PropertyEditor) state changes; tracks global state. - * - * @param from - * The previous state. - * @param to - * The new state. - * @param editor - * The PropertyEditor widget object. - */ - editorStateChange: function(from, to, editor) { - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Get rid of this once that issue is solved. - if (!editor) { - return; - } - else { - editor.stateChange(from, to); - } - - // Keep track of the highlighted editor in the global state. - if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== editor) { - this.model.set('highlightedEditor', editor); - } - else if (this.model.get('highlightedEditor') === editor && to === 'candidate') { - this.model.set('highlightedEditor', null); - } - - // Keep track of the active editor in the global state. - if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== editor) { - this.model.set('activeEditor', editor); - } - else if (this.model.get('activeEditor') === editor && to === 'candidate') { - // Discarded if it transitions from a changed state to 'candidate'. - if (from === 'changed' || from === 'invalid') { - // Retrieve the storage widget from DOM. - var createStorageWidget = this.$el.data('DrupalCreateStorage'); - // Revert changes in the model, this will trigger the direct editable - // content to be reset and redrawn. - createStorageWidget.revertChanges(editor.options.entity); - } - this.model.set('activeEditor', null); - } - - // Propagate the state change to the decoration and toolbar views. - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Uncomment this once that issue is solved. - // editor.decorationView.stateChange(from, to); - // editor.toolbarView.stateChange(from, to); - }, - - /** - * Decorates an editor (PropertyEditor). - * - * Upon the page load, all appropriate editors are initialized and decorated - * (i.e. even before anything of the editing UI becomes visible; even before - * edit mode is enabled). - * - * @param editor - * The PropertyEditor widget object. - */ - decorateEditor: function(editor) { - // Toolbars are rendered "on-demand" (highlighting or activating). - // They are a sibling element before the editor's DOM element. - editor.toolbarView = new Drupal.edit.views.ToolbarView({ - editor: editor, - $storageWidgetEl: this.$el - }); - - // Decorate the editor's DOM element depending on its state. - editor.decorationView = new Drupal.edit.views.PropertyEditorDecorationView({ - el: editor.element, - editor: editor, - toolbarId: editor.toolbarView.getId() - }); - - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Get rid of this once that issue is solved. - editor.options.widget.element.on('createeditablestatechange', function(event, data) { - editor.decorationView.stateChange(data.previous, data.current); - editor.toolbarView.stateChange(data.previous, data.current); - }); - }, - - /** - * Undecorates an editor (PropertyEditor). - * - * Whenever a property has been updated, the old HTML will be replaced by - * the new (re-rendered) HTML. The EditableEntity widget will be destroyed, - * as will be the PropertyEditor widget. This method ensures Edit's editor - * views also are removed properly. - * - * @param editor - * The PropertyEditor widget object. - */ - undecorateEditor: function(editor) { - editor.toolbarView.undelegateEvents(); - editor.toolbarView.remove(); - delete editor.toolbarView; - editor.decorationView.undelegateEvents(); - // Don't call .remove() on the decoration view, because that would remove - // a potentially rerendered field. - delete editor.decorationView; - } - - }); - -})(jQuery, _, Backbone, Drupal, VIE); diff --git a/core/modules/edit/js/backbone.drupalform.js b/core/modules/edit/js/backbone.drupalform.js index 750d16f..170f4a9 100644 --- a/core/modules/edit/js/backbone.drupalform.js +++ b/core/modules/edit/js/backbone.drupalform.js @@ -105,7 +105,7 @@ Backbone.syncDirect = function(method, model, options) { if (jQuery('#edit_backstage form').length === 0) { var formOptions = { propertyID: Drupal.edit.util.calcPropertyID(entity, predicate), - $editorElement: options.editor.element, + $editorElement: options.editor.$el, nocssjs: true }; Drupal.edit.util.form.load(formOptions, function(form, ajax) { diff --git a/core/modules/edit/js/createjs/editable.js b/core/modules/edit/js/createjs/editable.js deleted file mode 100644 index 1316023..0000000 --- a/core/modules/edit/js/createjs/editable.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @file - * Determines which editor (Create.js PropertyEditor widget) to use. - */ -(function (jQuery, Drupal, drupalSettings) { - -"use strict"; - - jQuery.widget('Drupal.createEditable', jQuery.Midgard.midgardEditable, { - _create: function() { - this.vie = this.options.vie; - - this.options.domService = 'edit'; - this.options.predicateSelector = '*'; //'.edit-field.edit-allowed'; - - // The Create.js PropertyEditor widget configuration is not hardcoded; it - // is generated by the server. - this.options.propertyEditorWidgetsConfiguration = drupalSettings.edit.editors; - - jQuery.Midgard.midgardEditable.prototype._create.call(this); - }, - - _propertyEditorName: function(data) { - // Pick a PropertyEditor widget for a property depending on its metadata. - var propertyID = Drupal.edit.util.calcPropertyID(data.entity, data.property); - return Drupal.edit.metadataCache[propertyID].editor; - } - }); - -})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js deleted file mode 100644 index cde6163..0000000 --- a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @file - * Override of Create.js' default "base" (plain contentEditable) widget. - */ -(function (jQuery, Drupal) { - -"use strict"; - - // @todo D8: use jQuery UI Widget bridging. - // @see http://drupal.org/node/1874934#comment-7124904 - jQuery.widget('Midgard.direct', jQuery.Midgard.editWidget, { - - /** - * Implements getEditUISettings() method. - */ - getEditUISettings: function() { - return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; - }, - - /** - * Implements jQuery UI widget factory's _init() method. - * - * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142) - * Get rid of this once that issue is solved. - */ - _init: function() {}, - - /** - * Implements Create's _initialize() method. - */ - _initialize: function() { - var that = this; - - // Sets the state to 'changed' whenever the content has changed. - var before = jQuery.trim(this.element.text()); - this.element.on('keyup paste', function (event) { - if (that.options.disabled) { - return; - } - var current = jQuery.trim(that.element.text()); - if (before !== current) { - before = current; - that.options.changed(current); - } - }); - }, - - /** - * Makes this PropertyEditor widget react to state changes. - */ - stateChange: function(from, to) { - switch (to) { - case 'inactive': - break; - case 'candidate': - if (from !== 'inactive') { - // Removes the "contenteditable" attribute. - this.disable(); - } - break; - case 'highlighted': - break; - case 'activating': - this.options.activated(); - break; - case 'active': - // Sets the "contenteditable" attribute to "true". - this.enable(); - break; - case 'changed': - break; - case 'saving': - break; - case 'saved': - break; - case 'invalid': - break; - } - } - - }); - -})(jQuery, Drupal); diff --git a/core/modules/edit/js/createjs/editingWidgets/formwidget.js b/core/modules/edit/js/createjs/editingWidgets/formwidget.js deleted file mode 100644 index aa2dd0a..0000000 --- a/core/modules/edit/js/createjs/editingWidgets/formwidget.js +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @file - * Form-based Create.js widget for structured content in Drupal. - */ -(function ($, Drupal) { - -"use strict"; - - // @todo D8: change the name to "form" + use jQuery UI Widget bridging. - // @see http://drupal.org/node/1874934#comment-7124904 - $.widget('Midgard.formEditEditor', $.Midgard.editWidget, { - - id: null, - $formContainer: null, - - /** - * Implements getEditUISettings() method. - */ - getEditUISettings: function() { - return { padding: false, unifiedToolbar: false, fullWidthToolbar: false }; - }, - - /** - * Implements jQuery UI widget factory's _init() method. - * - * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142) - * Get rid of this once that issue is solved. - */ - _init: function() {}, - - /** - * Implements Create's _initialize() method. - */ - _initialize: function() {}, - - /** - * Makes this PropertyEditor widget react to state changes. - */ - stateChange: function(from, to) { - switch (to) { - case 'inactive': - break; - case 'candidate': - if (from !== 'inactive') { - this.disable(); - } - break; - case 'highlighted': - break; - case 'activating': - this.enable(); - break; - case 'active': - break; - case 'changed': - break; - case 'saving': - break; - case 'saved': - break; - case 'invalid': - break; - } - }, - - /** - * Enables the widget. - */ - enable: function () { - var $editorElement = $(this.options.widget.element); - var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); - - // Generate a DOM-compatible ID for the form container DOM element. - this.id = 'edit-form-for-' + propertyID.replace(/\//g, '_'); - - // Render form container. - this.$formContainer = $(Drupal.theme('editFormContainer', { - id: this.id, - loadingMsg: Drupal.t('Loading…')} - )); - this.$formContainer - .find('.edit-form') - .addClass('edit-editable edit-highlighted edit-editing') - .attr('role', 'dialog'); - - // Insert form container in DOM. - if ($editorElement.css('display') === 'inline') { - // @todo: POSTPONED_ON(Drupal core, title/author/date as Entity Properties) - // This is untested in Drupal 8, because in Drupal 8 we don't yet - // have the ability to edit the node title/author/date, because they - // haven't been converted into Entity Properties yet, and they're the - // only examples in core of "display: inline" properties. - this.$formContainer.prependTo($editorElement.offsetParent()); - - var pos = $editorElement.position(); - this.$formContainer.css('left', pos.left).css('top', pos.top); - } - else { - this.$formContainer.insertBefore($editorElement); - } - - // Load form, insert it into the form container and attach event handlers. - var widget = this; - var formOptions = { - propertyID: propertyID, - $editorElement: $editorElement, - nocssjs: false - }; - Drupal.edit.util.form.load(formOptions, function(form, ajax) { - Drupal.ajax.prototype.commands.insert(ajax, { - data: form, - selector: '#' + widget.id + ' .placeholder' - }); - - var $submit = widget.$formContainer.find('.edit-form-submit'); - Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); - widget.$formContainer - .on('formUpdated.edit', ':input', function () { - // Sets the state to 'changed'. - widget.options.changed(); - }) - .on('keypress.edit', 'input', function (event) { - if (event.keyCode === 13) { - return false; - } - }); - - // Sets the state to 'activated'. - widget.options.activated(); - }); - }, - - /** - * Disables the widget. - */ - disable: function () { - if (this.$formContainer === null) { - return; - } - - Drupal.edit.util.form.unajaxifySaving(this.$formContainer.find('.edit-form-submit')); - // Allow form widgets to detach properly. - Drupal.detachBehaviors(this.$formContainer, null, 'unload'); - this.$formContainer - .off('change.edit', ':input') - .off('keypress.edit', 'input') - .remove(); - this.$formContainer = null; - } - }); - -})(jQuery, Drupal); diff --git a/core/modules/edit/js/createjs/storage.js b/core/modules/edit/js/createjs/storage.js deleted file mode 100644 index 580ff82..0000000 --- a/core/modules/edit/js/createjs/storage.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @file - * Subclasses jQuery.Midgard.midgardStorage to have consistent namespaces. - */ -(function(jQuery) { - -"use strict"; - - jQuery.widget('Drupal.createStorage', jQuery.Midgard.midgardStorage, {}); - -})(jQuery); diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index f924e7b..52088ec 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -13,29 +13,58 @@ Drupal.edit.metadataCache = Drupal.edit.metadataCache || {}; * Attach toggling behavior and in-place editing. */ Drupal.behaviors.edit = { + + views: { + contextualLinks: {}, + decorators: {}, + editables: {}, + entities: {}, + propertyEditors: {}, + toolbars: { + entities: {}, + fields: {} + } + }, + + collections: { + entities: null, + fields: null + }, + attach: function(context) { + var that = this; var $context = $(context); var $fields = $context.find('[data-edit-id]'); + var options = $.extend({}, this.defaults, (drupalSettings.edit || {})); + var Collection; - // Initialize the Edit app. - $('body').once('edit-init', Drupal.edit.init); + // A collection of all in-place editable entities on the page. + if (!this.collections.entities) { + Collection = Backbone.Collection.extend({ + model: Drupal.edit.EntityModel + }); + this.collections.entities = new Collection(); + } - var annotateField = function(field) { - if (_.has(Drupal.edit.metadataCache, field.editID)) { - var meta = Drupal.edit.metadataCache[field.editID]; + // A collection of all in-place editable fields on the page. + if (!this.collections.fields) { + this.collections.fields = new Drupal.edit.FieldCollection(); + } - field.$el.addClass((meta.access) ? 'edit-allowed' : 'edit-disallowed'); - if (meta.access) { - field.$el - .attr('data-edit-field-label', meta.label) - .attr('aria-label', meta.aria) - .addClass('edit-field edit-type-' + ((meta.editor === 'form') ? 'form' : 'direct')); - } + // Respond to entity model change events. + this.collections.entities + .on('change:isActive', this.enforceSingleActiveEntity, this); - return true; - } - return false; - }; + this.collections.fields + // Respond to field model HTML representation change events. + .on('change:html', this.updateAllKnownInstances, this); + + // Initialize the Edit app. + $('body').once('edit-init', $.proxy(this.init, this, options)); + + // @todo currently must be after the call to init, because the app needs to be initialized + this.collections.fields + .on('change:state', Drupal.edit.app.editorStateChange, Drupal.edit.app); // Find all fields in the context without metadata. var fieldsToAnnotate = _.map($fields.not('.edit-allowed, .edit-disallowed'), function(el) { @@ -46,14 +75,15 @@ Drupal.behaviors.edit = { // Fields whose metadata is known (typically when they were just modified) // can be annotated immediately, those remaining must be requested. var remainingFieldsToAnnotate = _.reduce(fieldsToAnnotate, function(result, field) { - if (!annotateField(field)) { + var isAnnotated = $.proxy(that.annotateField, that, field)(); + if (!isAnnotated) { result.push(field); } return result; }, []); // Make fields that could be annotated immediately available for editing. - Drupal.edit.app.findEditableProperties($context); + this.findEditableProperties($context, options); if (remainingFieldsToAnnotate.length) { $(window).ready(function() { @@ -69,45 +99,189 @@ Drupal.behaviors.edit = { }); // Annotate the remaining fields based on the updated access cache. - _.each(remainingFieldsToAnnotate, annotateField); + _.each(remainingFieldsToAnnotate, $.proxy(that.annotateField, that)); - // Find editable fields, make them editable. - Drupal.edit.app.findEditableProperties($context); + // Make fields that could be annotated immediately available for editing. + that.findEditableProperties($context, options); } }); }); } - } -}; + }, -Drupal.edit.init = function() { - // Instantiate EditAppView, which is the controller of it all. EditAppModel - // instance tracks global state (viewing/editing in-place). - var appModel = new Drupal.edit.models.EditAppModel(); - var app = new Drupal.edit.EditAppView({ - el: $('body'), - model: appModel - }); - - // Add "Quick edit" links to all contextual menus where editing the full - // node is possible. - // @todo Generalize this to work for all entities. - $('ul.contextual-links li.node-edit') - .before('
  • ') - .each(function() { - // Instantiate ContextualLinkView. - var $editContextualLink = $(this).prev(); - var editContextualLinkView = new Drupal.edit.views.ContextualLinkView({ - el: $editContextualLink.get(0), + init: function (options) { + var that = this; + // Instantiate EditAppView, which is the controller of it all. EditAppModel + // instance tracks global state (viewing/editing in-place). + var appModel = new Drupal.edit.AppModel(); + var app = new Drupal.edit.AppView({ + el: $('body').get(0), model: appModel, - entity: $editContextualLink.parents('[data-edit-entity]').attr('data-edit-entity') + entitiesCollection: this.collections.entities }); - }); + var entityModel; + + // Create a view for the Entity just once. + $('[data-edit-entity]').once('editEntity', function (index) { + var $this = $(this); + var id = $this.data('edit-entity'); + + entityModel = new Drupal.edit.EntityModel({ + id: id + }); + that.collections.entities.add(entityModel); + + // Create a Toolbar for the entity. + // @todo, the toolbar should really be create when quick edit is launched + // for an entity, not up front for all of them. + that.views.toolbars.entities[id] = new Drupal.edit.EntityToolbarView({ + el: this, + model: entityModel + }); + + // Create a view for the contextual links. + $this.find('.contextual-links') + .each(function () { + // Instantiate ContextualLinkView. + that.views.contextualLinks[id] = new Drupal.edit.ContextualLinkView($.extend({ + el: this, + model: entityModel, + appModel: appModel + }, options)); + }); + }); + + // Add "Quick edit" links to all contextual menus where editing the full + // node is possible. + // @todo Generalize this to work for all entities. + + + // For now, we work with a singleton app, because for Drupal.behaviors to be + // able to discover new editable properties that get AJAXed in, it must know + // with which app instance they should be associated. + Drupal.edit.app = app; + }, + + /** + * Finds editable properties within a given context. + * + * Finds editable properties, registers them with the app, updates their + * state to match the current app state. + * + * @param $context + * A jQuery-wrapped context DOM element within which will be searched. + */ + findEditableProperties: function($context) { + var that = this; + // Retrieve the active entity, of which there can only ever be one. + var activeEntity = this.collections.entities.where({ isActive: true })[0]; - // For now, we work with a singleton app, because for Drupal.behaviors to be - // able to discover new editable properties that get AJAXed in, it must know - // with which app instance they should be associated. - Drupal.edit.app = app; + $context.find('[data-edit-id]').each(function() { + var $element = $(this); + var editID = $element.attr('data-edit-id'); + + if (!_.has(Drupal.edit.metadataCache, editID)) { + return; + } + + var entityId = $element.closest('[data-edit-entity]').data('edit-entity'); + + // Early-return when no surrounding [data-edit-entity] is found. + // @todo This breaks down in weird cases like full node views, where the + // node title is not rendered within the node DOM, but as the *page* title + if (entityId === null) { + return; + } + + // @todo whether the "Quick Edit" + // link should appear depends on whether the user has access to edit any + // of the entity's fields. So, it is blocked on the metadata callback to + // the server. This is not yet implemented in the D8 HEAD Edit, but it is + // in http://drupal.org/node/1971108. We should take this into account if + // possible. If not, then we'll just have to refactor later. + // For now, this assumption is acceptable. + var entity = that.collections.entities.where({ id: entityId })[0]; + + // The FieldModel stores the state of an in-place editable entity field. + var field = new Drupal.edit.FieldModel({ + entity: entity, + $el: $element, + // Store the field in a collection in its entity's model. + collection: entity.get('fields'), + editID: editID, + label: Drupal.edit.metadataCache[editID].label, + editor: Drupal.edit.metadataCache[editID].editor, + html: $element[0].outerHTML, + acceptStateChange: _.bind(Drupal.edit.app.acceptEditorStateChange, Drupal.edit.app) + }); + + // Track all fields on the page. + that.collections.fields.add(field); + + // If the new in-place editable field is for the entity that's currently + // being edited, then transition it to the 'candidate' state. + // (This happens when a field was modified and is re-rendered.) + if (entity === activeEntity) { + field.set('state', 'candidate'); + } + }); + }, + + annotateField: function (field) { + if (_.has(Drupal.edit.metadataCache, field.editID)) { + var meta = Drupal.edit.metadataCache[field.editID]; + + field.$el.addClass((meta.access) ? 'edit-allowed' : 'edit-disallowed'); + if (meta.access) { + field.$el + .attr('data-edit-field-label', meta.label) + .attr('aria-label', meta.aria) + .addClass('edit-field edit-type-' + ((meta.editor === 'form') ? 'form' : 'direct')); + } + + return true; + } + return false; + }, + + /** + * EntityModel Collection change handler, called on change:isActive, enforces + * a single active entity. + */ + enforceSingleActiveEntity: function (changedEntityModel) { + // When an entity is deactivated, we don't need to enforce anything. + if (changedEntityModel.get('isActive') === false) { + return; + } + + // This entity was activated; deactivate all other entities. + changedEntityModel.collection.chain() + .filter(function (entityModel) { + return entityModel.get('isActive') === true && entityModel !== changedEntityModel; + }) + .each(function (entityModel) { + entityModel.set('isActive', false); + }); + }, + + // Updates all known instances of a specific entity field whenever the HTML + // representation of one of them has changed. + // @todo: this is currently prototype-level code, test this. The principle is + // sound, but the tricky thing is that an editID includes the view mode, but + // we actually want to update the same field in other view modes too, which + // means this method will have to check if there are any such instances, and + // if so, go to the server and re-render those too. + updateAllKnownInstances: function (changedModel) { + changedModel.collection.where({ editID: changedModel.get('editID') }) + .set('html', changedModel.get('html')); + }, + + defaults: { + strings: { + quickEdit: Drupal.t('Quick edit'), + stopQuickEdit: Drupal.t('Stop quick edit') + } + } }; })(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/edit/js/editors/directEditor.js b/core/modules/edit/js/editors/directEditor.js new file mode 100644 index 0000000..4864a4b --- /dev/null +++ b/core/modules/edit/js/editors/directEditor.js @@ -0,0 +1,89 @@ +/** + * @file + * Override of Create.js' default "base" (plain contentEditable) widget. + */ +(function ($, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.editors = Drupal.edit.editors || {}; + +Drupal.edit.editors.direct = Backbone.View.extend({ + + /** + * Implements getEditUISettings() method. + */ + getEditUISettings: function () { + return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function () { + var that = this; + + // Sets the state to 'changed' whenever the content has changed. + var before = jQuery.trim(this.element.text()); + this.element.on('keyup paste', function (event) { + var current = jQuery.trim(that.element.text()); + if (before !== current) { + before = current; + that.model.set('state', 'changed'); + + // @todo we have yet to set this value originally (before the editing + // starts) AND we have to handle the reverting aspect when editing is + // canceled, see editorStateChange(). + that.model.set('value', current); + } + }); + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + case 'candidate': + if (from !== 'inactive') { + // Removes the "contenteditable" attribute. + this.disable(); + } + break; + case 'highlighted': + break; + case 'activating': + // As soon as the current state change has propagated, apply this one. + // @see http://jsfiddle.net/5MVzp/2/ vs. http://jsfiddle.net/5MVzp/3/ + var that = this; + _.defer(function() { + that.model.set('state', 'active'); + }); + break; + case 'active': + // Sets the "contenteditable" attribute to "true". + this.enable(); + break; + case 'changed': + break; + case 'saving': + this.save(); + break; + case 'saved': + break; + case 'invalid': + break; + } + } +}); + +})(jQuery, Drupal); diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js new file mode 100644 index 0000000..9625230 --- /dev/null +++ b/core/modules/edit/js/editors/formEditor.js @@ -0,0 +1,220 @@ +/** + * @file + * Form-based Create.js widget for structured content in Drupal. + */ +(function ($, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.editors = Drupal.edit.editors || {}; + +Drupal.edit.editors.form = Backbone.View.extend({ + id: null, + $formContainer: null, + + /** + * Implements getEditUISettings() method. + */ + getEditUISettings: function () { + return { padding: false, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function (options) { + this.model.on('change:state', this.stateChange, this); + + // Generate a DOM-compatible ID for the form container DOM element. + this.elementId = 'edit-form-for-' + this.model.get('editID').replace(/\//g, '_'); + }, + + /** + * Enables the widget. + */ + enable: function () { + // Render form container. + this.$formContainer = $(Drupal.theme('editFormContainer', { + id: this.elementId, + loadingMsg: Drupal.t('Loading…')} + )); + this.$formContainer + .find('.edit-form') + .addClass('edit-editable edit-highlighted edit-editing') + .attr('role', 'dialog'); + + // Insert form container in DOM. + if (this.$el.css('display') === 'inline') { + this.$formContainer.prependTo(this.$el.offsetParent()); + // Position the form container to render on top of the field's element. + var pos = this.$el.position(); + this.$formContainer.css('left', pos.left).css('top', pos.top); + } + else { + this.$formContainer.insertBefore(this.$el); + } + + // Load form, insert it into the form container and attach event handlers. + var that = this; + var formOptions = { + propertyID: this.model.get('editID'), + $editorElement: this.$el, + nocssjs: false + }; + Drupal.edit.util.form.load(formOptions, function(form, ajax) { + Drupal.ajax.prototype.commands.insert(ajax, { + data: form, + selector: '#' + that.elementId + ' .placeholder' + }); + + var $submit = that.$formContainer.find('.edit-form-submit'); + Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); + that.$formContainer + .on('formUpdated.edit', ':input', function () { + // Sets the state to 'changed'. + that.model.set('state', 'changed'); + }) + .on('keypress.edit', 'input', function (event) { + if (event.keyCode === 13) { + return false; + } + }); + + // The in-place editor has loaded; change state to 'active'. + that.model.set('state', 'active'); + }); + }, + + /** + * Disables the widget. + */ + disable: function () { + if (this.$formContainer === null) { + return; + } + + Drupal.edit.util.form.unajaxifySaving(this.$formContainer.find('.edit-form-submit')); + // Allow form widgets to detach properly. + Drupal.detachBehaviors(this.$formContainer, null, 'unload'); + this.$formContainer + .off('change.edit', ':input') + .off('keypress.edit', 'input') + .remove(); + this.$formContainer = null; + }, + + /** + * Saves a property. + * + * This method deals with the complexity of the editor-dependent ways of + * inserting updated content and showing validation error messages. + * + * One might argue that this does not belong in a view. However, there is no + * actual "save" logic here, that lives in Backbone.sync. This is just some + * glue code, along with the logic for inserting updated content as well as + * showing validation error messages, the latter of which is certainly okay. + */ + save: function () { + var that = this; + var editor = this; + var editableEntity = this.el; + var entity = this.entity; + var predicate = this.predicate; + + // Use Create.js' Storage widget to handle saving. (Uses Backbone.sync.) + var storage = new Drupal.edit.Storage({ + vie: that.vie, + editableNs: 'createeditable', + element: this.$el + }); + storage.invoke('saveRemote', entity, { + editor: that, + + // Successfully saved without validation errors. + success: function (model) { + //editableEntity.setState('saved', predicate); + + // Now that the changes to this property have been saved, the saved + // attributes are now the "original" attributes. + entity._originalAttributes = entity._previousAttributes = _.clone(entity.attributes); + + // Get data necessary to rerender property before it is unavailable. + var updatedProperty = entity.get(predicate + '/rendered'); + var $propertyWrapper = editor.$el.closest('.edit-field'); + var $context = $propertyWrapper.parent(); + + //editableEntity.setState('candidate', predicate); + // Unset the property, because it will be parsed again from the DOM, iff + // its new value causes it to still be rendered. + entity.unset(predicate, { silent: true }); + entity.unset(predicate + '/rendered', { silent: true }); + // Trigger event to allow for proper clean-up of editor-specific views. + editor.$el.trigger('destroyedPropertyEditor.edit', editor); + + // Replace the old content with the new content. + $propertyWrapper.replaceWith(updatedProperty); + Drupal.attachBehaviors($context); + }, + + // Save attempted but failed due to validation errors. + error: function (validationErrorMessages) { + editableEntity.setState('invalid', predicate); + + if (that.editorName === 'form') { + editor.$formContainer + .find('.edit-form') + .addClass('edit-validation-error') + .find('form') + .prepend(validationErrorMessages); + } + else { + var $errors = $('
    ') + .append(validationErrorMessages); + editor.element + .addClass('edit-validation-error') + .after($errors); + } + } + }); + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + case 'candidate': + if (from !== 'inactive') { + this.disable(); + } + break; + case 'highlighted': + break; + case 'activating': + this.enable(); + break; + case 'active': + break; + case 'changed': + break; + case 'saving': + this.save(); + break; + case 'saved': + break; + case 'invalid': + break; + } + } +}); + +})(jQuery, Drupal); diff --git a/core/modules/edit/js/models/AppModel.js b/core/modules/edit/js/models/AppModel.js new file mode 100644 index 0000000..402d0f7 --- /dev/null +++ b/core/modules/edit/js/models/AppModel.js @@ -0,0 +1,23 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + AppModel: Backbone.Model.extend({ + defaults: { + activeEntity: null, + highlightedEditor: null, + activeEditor: null, + // Reference to a ModalView-instance if a state change requires confirmation. + activeModal: null + } + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js new file mode 100644 index 0000000..a527a0c --- /dev/null +++ b/core/modules/edit/js/models/EntityModel.js @@ -0,0 +1,74 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + EntityModel: Backbone.Model.extend({ + defaults: { + // An entity ID, of the form "/", e.g. "node/1". + id: null, + // Indicates whether this instance of this entity is currently being + // edited. + isActive: false, + // A Drupal.edit.FieldCollection for all fields of this entity. + fields: null, + // The model for the currently active field. The default is a stub object + // with an 'on' method so that it can be result to the default value + // when no fields are active on this entity without breaking listener + // attachment calls to this property's object. + fieldModel: (function () { + return { + on: function () {}, + off: function () {} + }; + }()) + }, + + /** + * + */ + initialize: function () { + this.set('fields', new Drupal.edit.FieldCollection()); + + this.get('fields').on('change:state', this.stateChange, this); + }, + + /** + * Listens to FieldModel editor state changes. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of an editable element. Used to determine display and behavior. + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + this.set('fieldModel', this.defaults.fieldModel); + break; + case 'candidate': + case 'highlighted': + break; + case 'activating': + this.set('fieldModel', model); + break; + case 'active': + case 'changed': + case 'saving': + case 'saved': + case 'invalid': + default: + break; + } + } + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js new file mode 100644 index 0000000..7703d2f --- /dev/null +++ b/core/modules/edit/js/models/FieldModel.js @@ -0,0 +1,119 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + FieldModel: Backbone.Model.extend({ + defaults: { + // @todo Try to get rid of this, but it's hard; see appStateChange() + $el: null, + + // Possible states: + // - inactive + // - candidate + // - highlighted + // - activating + // - active + // - changed + // - saving + // - saved + // - saved + // - invalid + // @see http://createjs.org/guide/#states + state: 'inactive', + // A Drupal.edit.EntityModel. Its "fields" attribute, which is a + // FieldCollection, is automatically updated to include this FieldModel. + entity: null, + // A place to store any decoration views. + decorationViews: {}, + + + // + // Data set by the metadata callback. + // + // The ID of the in-place editor to use. + editor: null, + // The label to use. + label: null, + + // + // Data derived from the information in the DOM. + // + + // The edit ID, format: `::::`. + editID: null, + // The full HTML representation of this field (with the element that has + // the data-edit-id as the outer element). Used to propagate changes from + // this field instance to other instances of the same field. + html: null, + + // + // Callbacks. + // + + // Callback function for validating changes between states. Receives the + // previous state, new state, context, and a callback + acceptStateChange: null + }, + initialize: function () { + this.get('entity').get('fields').add(this); + this.on('change:state', function(model, state) { + console.log('%c [MOD] ' + this.previous('state') + ' → ' + state + ' ' + model.get('editID'), 'background-color: blue; color: white'); + }); + this.on('error', function(model, error) { + console.log('%c' + model.get("editID") + " " + error, 'color: orange'); + }); + }, + validate: function (attrs, options) { + // We only care about validating the 'state' attribute. + if (!_.has(attrs, 'state')) { + return; + } + + var current = this.get('state'); + var next = attrs.state; + if (current !== next) { + // Ensure it's a valid state. + if (_.indexOf(this.constructor.states, next) === -1) { + return '"' + next + '" is an invalid state'; + } + // Check if the acceptStateChange callback accepts it. + if (!this.get('acceptStateChange')(current, next, options)) { + return 'state change not accepted'; + } + } + }, + // @see VIE.prototype.EditService.prototype.getElementSubject() + // Parses the `/` part from the edit ID. + getEntityID: function() { + return this.get('editID').split('/').slice(0, 2).join('/'); + }, + // @see VIE.prototype.EditService.prototype.getElementPredicate() + // Parses the `//` part from the edit ID. + getFieldID: function() { + return this.get('editID').split('/').slice(2, 5).join('/'); + } + }, { + states: [ + 'inactive', 'candidate', 'highlighted', + 'activating', 'active', 'changed', 'saving', 'saved', 'invalid' + ], + followsStateSequence: function (from, to) { + return _.indexOf(this.states, from) < _.indexOf(this.states, to); + } + }), + + // A collection of FieldModels. + // @todo link back to the entity at the collection level (not the collection element level)? + FieldCollection: Backbone.Collection.extend({ + model: Drupal.edit.FieldModel + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/models/edit-app-model.js b/core/modules/edit/js/models/edit-app-model.js deleted file mode 100644 index 0c90fd0..0000000 --- a/core/modules/edit/js/models/edit-app-model.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @file - * A Backbone Model that models the current Edit application state. - */ -(function(Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.models = Drupal.edit.models || {}; -Drupal.edit.models.EditAppModel = Backbone.Model.extend({ - defaults: { - activeEntity: null, - highlightedEditor: null, - activeEditor: null, - // Reference to a ModalView-instance if a transition requires confirmation. - activeModal: null - } -}); - -})(Backbone, Drupal); diff --git a/core/modules/edit/js/storage.js b/core/modules/edit/js/storage.js new file mode 100644 index 0000000..196337e --- /dev/null +++ b/core/modules/edit/js/storage.js @@ -0,0 +1,518 @@ +/** + * @file + * Subclasses jQuery.Midgard.midgardStorage to have consistent namespaces. + */ +(function($, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +Drupal.edit.Storage = function (options) { + $.extend(this, { + saveEnabled: true, + // VIE instance to use for storage handling + vie: null, + // Whether to use localstorage + localStorage: false, + removeLocalstorageOnIgnore: true, + // URL callback for Backbone.sync + url: '', + // Whether to enable automatic saving + autoSave: false, + // How often to autosave in milliseconds + autoSaveInterval: 5000, + // Whether to save entities that are referenced by entities + // we're saving to the server. + saveReferencedNew: false, + saveReferencedChanged: false, + // Namespace used for events from midgardEditable-derived widget + editableNs: 'midgardeditable', + // CSS selector for the Edit button, leave to null to not bind + // notifications to any element + editSelector: '#midgardcreate-edit a', + localize: function (id, language) { + return window.midgardCreate.localize(id, language); + }, + language: null + }, options); + this._create(); +}; + +$.extend(Drupal.edit.Storage.prototype, { + + invoke: function(method) { + var args = Array.prototype.slice.call(arguments, 1); + if (method in this && typeof this[method] === 'function') { + return this[method].apply(this, args); + } + }, + _create: function () { + var widget = this; + this.changedModels = []; + + if (window.localStorage) { + this.localStorage = true; + } + + this.vie.entities.on('add', function (model) { + // Add the back-end URL used by Backbone.sync + model.url = widget.url; + model.toJSON = model.toJSONLD; + }); + + widget._bindEditables(); + if (widget.autoSave) { + widget._autoSave(); + } + }, + + _autoSave: function () { + var widget = this; + widget.saveEnabled = true; + + var doAutoSave = function () { + if (!widget.saveEnabled) { + return; + } + + if (widget.changedModels.length === 0) { + return; + } + + widget.saveRemoteAll({ + // We make autosaves silent so that potential changes from server + // don't disrupt user while writing. + silent: true + }); + }; + + var timeout = window.setInterval(doAutoSave, widget.options.autoSaveInterval); + + this.element.on('startPreventSave', function () { + if (timeout) { + window.clearInterval(timeout); + timeout = null; + } + widget.disableAutoSave(); + }); + this.element.on('stopPreventSave', function () { + if (!timeout) { + timeout = window.setInterval(doAutoSave, widget.options.autoSaveInterval); + } + widget.enableAutoSave(); + }); + + }, + + enableAutoSave: function () { + this.saveEnabled = true; + }, + + disableAutoSave: function () { + this.saveEnabled = false; + }, + + _bindEditables: function () { + var widget = this; + this.restorables = []; + var restorer; + + widget.element.on(widget.editableNs + 'changed', function (event, options) { + if (_.indexOf(widget.changedModels, options.instance) === -1) { + widget.changedModels.push(options.instance); + } + widget._saveLocal(options.instance); + }); + + widget.element.on(widget.editableNs + 'disable', function (event, options) { + widget.revertChanges(options.instance); + }); + + widget.element.on(widget.editableNs + 'enable', function (event, options) { + if (!options.instance._originalAttributes) { + options.instance._originalAttributes = _.clone(options.instance.attributes); + } + + if (!options.instance.isNew() && widget._checkLocal(options.instance)) { + // We have locally-stored modifications, user needs to be asked + widget.restorables.push(options.instance); + } + + /*_.each(options.instance.attributes, function (attributeValue, property) { + if (attributeValue instanceof widget.vie.Collection) { + widget._readLocalReferences(options.instance, property, attributeValue); + } + });*/ + }); + + widget.element.on('midgardcreatestatechange', function (event, options) { + if (options.state === 'browse' || widget.restorables.length === 0) { + widget.restorables = []; + if (restorer) { + restorer.close(); + } + return; + } + restorer = widget.checkRestore(); + }); + + widget.element.on('midgardstorageloaded', function (event, options) { + if (_.indexOf(widget.changedModels, options.instance) === -1) { + widget.changedModels.push(options.instance); + } + }); + }, + + checkRestore: function () { + var widget = this; + if (widget.restorables.length === 0) { + return; + } + + var message; + var restorer; + if (widget.restorables.length === 1) { + message = _.template(widget.options.localize('localModification', widget.options.language), { + label: widget.restorables[0].getSubjectUri() + }); + } else { + message = _.template(widget.options.localize('localModifications', widget.options.language), { + number: widget.restorables.length + }); + } + + var doRestore = function (event, notification) { + widget.restoreLocalAll(); + restorer.close(); + }; + + var doIgnore = function (event, notification) { + widget.ignoreLocal(); + restorer.close(); + }; + + restorer = jQuery('body').midgardNotifications('create', { + bindTo: widget.options.editSelector, + gravity: 'TR', + body: message, + timeout: 0, + actions: [ + { + name: 'restore', + label: widget.options.localize('Restore', widget.options.language), + cb: doRestore, + className: 'create-ui-btn' + }, + { + name: 'ignore', + label: widget.options.localize('Ignore', widget.options.language), + cb: doIgnore, + className: 'create-ui-btn' + } + ], + callbacks: { + beforeShow: function () { + if (!window.Mousetrap) { + return; + } + window.Mousetrap.bind(['command+shift+r', 'ctrl+shift+r'], function (event) { + event.preventDefault(); + doRestore(); + }); + window.Mousetrap.bind(['command+shift+i', 'ctrl+shift+i'], function (event) { + event.preventDefault(); + doIgnore(); + }); + }, + afterClose: function () { + if (!window.Mousetrap) { + return; + } + window.Mousetrap.unbind(['command+shift+r', 'ctrl+shift+r']); + window.Mousetrap.unbind(['command+shift+i', 'ctrl+shift+i']); + } + } + }); + return restorer; + }, + + restoreLocalAll: function () { + _.each(this.restorables, function (instance) { + this.readLocal(instance); + }, this); + this.restorables = []; + }, + + ignoreLocal: function () { + if (this.options.removeLocalstorageOnIgnore) { + _.each(this.restorables, function (instance) { + this._removeLocal(instance); + }, this); + } + this.restorables = []; + }, + + saveReferences: function (model) { + _.each(model.attributes, function (value, property) { + if (!value || !value.isCollection) { + return; + } + + value.each(function (referencedModel) { + if (this.changedModels.indexOf(referencedModel) !== -1) { + // The referenced model is already in the save queue + return; + } + + if (referencedModel.isNew() && this.options.saveReferencedNew) { + return referencedModel.save(); + } + + if (referencedModel.hasChanged() && this.options.saveReferencedChanged) { + return referencedModel.save(); + } + }, this); + }, this); + }, + + saveRemote: function (model, options) { + // Optionally handle entities referenced in this model first + this.saveReferences(model); + + // this._trigger('saveentity', null, { + // entity: model, + // options: options + // }); + + var widget = this; + model.save(null, _.extend({}, options, { + success: function (m, response) { + // From now on we're going with the values we have on server + model._originalAttributes = _.clone(model.attributes); + widget._removeLocal(model); + window.setTimeout(function () { + // Remove the model from the list of changed models after saving + widget.changedModels.splice(widget.changedModels.indexOf(model), 1); + }, 0); + if (_.isFunction(options.success)) { + options.success(m, response); + } + // widget._trigger('savedentity', null, { + // entity: model, + // options: options + // }); + }, + error: function (m, response) { + if (_.isFunction(options.error)) { + options.error(m, response); + } + } + })); + }, + + saveRemoteAll: function (options) { + var widget = this; + if (widget.changedModels.length === 0) { + return; + } + + widget._trigger('save', null, { + entities: widget.changedModels, + options: options, + // Deprecated + models: widget.changedModels + }); + + var notification_msg; + var needed = widget.changedModels.length; + if (needed > 1) { + notification_msg = _.template(widget.options.localize('saveSuccessMultiple', widget.options.language), { + number: needed + }); + } else { + notification_msg = _.template(widget.options.localize('saveSuccess', widget.options.language), { + label: widget.changedModels[0].getSubjectUri() + }); + } + + widget.disableAutoSave(); + _.each(widget.changedModels, function (model) { + this.saveRemote(model, { + success: function (m, response) { + needed--; + if (needed <= 0) { + // All models were happily saved + widget._trigger('saved', null, { + options: options + }); + if (options && _.isFunction(options.success)) { + options.success(m, response); + } + jQuery('body').midgardNotifications('create', { + body: notification_msg + }); + widget.enableAutoSave(); + } + }, + error: function (m, err) { + if (options && _.isFunction(options.error)) { + options.error(m, err); + } + jQuery('body').midgardNotifications('create', { + body: _.template(widget.options.localize('saveError', widget.options.language), { + error: err.responseText || '' + }), + timeout: 0 + }); + + widget._trigger('error', null, { + instance: model + }); + } + }); + }, this); + }, + + _saveLocal: function (model) { + if (!this.options.localStorage) { + return; + } + + if (model.isNew()) { + // Anonymous object, save as refs instead + if (!model.primaryCollection) { + return; + } + return this._saveLocalReferences(model.primaryCollection.subject, model.primaryCollection.predicate, model); + } + window.localStorage.setItem(model.getSubjectUri(), JSON.stringify(model.toJSONLD())); + }, + + _getReferenceId: function (model, property) { + return model.id + ':' + property; + }, + + _saveLocalReferences: function (subject, predicate, model) { + if (!this.options.localStorage) { + return; + } + + if (!subject || !predicate) { + return; + } + + var widget = this; + var identifier = subject + ':' + predicate; + var json = model.toJSONLD(); + if (window.localStorage.getItem(identifier)) { + var referenceList = JSON.parse(window.localStorage.getItem(identifier)); + var index = _.pluck(referenceList, '@').indexOf(json['@']); + if (index !== -1) { + referenceList[index] = json; + } else { + referenceList.push(json); + } + window.localStorage.setItem(identifier, JSON.stringify(referenceList)); + return; + } + window.localStorage.setItem(identifier, JSON.stringify([json])); + }, + + _checkLocal: function (model) { + if (!this.options.localStorage) { + return false; + } + + var local = window.localStorage.getItem(model.getSubjectUri()); + if (!local) { + return false; + } + + return true; + }, + + hasLocal: function (model) { + if (!this.options.localStorage) { + return false; + } + + if (!window.localStorage.getItem(model.getSubjectUri())) { + return false; + } + return true; + }, + + readLocal: function (model) { + if (!this.options.localStorage) { + return; + } + + var local = window.localStorage.getItem(model.getSubjectUri()); + if (!local) { + return; + } + if (!model._originalAttributes) { + model._originalAttributes = _.clone(model.attributes); + } + var parsed = JSON.parse(local); + var entity = this.vie.entities.addOrUpdate(parsed, { + overrideAttributes: true + }); + + this._trigger('loaded', null, { + instance: entity + }); + }, + + _readLocalReferences: function (model, property, collection) { + if (!this.options.localStorage) { + return; + } + + var identifier = this._getReferenceId(model, property); + var local = window.localStorage.getItem(identifier); + if (!local) { + return; + } + collection.add(JSON.parse(local)); + }, + + revertChanges: function (model) { + var widget = this; + + // Remove unsaved collection members + if (!model) { return; } + _.each(model.attributes, function (attributeValue, property) { + if (attributeValue instanceof widget.vie.Collection) { + var removables = []; + attributeValue.forEach(function (model) { + if (model.isNew()) { + removables.push(model); + } + }); + attributeValue.remove(removables); + } + }); + + // Restore original object properties + if (!model.changedAttributes()) { + if (model._originalAttributes) { + model.set(model._originalAttributes); + } + return; + } + + model.set(model.previousAttributes()); + }, + + _removeLocal: function (model) { + if (!this.localStorage) { + return; + } + + window.localStorage.removeItem(model.getSubjectUri()); + } +}); + +})(jQuery, Drupal); diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js index 7bef553..1c86fe4 100644 --- a/core/modules/edit/js/theme.js +++ b/core/modules/edit/js/theme.js @@ -75,6 +75,23 @@ Drupal.theme.editToolbarContainer = function(settings) { }; /** + * Theme function for a toolbar container of the Edit module. + * + * @param settings + * An object with the following keys: + * - id: the id to apply to the toolbar container. + * @return + * The corresponding HTML. + */ +Drupal.theme.editEntityToolbarContainer = function(settings) { + var html = ''; + html += '
    '; + html += '
    '; + html += '
    '; + return html; +}; + +/** * Theme function for a toolbar toolgroup of the Edit module. * * @param settings diff --git a/core/modules/edit/js/viejs/EditService.js b/core/modules/edit/js/viejs/EditService.js deleted file mode 100644 index 00cb04b..0000000 --- a/core/modules/edit/js/viejs/EditService.js +++ /dev/null @@ -1,289 +0,0 @@ -/** - * @file - * VIE DOM parsing service for Edit. - */ -(function(jQuery, _, VIE, Drupal, drupalSettings) { - -"use strict"; - - VIE.prototype.EditService = function (options) { - var defaults = { - name: 'edit', - subjectSelector: '.edit-field.edit-allowed' - }; - this.options = _.extend({}, defaults, options); - - this.views = []; - this.vie = null; - this.name = this.options.name; - }; - - VIE.prototype.EditService.prototype = { - load: function (loadable) { - var correct = loadable instanceof this.vie.Loadable; - if (!correct) { - throw new Error('Invalid Loadable passed'); - } - - var element; - if (!loadable.options.element) { - if (typeof document === 'undefined') { - return loadable.resolve([]); - } else { - element = drupalSettings.edit.context; - } - } else { - element = loadable.options.element; - } - - var entities = this.readEntities(element); - loadable.resolve(entities); - }, - - _getViewForElement:function (element, collectionView) { - var viewInstance; - - jQuery.each(this.views, function () { - if (jQuery(this.el).get(0) === element.get(0)) { - if (collectionView && !this.template) { - return true; - } - viewInstance = this; - return false; - } - }); - return viewInstance; - }, - - _registerEntityView:function (entity, element, isNew) { - if (!element.length) { - return; - } - - // Let's only have this overhead for direct types. Form-based editors are - // handled in backbone.drupalform.js and the PropertyEditor instance. - if (jQuery(element).hasClass('edit-type-form')) { - return; - } - - var service = this; - var viewInstance = this._getViewForElement(element); - if (viewInstance) { - return viewInstance; - } - - viewInstance = new this.vie.view.Entity({ - model:entity, - el:element, - tagName:element.get(0).nodeName, - vie:this.vie, - service:this.name - }); - - this.views.push(viewInstance); - - return viewInstance; - }, - - save: function(saveable) { - var correct = saveable instanceof this.vie.Savable; - if (!correct) { - throw "Invalid Savable passed"; - } - - if (!saveable.options.element) { - // FIXME: we could find element based on subject - throw "Unable to write entity to edit.module-markup, no element given"; - } - - if (!saveable.options.entity) { - throw "Unable to write to edit.module-markup, no entity given"; - } - - var $element = jQuery(saveable.options.element); - this._writeEntity(saveable.options.entity, saveable.options.element); - saveable.resolve(); - }, - - _writeEntity:function (entity, element) { - var service = this; - this.findPredicateElements(this.getElementSubject(element), element, true).each(function () { - var predicateElement = jQuery(this); - var predicate = service.getElementPredicate(predicateElement); - if (!entity.has(predicate)) { - return true; - } - - var value = entity.get(predicate); - if (value && value.isCollection) { - // Handled by CollectionViews separately - return true; - } - if (value === service.readElementValue(predicate, predicateElement)) { - return true; - } - // Unlike in the VIE's RdfaService no (re-)mapping needed here. - predicateElement.html(value); - }); - return true; - }, - - // The edit-id data attribute contains the full identifier of - // each entity element in the format - // `::::`. - _getID: function (element) { - var id = jQuery(element).attr('data-edit-id'); - if (!id) { - id = jQuery(element).closest('[data-edit-id]').attr('data-edit-id'); - } - return id; - }, - - // Returns the "URI" of an entity of an element in format - // `/`. - getElementSubject: function (element) { - return this._getID(element).split('/').slice(0, 2).join('/'); - }, - - // Returns the field name for an element in format - // `//`. - // (Slashes instead of colons because the field name is no namespace.) - getElementPredicate: function (element) { - if (!this._getID(element)) { - throw new Error('Could not find predicate for element'); - } - return this._getID(element).split('/').slice(2, 5).join('/'); - }, - - getElementType: function (element) { - return this._getID(element).split('/').slice(0, 1)[0]; - }, - - // Reads all editable entities (currently each Drupal field is considered an - // entity, in the future Drupal entities should be mapped to VIE entities) - // from DOM and returns the VIE enties it found. - readEntities: function (element) { - var service = this; - var entities = []; - var entityElements = jQuery(this.options.subjectSelector, element); - entityElements = entityElements.add(jQuery(element).filter(this.options.subjectSelector)); - entityElements.each(function () { - var entity = service._readEntity(jQuery(this)); - if (entity) { - entities.push(entity); - } - }); - return entities; - }, - - // Returns a filled VIE Entity instance for a DOM element. The Entity - // is also registered in the VIE entities collection. - _readEntity: function (element) { - var subject = this.getElementSubject(element); - var type = this.getElementType(element); - var entity = this._readEntityPredicates(subject, element, false); - if (jQuery.isEmptyObject(entity)) { - return null; - } - entity['@subject'] = subject; - if (type) { - entity['@type'] = this._registerType(type, element); - } - - var entityInstance = new this.vie.Entity(entity); - entityInstance = this.vie.entities.addOrUpdate(entityInstance, { - updateOptions: { - silent: true, - ignoreChanges: true - } - }); - - this._registerEntityView(entityInstance, element); - return entityInstance; - }, - - _registerType: function (typeId, element) { - typeId = ''; - var type = this.vie.types.get(typeId); - if (!type) { - this.vie.types.add(typeId, []); - type = this.vie.types.get(typeId); - } - - var predicate = this.getElementPredicate(element); - if (type.attributes.get(predicate)) { - return type; - } - var range = predicate.split('/')[0]; - type.attributes.add(predicate, [range], 0, 1, { - label: element.data('edit-field-label') - }); - - return type; - }, - - _readEntityPredicates: function (subject, element, emptyValues) { - var entityPredicates = {}; - var service = this; - this.findPredicateElements(subject, element, true).each(function () { - var predicateElement = jQuery(this); - var predicate = service.getElementPredicate(predicateElement); - if (!predicate) { - return; - } - var value = service.readElementValue(predicate, predicateElement); - if (value === null && !emptyValues) { - return; - } - - entityPredicates[predicate] = value; - entityPredicates[predicate + '/rendered'] = predicateElement[0].outerHTML; - }); - return entityPredicates; - }, - - readElementValue : function(predicate, element) { - // Unlike in RdfaService there is parsing needed here. - if (element.hasClass('edit-type-form')) { - return undefined; - } - else { - return jQuery.trim(element.html()); - } - }, - - // Subject elements are the DOM elements containing a single or multiple - // editable fields. - findSubjectElements: function (element) { - if (!element) { - element = drupalSettings.edit.context; - } - return jQuery(this.options.subjectSelector, element); - }, - - // Predicate Elements are the actual DOM elements that users will be able - // to edit. - findPredicateElements: function (subject, element, allowNestedPredicates, stop) { - var predicates = jQuery(); - // Make sure that element is wrapped by jQuery. - var $element = jQuery(element); - - // Form-type predicates - predicates = predicates.add($element.filter('.edit-type-form')); - - // Direct-type predicates - var direct = $element.filter('.edit-type-direct'); - predicates = predicates.add(direct.find('.field-item')); - - if (!predicates.length && !stop) { - var parentElement = $element.parent(this.options.subjectSelector); - if (parentElement.length) { - return this.findPredicateElements(subject, parentElement, allowNestedPredicates, true); - } - } - - return predicates; - } - }; - -})(jQuery, _, VIE, Drupal, drupalSettings); diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js new file mode 100644 index 0000000..900e910 --- /dev/null +++ b/core/modules/edit/js/views/AppView.js @@ -0,0 +1,292 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + AppView: Backbone.View.extend({ + + // Configuration for state handling. + activeEditorStates: [], + singleEditorStates: [], + + /** + * Implements Backbone Views' initialize() function. + */ + initialize: function (options) { + this.entitiesCollection = options.entitiesCollection; + + _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange'); + + // Instantiate configuration for state handling. + // @see Drupal.edit.FieldModel.states + this.activeEditorStates = ['activating', 'active']; + this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates); + + this.entitiesCollection.on('change:isActive', this.appStateChange, this); + }, + + /** + * Handles setup/teardown and state changes when the active entity changes. + */ + appStateChange: function (entityModel, isActive) { + var app = this; + if (isActive) { + // Move all fields of this entity from the 'inactive' state to the + // 'candidate' state. + entityModel.get('fields').each(function (fieldModel) { + // First, set up decoration views. + app.decorate(fieldModel); + // Second, change the field's state. + fieldModel.set('state', 'candidate'); + }); + } + else { + // Move all fields of this entity from whatever state they are in to + // the 'inactive' state. + entityModel.get('fields').each(function (fieldModel) { + // First, change the field's state. + fieldModel.set('state', 'inactive', { reason: 'stop' }); + // Second, tear down decoration views. + app.undecorate(fieldModel); + }); + } + }, + + /** + * Accepts or reject editor (PropertyEditor) state changes. + * + * This is what ensures that the app is in control of what happens. + * + * @param String from + * The previous state. + * @param String to + * The new state. + * @param null|Object context + * The context that is trying to trigger the state change. + * @param Function callback + * The callback function that should receive the state acceptance result. + */ + acceptEditorStateChange: function(from, to, context, callback) { + var accept = true; + + console.log("accept? %s → %s (reason: %s)", from, to, (context && context.reason) ? context.reason : 'NONE'); + + // If the app is in view mode, then reject all state changes except for + // those to 'inactive'. + if (context && context.reason === 'stop') { + if (from === 'candidate' && to === 'inactive') { + accept = true; + } + } + // Handling of edit mode state changes is more granular. + else { + // In general, enforce the states sequence. Disallow going back from a + // "later" state to an "earlier" state, except in explicitly allowed + // cases. + if (!Drupal.edit.FieldModel.followsStateSequence(from, to)) { + accept = false; + // Allow: activating/active -> candidate. + // Necessary to stop editing a property. + if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { + accept = true; + } + // Allow: changed/invalid -> candidate. + // Necessary to stop editing a property when it is changed or invalid. + else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { + accept = true; + } + // Allow: highlighted -> candidate. + // Necessary to stop highlighting a property. + else if (from === 'highlighted' && to === 'candidate') { + accept = true; + } + // Allow: saved -> candidate. + // Necessary when successfully saved a property. + else if (from === 'saved' && to === 'candidate') { + accept = true; + } + // Allow: invalid -> saving. + // Necessary to be able to save a corrected, invalid property. + else if (from === 'invalid' && to === 'saving') { + accept = true; + } + } + + // If it's not against the general principle, then here are more + // disallowed cases to check. + if (accept) { + // Ensure only one editor (field) at a time may be higlighted or active. + if (from === 'candidate' && _.indexOf(this.singleEditorStates, to) !== -1) { + if (this.model.get('highlightedEditor') || this.model.get('activeEditor')) { + accept = false; + } + } + // Reject going from activating/active to candidate because of a + // mouseleave. + else if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { + if (context && context.reason === 'mouseleave') { + accept = false; + } + } + // When attempting to stop editing a changed/invalid property, ask for + // confirmation. + else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { + if (context && context.reason === 'mouseleave') { + accept = false; + } + else { + // Check whether the transition has been confirmed? + if (context && context.confirmed) { + accept = true; + } + // Confirm this transition. + else { + // Do not accept this change right now, instead open a modal + // that will ask the user to confirm his choice. + accept = false; + // The callback will be called from the helper function. + this._confirmStopEditing(callback); + } + } + } + } + } + + return accept; + }, + + // @todo rename to decorateField + decorate: function (fieldModel) { + var editID = fieldModel.get('editID'); + var $el = fieldModel.get('$el'); + + // Create a new Editor. + var editorName = fieldModel.get('editor'); + var editorView = new Drupal.edit.editors[editorName]({ + el: $el, + model: fieldModel + }); + + // Toolbars are rendered "on-demand" (highlighting or activating). + // They are a sibling element before the editor's DOM element. + var toolbarView = new Drupal.edit.ToolbarView({ + model: fieldModel, + $field: $el, + editorView: editorView + }); + + // Decorate the editor's DOM element depending on its state. + var decorationView = new Drupal.edit.PropertyEditorDecorationView({ + el: $el, + model: fieldModel, + editorView: editorView, + toolbarId: toolbarView.getId() + }); + + // Create references in the field model; necessary for undecorate() and + // necessary for some EditorView implementations. + fieldModel.set('editorView', editorView); + fieldModel.set('toolbarView', toolbarView); + fieldModel.set('decorationView', decorationView); + }, + + // @todo rename to undecorateField + undecorate: function (fieldModel) { + // Unbind event handlers; remove toolbar element; delete toolbar view. + var toolbarView = fieldModel.get('toolbarView'); + toolbarView.undelegateEvents(); + toolbarView.remove(); + fieldModel.unset('toolbarView'); + + // Unbind event handlers; delete decoration view. Don't remove the element + // because that would remove the field itself. + var decorationView = fieldModel.get('decorationView'); + decorationView.undelegateEvents(); + fieldModel.unset('decorationView'); + + // Unbind event handlers; delete editor view. Don't remove the element + // because that would remove the field itself. + var editorView = fieldModel.get('editorView'); + editorView.undelegateEvents(); + fieldModel.unset('editorView'); + }, + + /** + * Asks the user to confirm whether he wants to stop editing via a modal. + * + * @see acceptEditorStateChange() + */ + _confirmStopEditing: function () { + // Only instantiate if there isn't a modal instance visible yet. + if (!this.model.get('activeModal')) { + var that = this; + var modal = new Drupal.edit.ModalView({ + model: this.model, + message: Drupal.t('You have unsaved changes'), + buttons: [ + { action: 'discard', classes: 'gray-button', label: Drupal.t('Discard changes') }, + { action: 'save', type: 'submit', classes: 'blue-button', label: Drupal.t('Save') } + ], + callback: function(action) { + // The active modal has been removed. + that.model.set('activeModal', null); + // Set the state that matches the user's action. + var targetState = (action === 'discard') ? 'candidate' : 'saving'; + that.model.get('activeEditor').set('state', 'candidate', { confirmed: true }); + } + }); + this.model.set('activeModal', modal); + // The modal will set the activeModal property on the model when rendering + // to prevent multiple modals from being instantiated. + modal.render(); + } + }, + + /** + * Reacts to field state changes; tracks global state. + * + * @param Drupal.edit.FieldModel fieldModel + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + editorStateChange: function(fieldModel, state) { + var from = fieldModel.previous('state'); + var to = state; + + console.log("%c [APP] %s → %s %s", "background-color: black; color: white", from, to, fieldModel.get('editID')); + + // Keep track of the highlighted editor in the global state. + if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== fieldModel) { + this.model.set('highlightedEditor', fieldModel); + } + else if (this.model.get('highlightedEditor') === fieldModel && to === 'candidate') { + this.model.set('highlightedEditor', null); + } + + // Keep track of the active editor in the global state. + if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== fieldModel) { + this.model.set('activeEditor', fieldModel); + } + else if (this.model.get('activeEditor') === fieldModel && to === 'candidate') { + // Discarded if it transitions from a changed state to 'candidate'. + if (from === 'changed' || from === 'invalid') { + // @todo FIX THIS! + // Retrieve the storage widget from DOM. + // var createStorageWidget = this.$el.data('DrupalCreateStorage'); + // Revert changes in the model, this will trigger the direct editable + // content to be reset and redrawn. + // createStorageWidget.revertChanges(fieldModel.options.entity); + } + this.model.set('activeEditor', null); + } + } + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/views/ContextualLinkView.js b/core/modules/edit/js/views/ContextualLinkView.js new file mode 100644 index 0000000..401e60a --- /dev/null +++ b/core/modules/edit/js/views/ContextualLinkView.js @@ -0,0 +1,75 @@ +/** + * @file + * A Backbone View that a dynamic contextual link. + */ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +// Main model: Drupal.edit.EntityModel +Drupal.edit.ContextualLinkView = Backbone.View.extend({ + + events: { + 'click .quick-edit a': function (event) { + event.preventDefault(); + this.model.set('isActive', !this.model.get('isActive')); + } + }, + + /** + * Implements Backbone.View.prototype.initialize(). + * + * @param Object options + * An object with the following keys: + * - appModel: the application state model + * - strings: the strings for the "Quick edit" link + */ + initialize: function (options) { + this.appModel = options.appModel; + this.strings = options.strings; + + // @todo Is this a good idea at all? I'm very unsure… + if (!this.model instanceof Drupal.edit.EntityModel) { + throw new Error('Drupal.edit.ContextualLinkView only accepts Drupal.edit.EntityModel models.'); + } + + // Build the DOM elements. + // @todo make this work in a generic way instead of applying this hack, + // being handled at http://drupal.org/node/1971108. + this.$el + .find('li.node-edit') + .before('
  • '); + + // Initial render. + this.render(); + + // Re-render whenever this entity's isActive attribute changes. + this.model.on('change:isActive', this.render, this); + + // Hide the contextual links whenever an in-place editor is active. + this.appModel.on('change:activeEditor', this.toggleContextualLinksVisibility, this); + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function () { + var isActive = this.model.get('isActive'); + this.$el.find('.quick-edit a').text((!isActive) ? this.strings.quickEdit : this.strings.stopQuickEdit); + return this; + }, + + /** + * Hides the contextual links if an in-place editor is active. + * + * @param Drupal.edit.AppModel model + * @param null|Drupal.edit.FieldModel activeEditor + * The model of the field that is currently being edited, or, if none, null. + */ + toggleContextualLinksVisibility: function (model, activeEditor) { + this.$el.parents('.contextual').toggle(activeEditor === null); + } + +}); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js new file mode 100644 index 0000000..cbc46f1 --- /dev/null +++ b/core/modules/edit/js/views/EntityToolbarView.js @@ -0,0 +1,333 @@ +/** + * @file + * A Backbone View that provides an entity level toolbar. + */ +(function ($, Backbone, Drupal, debounce) { + +"use strict"; + +Drupal.edit.EntityToolbarView = Backbone.View.extend({ + + _loader: null, + _loaderVisibleStart: 0, + + events: function () { + var map = { + 'click.edit button.field-save': 'onClickSave', + 'click.edit button.field-close': 'onClickClose' + } + return map; + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function (options) { + var that = this; + + this.model.on('change:isActive', this.render, this); + + // When the fieldModel attribute of the EntityModel changes, set a listener + // on the active fieldModel that proxies its state changes to this view. + this.model.on('change:fieldModel', function (model, fieldModel) { + // Remove the listener from the previous fieldModel. + that.model.previous('fieldModel').off('change:state', that.fieldStateChange, that); + // Attach a change listener to the current active fieldModel. + fieldModel.on('change:state', that.fieldStateChange, that); + }); + + $(window).on('resize.edit scroll.edit', debounce($.proxy(this.windowChangeHandler, this), 150)); + + // Set the el into its own property. Eventually the el property will be + // replaced with the rendered toolbar. + this.$entity = this.$el; + + // Set the toolbar container to this view's el property. + this.buildToolbar(); + + this._loader = null; + this._loaderVisibleStart = 0; + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function (model, changeValue) { + + if (this.model.get('isActive')) { + // If the toolbar container doesn't exist, create it. + if ($('body').children('#edit-entity-toolbar').length === 0) { + $('body').append(this.$el); + } + // If render is being called and the toolbar is already visible, just + // reposition it. + this.position(); + this.show('ops'); + } + else { + this.$el.detach(); + } + + return this; + }, + + /** + * + */ + windowChangeHandler: function (event) { + this.position(); + }, + + /** + * + */ + position: function (element) { + var of = element || this.$entity; + this.$el + .position({ + my: 'left bottom', + at: 'left top', + of: of + }); + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + fieldStateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + if (from) { + this.remove(); + if (this.model.get('editor') !== 'form') { + Backbone.syncDirectCleanUp(); + } + } + break; + case 'candidate': + break; + if (from === 'inactive') { + this.render(); + } + else { + if (this.model.get('editor') !== 'form') { + Backbone.syncDirectCleanUp(); + } + // Remove all toolgroups; they're no longer necessary. + this.$el + .removeClass('edit-highlighted edit-editing') + .find('.edit-toolbar .edit-toolgroup').remove(); + if (from !== 'highlighted' && this.getEditUISetting('padding')) { + this._unpad(); + } + } + break; + case 'highlighted': + // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title). + this.startHighlight(); + break; + case 'activating': + this.setLoadingIndicator(true); + break; + case 'active': + this.startEdit(); + this.setLoadingIndicator(false); + // Position the toolbar against the active field. + this.position(model.get('$el')); + /*if (this.getEditUISetting('fullWidthToolbar')) { + this.$el.addClass('edit-toolbar-fullwidth'); + }*/ + + /*if (this.getEditUISetting('padding')) { + this._pad(); + }*/ + /*if (this.getEditUISetting('unifiedToolbar')) { + this.insertWYSIWYGToolGroups(); + }*/ + break; + case 'changed': + this.$el + .find('button.save') + .addClass('blue-button') + .removeClass('gray-button'); + break; + case 'saving': + this.setLoadingIndicator(true); + break; + case 'saved': + this.setLoadingIndicator(false); + break; + case 'invalid': + this.setLoadingIndicator(false); + break; + } + }, + + /** + * Set the model state to 'saving' when the save button is clicked. + * + * @param jQuery event + */ + onClickSave: function (event) { + event.stopPropagation(); + event.preventDefault(); + this.model.set('state', 'saving'); + }, + + /** + * Sets the model state to candidate when the cancel button is clicked. + * + * @param jQuery event + */ + onClickClose: function (event) { + event.stopPropagation(); + event.preventDefault(); + this.model.get('fields') + .each(function (fieldModel) { + fieldModel.set('state', 'candidate', { reason: 'cancel' }); + }); + this.model.set('isActive', false); + }, + + /** + * + */ + buildToolbar: function () { + var $toolbar; + $toolbar = $(Drupal.theme('editEntityToolbarContainer', { + id: 'edit-entity-toolbar' + })); + + $toolbar + .find('.edit-toolbar') + // Append the "ops" toolgroup into the toolbar. + .append(Drupal.theme('editToolgroup', { + classes: 'ops', + buttons: [ + { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' }, + { label: '' + Drupal.t('Close') + '', classes: 'field-close close gray-button' } + ] + })); + + // Give the toolbar a sensible starting position so that it doesn't + // animiate on to the screen from a far off corner. + $toolbar + .css({ + left: this.$entity.offset().left, + top: this.$entity.offset().top + }); + + this.setElement($toolbar); + }, + + /** + * + */ + startEdit: function () { + this.$el.addClass('edit-editing'); + }, + + /** + * + */ + startHighlight: function () { + // Retrieve the label to show for this field. + var label = this.model.get('label'); + + this.$el + .addClass('edit-highlighted') + .find('.edit-toolbar') + // Append the "info" toolgroup into the toolbar. + .append(Drupal.theme('editToolgroup', { + classes: 'info edit-animate-only-background-and-padding', + buttons: [ + { label: label, classes: 'blank-button label' } + ] + })); + + // Animations. + var that = this; + setTimeout(function () { + that.show('info'); + }, 0); + }, + + /** + * Indicates in the 'info' toolgroup that we're waiting for a server reponse. + * + * Prevents flickering loading indicator by only showing it after 0.6 seconds + * and if it is shown, only hiding it after another 0.6 seconds. + * + * @param Boolean enabled + * Whether the loading indicator should be displayed or not. + */ + setLoadingIndicator: function (enabled) { + var that = this; + if (enabled) { + this._loader = setTimeout(function() { + that.addClass('info', 'loading'); + that._loaderVisibleStart = new Date().getTime(); + }, 600); + } + else { + var currentTime = new Date().getTime(); + clearTimeout(this._loader); + if (this._loaderVisibleStart) { + setTimeout(function() { + that.removeClass('info', 'loading'); + }, this._loaderVisibleStart + 600 - currentTime); + } + this._loader = null; + this._loaderVisibleStart = 0; + } + }, + + /** + * Adds classes to a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + addClass: function (toolgroup, classes) { + this._find(toolgroup).addClass(classes); + }, + + /** + * Removes classes from a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + removeClass: function (toolgroup, classes) { + this._find(toolgroup).removeClass(classes); + }, + + /** + * Finds a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + _find: function (toolgroup) { + return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup); + }, + + /** + * Shows a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + show: function (toolgroup) { + this._find(toolgroup).removeClass('edit-animate-invisible'); + } +}); + +})(jQuery, Backbone, Drupal, Drupal.debounce); diff --git a/core/modules/edit/js/views/ModalView.js b/core/modules/edit/js/views/ModalView.js new file mode 100644 index 0000000..15497cc --- /dev/null +++ b/core/modules/edit/js/views/ModalView.js @@ -0,0 +1,80 @@ +/** + * @file + * A Backbone View that provides an interactive modal. + */ +(function ($, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.ModalView = Backbone.View.extend({ + + message: null, + buttons: null, + callback: null, + $elementsToHide: null, + + events: { + 'click button': 'onButtonClick' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + * + * @param Object options + * An object with the following keys: + * - message: a message to show in the modal. + * - buttons: a set of buttons with 'action's defined, ready to be passed to + * Drupal.theme.editButtons(). + * - callback: a callback that will receive the 'action' of the clicked + * button. + * + * @see Drupal.theme.editModal() + * @see Drupal.theme.editButtons() + */ + initialize: function (options) { + this.message = options.message; + this.buttons = options.buttons; + this.callback = options.callback; + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function () { + this.setElement(Drupal.theme('editModal', {})); + this.$el.appendTo('body'); + // Template. + this.$('.main p').text(this.message); + var $actions = $(Drupal.theme('editButtons', { 'buttons' : this.buttons})); + this.$('.actions').append($actions); + + // Show the modal with an animation. + var that = this; + setTimeout(function() { + that.$el.removeClass('edit-animate-invisible'); + }, 0); + }, + + /** + * Passes the clicked button action to the callback; closes the modal. + * + * @param jQuery event + */ + onButtonClick: function (event) { + event.stopPropagation(); + event.preventDefault(); + + // Remove after animation. + var that = this; + this.$el + .addClass('edit-animate-invisible') + .on(Drupal.edit.util.constants.transitionEnd, function(e) { + that.remove(); + }); + + var action = $(event.target).attr('data-edit-modal-action'); + return this.callback(action); + } +}); + +})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/PropertyEditorDecorationView.js b/core/modules/edit/js/views/PropertyEditorDecorationView.js new file mode 100644 index 0000000..6eb7205 --- /dev/null +++ b/core/modules/edit/js/views/PropertyEditorDecorationView.js @@ -0,0 +1,393 @@ +/** + * @file + * A Backbone View that decorates a Property Editor widget. + * + * It listens to state changes of the property editor. + */ +(function ($, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.PropertyEditorDecorationView = Backbone.View.extend({ + toolbarId: null, + + _widthAttributeIsEmpty: null, + + events: { + 'mouseenter.edit' : 'onMouseEnter', + 'mouseleave.edit' : 'onMouseLeave', + 'click': 'onClick', + 'tabIn.edit': 'onMouseEnter', + 'tabOut.edit': 'onMouseLeave' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + * + * @param Object options + * An object with the following keys: + * - editor: the editor object with an 'options' object that has these keys: + * * entity: the VIE entity for the property. + * * property: the predicate of the property. + * * widget: the parent EditableEntity widget. + * * editorName: the name of the PropertyEditor widget + * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. + */ + initialize: function (options) { + this.editorView = options.editorView; + + this.toolbarId = options.toolbarId; + + this.model.on('change:state', this.stateChange, this); + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + if (from !== null) { + this.undecorate(); + if (from === 'invalid') { + this._removeValidationErrors(); + } + } + break; + case 'candidate': + this.decorate(); + if (from !== 'inactive') { + this.stopHighlight(); + if (from !== 'highlighted') { + this.stopEdit(); + if (from === 'invalid') { + this._removeValidationErrors(); + } + } + } + break; + case 'highlighted': + this.startHighlight(); + break; + case 'activating': + // NOTE: this state is not used by every editor! It's only used by those + // that need to interact with the server. + this.prepareEdit(); + break; + case 'active': + if (from !== 'activating') { + this.prepareEdit(); + } + this.startEdit(); + break; + case 'changed': + break; + case 'saving': + if (from === 'invalid') { + this._removeValidationErrors(); + } + break; + case 'saved': + break; + case 'invalid': + break; + } + }, + + /** + * Starts hover; transitions to 'highlight' state. + * + * @param jQuery event + */ + onMouseEnter: function (event) { + var that = this; + this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { + that.model.set('state', 'highlighted'); + event.stopPropagation(); + }); + }, + + /** + * Stops hover; transitions to 'candidate' state. + * + * @param jQuery event + */ + onMouseLeave: function (event) { + var that = this; + this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { + that.model.set('state', 'candidate', { reason: 'mouseleave' }); + event.stopPropagation(); + }); + }, + + /** + * Transition to 'activating' stage. + * + * @param jQuery event + */ + onClick: function (event) { + this.model.set('state', 'activating'); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Adds classes used to indicate an elements editable state. + */ + decorate: function () { + this.$el.addClass('edit-animate-fast edit-candidate edit-editable'); + }, + + /** + * Removes classes used to indicate an elements editable state. + */ + undecorate: function () { + this.$el.removeClass('edit-candidate edit-editable edit-highlighted edit-editing'); + }, + + /** + * Adds that class that indicates that an element is highlighted. + */ + startHighlight: function () { + // Animations. + var that = this; + // Use a timeout to grab the next available animation frame. + setTimeout(function () { + that.$el.addClass('edit-highlighted'); + }, 0); + }, + + /** + * Removes the class that indicates that an element is highlighted. + */ + stopHighlight: function () { + this.$el.removeClass('edit-highlighted'); + }, + + /** + * Removes the class that indicates that an element as editable. + */ + prepareEdit: function () { + this.$el.addClass('edit-editing'); + + // While editing, do not show any other editors. + $('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); + }, + + /** + * Updates the display of the editable element once editing has begun. + */ + startEdit: function () { + if (this.getEditUISetting('padding')) { + this._pad(); + } + }, + + /** + * Removes the class that indicates that an element is being edited. + * + * Reapplies the class that indicates that a candidate editable element is + * again available to be edited. + */ + stopEdit: function () { + this.$el.removeClass('edit-highlighted edit-editing'); + + // Make the other editors show up again. + // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) + // Revisit this. + $('.edit-candidate').addClass('edit-editable'); + + if (this.getEditUISetting('padding')) { + this._unpad(); + } + }, + + /** + * Retrieves a setting of the editor-specific Edit UI integration. + * + * @param String setting + * + * @see Drupal.edit.util.getEditUISetting(). + */ + getEditUISetting: function (setting) { + return Drupal.edit.util.getEditUISetting(this.editorView, setting); + }, + + /** + * Adds padding around the editable element in order to make it pop visually. + */ + _pad: function () { + var self = this; + + // Add 5px padding for readability. This means we'll freeze the current + // width and *then* add 5px padding, hence ensuring the padding is added "on + // the outside". + // 1) Freeze the width (if it's not already set); don't use animations. + if (this.$el[0].style.width === "") { + this._widthAttributeIsEmpty = true; + this.$el + .addClass('edit-animate-disable-width') + .css('width', this.$el.width()) + .css('background-color', this._getBgColor(this.$el)); + } + + // 2) Add padding; use animations. + var posProp = this._getPositionProperties(this.$el); + setTimeout(function() { + // Re-enable width animations (padding changes affect width too!). + self.$el.removeClass('edit-animate-disable-width'); + + // Pad the editable. + self.$el + .css({ + 'position': 'relative', + 'top': posProp.top - 5 + 'px', + 'left': posProp.left - 5 + 'px', + 'padding-top' : posProp['padding-top'] + 5 + 'px', + 'padding-left' : posProp['padding-left'] + 5 + 'px', + 'padding-right' : posProp['padding-right'] + 5 + 'px', + 'padding-bottom': posProp['padding-bottom'] + 5 + 'px', + 'margin-bottom': posProp['margin-bottom'] - 10 + 'px' + }); + }, 0); + }, + + /** + * Removes the padding around the element being edited when editing ceases. + */ + _unpad: function () { + var self = this; + + // 1) Set the empty width again. + if (this._widthAttributeIsEmpty) { + this.$el + .addClass('edit-animate-disable-width') + .css('width', '') + .css('background-color', ''); + } + + // 2) Remove padding; use animations (these will run simultaneously with) + // the fading out of the toolbar as its gets removed). + var posProp = this._getPositionProperties(this.$el); + setTimeout(function() { + // Re-enable width animations (padding changes affect width too!). + self.$el.removeClass('edit-animate-disable-width'); + + // Unpad the editable. + self.$el + .css({ + 'position': 'relative', + 'top': posProp.top + 5 + 'px', + 'left': posProp.left + 5 + 'px', + 'padding-top' : posProp['padding-top'] - 5 + 'px', + 'padding-left' : posProp['padding-left'] - 5 + 'px', + 'padding-right' : posProp['padding-right'] - 5 + 'px', + 'padding-bottom': posProp['padding-bottom'] - 5 + 'px', + 'margin-bottom': posProp['margin-bottom'] + 10 + 'px' + }); + }, 0); + }, + + /** + * Gets the background color of an element (or the inherited one). + * + * @param DOM $e + */ + _getBgColor: function ($e) { + var c; + + if ($e === null || $e[0].nodeName === 'HTML') { + // Fallback to white. + return 'rgb(255, 255, 255)'; + } + c = $e.css('background-color'); + // TRICKY: edge case for Firefox' "transparent" here; this is a + // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724 + if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') { + return this._getBgColor($e.parent()); + } + return c; + }, + + /** + * Gets the top and left properties of an element. + * + * Convert extraneous values and information into numbers ready for + * subtraction. + * + * @param DOM $e + */ + _getPositionProperties: function ($e) { + var p, + r = {}, + props = [ + 'top', 'left', 'bottom', 'right', + 'padding-top', 'padding-left', 'padding-right', 'padding-bottom', + 'margin-bottom' + ]; + + var propCount = props.length; + for (var i = 0; i < propCount; i++) { + p = props[i]; + r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10); + } + return r; + }, + + /** + * Replaces blank or 'auto' CSS "position: " values with "0px". + * + * @param String pos + * (optional) The value for a CSS position declaration. + */ + _replaceBlankPosition: function (pos) { + if (pos === 'auto' || !pos) { + pos = '0px'; + } + return pos; + }, + + /** + * Ignores hovering to/from the given closest element. + * + * When a hover occurs to/from another element, invoke the callback. + * + * @param jQuery event + * @param jQuery closest + * A jQuery-wrapped DOM element or compatibale jQuery input. The element + * whose mouseenter and mouseleave events should be ignored. + * @param Function callback + */ + _ignoreHoveringVia: function (event, closest, callback) { + if ($(event.relatedTarget).closest(closest).length > 0) { + event.stopPropagation(); + } + else { + callback(); + } + }, + + /** + * Removes validation error's markup changes, if any. + * + * Note: this only needs to happen for type=direct, because for type=direct, + * the property DOM element itself is modified; this is not the case for + * type=form. + */ + _removeValidationErrors: function () { + if (this.model.get('editor') !== 'form') { + this.$el + .removeClass('edit-validation-error') + .next('.edit-validation-errors') + .remove(); + } + } +}); + +})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/ToolbarView.js b/core/modules/edit/js/views/ToolbarView.js new file mode 100644 index 0000000..08dcb78 --- /dev/null +++ b/core/modules/edit/js/views/ToolbarView.js @@ -0,0 +1,194 @@ +/** + * @file + * A Backbone View that provides an interactive toolbar (1 per property editor). + * + * It listens to state changes of the property editor. It also triggers state + * changes in response to user interactions with the toolbar, including saving. + */ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.ToolbarView = Backbone.View.extend({ + $field: null, + + _id: null, + + events: { + 'click.edit button.label': 'onClickInfoLabel', + 'mouseleave.edit': 'onMouseLeave' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function (options) { + this.$field = options.$field; + this.editorView = options.editorView; + + // Generate a DOM-compatible ID for the form container DOM element. + this._id = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_'); + }, + + /** + * Implements Backbone.View.prototype.render(). + * + * Depending on whether the display property of the $el for which a + * toolbar is being inserted into the DOM, it will be inserted differently. + */ + render: function () { + // Render toolbar. + this.setElement($(Drupal.theme('editToolbarContainer', { + id: this._id + }))); + + // Insert in DOM. + if (this.$field.css('display') === 'inline') { + this.$el.prependTo(this.$field.offsetParent()); + var pos = this.$field.position(); + this.$el.css('left', pos.left).css('top', pos.top); + } + else { + this.$el.insertBefore(this.$field); + } + + return this; + }, + + /** + * Redirects the click.edit-event to the editor DOM element. + * + * @param jQuery event + */ + onClickInfoLabel: function (event) { + event.stopPropagation(); + event.preventDefault(); + // Redirects the event to the editor DOM element. + this.$field.trigger('click.edit'); + }, + + /** + * Controls mouseleave events. + * + * A mouseleave to the editor doesn't matter; a mouseleave to something else + * counts as a mouseleave on the editor itself. + * + * @param jQuery event + */ + onMouseLeave: function (event) { + if (event.relatedTarget !== this.$field[0] && !$.contains(this.$field, event.relatedTarget)) { + this.$field.trigger('mouseleave.edit'); + } + event.stopPropagation(); + }, + + /** + * Retrieves a setting of the editor-specific Edit UI integration. + * + * @param String setting + * + * @see Drupal.edit.util.getEditUISetting(). + */ + getEditUISetting: function (setting) { + return Drupal.edit.util.getEditUISetting(this.editorView, setting); + }, + + /** + * Adjusts the toolbar to accomodate padding on the PropertyEditor widget. + * + * @see PropertyEditorDecorationView._pad(). + */ + _pad: function () { + // The whole toolbar must move to the top when the property's DOM element + // is displayed inline. + if (this.$field.css('display') === 'inline') { + this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); + } + + // The toolbar must move to the top and the left. + var $hf = this.$el.find('.edit-toolbar-heightfaker'); + $hf.css({ bottom: '6px', left: '-5px' }); + + if (this.getEditUISetting('fullWidthToolbar')) { + $hf.css({ width: this.$field.width() + 10 }); + } + }, + + /** + * Undoes the changes made by _pad(). + * + * @see PropertyEditorDecorationView._unpad(). + */ + _unpad: function () { + // Move the toolbar back to its original position. + var $hf = this.$el.find('.edit-toolbar-heightfaker'); + $hf.css({ bottom: '1px', left: '' }); + + if (this.getEditUISetting('fullWidthToolbar')) { + $hf.css({ width: '' }); + } + }, + + /** + * + */ + insertWYSIWYGToolGroups: function () { + this.$el + .find('.edit-toolbar') + .append(Drupal.theme('editToolgroup', { + id: this.getFloatedWysiwygToolgroupId(), + classes: 'wysiwyg-floated', + buttons: [] + })) + .append(Drupal.theme('editToolgroup', { + id: this.getMainWysiwygToolgroupId(), + classes: 'wysiwyg-main', + buttons: [] + })); + + // Animate the toolgroups into visibility. + var that = this; + setTimeout(function () { + that.show('wysiwyg-floated'); + that.show('wysiwyg-main'); + }, 0); + }, + + /** + * Retrieves the ID for this toolbar's container. + * + * Only used to make sane hovering behavior possible. + * + * @return String + * A string that can be used as the ID for this toolbar's container. + */ + getId: function () { + return 'edit-toolbar-for-' + this._id; + }, + + /** + * Retrieves the ID for this toolbar's floating WYSIWYG toolgroup. + * + * Used to provide an abstraction for any WYSIWYG editor to plug in. + * + * @return String + * A string that can be used as the ID. + */ + getFloatedWysiwygToolgroupId: function () { + return 'edit-wysiwyg-floated-toolgroup-for-' + this._id; + }, + + /** + * Retrieves the ID for this toolbar's main WYSIWYG toolgroup. + * + * Used to provide an abstraction for any WYSIWYG editor to plug in. + * + * @return String + * A string that can be used as the ID. + */ + getMainWysiwygToolgroupId: function () { + return 'edit-wysiwyg-main-toolgroup-for-' + this._id; + } +}); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/contextuallink-view.js b/core/modules/edit/js/views/contextuallink-view.js deleted file mode 100644 index efe8ddd..0000000 --- a/core/modules/edit/js/views/contextuallink-view.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @file - * A Backbone View that a dynamic contextual link. - */ -(function ($, _, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ - - entity: null, - - events: { - 'click': 'onClick' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - entity: the entity ID (e.g. node/1) of the entity - */ - initialize: function (options) { - this.entity = options.entity; - - // Initial render. - this.render(); - - // Re-render whenever the app state's active entity changes. - this.model.on('change:activeEntity', this.render, this); - - // Hide the contextual links whenever an in-place editor is active. - this.model.on('change:activeEditor', this.toggleContextualLinksVisibility, this); - }, - - /** - * Equates clicks anywhere on the overlay to clicking the active editor's (if - * any) "close" button. - * - * @param {Object} event - */ - onClick: function (event) { - event.preventDefault(); - - var that = this; - var updateActiveEntity = function() { - // The active entity is the current entity, i.e. stop editing the current - // entity. - if (that.model.get('activeEntity') === that.entity) { - that.model.set('activeEntity', null); - } - // The active entity is different from the current entity, i.e. start - // editing this entity instead of the previous one. - else { - that.model.set('activeEntity', that.entity); - } - }; - - // If there's an active editor, attempt to set its state to 'candidate', and - // only then do what the user asked. - // (Only when all PropertyEditor widgets of an entity are in the 'candidate' - // state, it is possible to stop editing it.) - var activeEditor = this.model.get('activeEditor'); - if (activeEditor) { - var editableEntity = activeEditor.options.widget; - var predicate = activeEditor.options.property; - editableEntity.setState('candidate', predicate, { reason: 'stop or switch' }, function(accepted) { - if (accepted) { - updateActiveEntity(); - } - else { - // No change. - } - }); - } - // Otherwise, we can immediately do what the user asked. - else { - updateActiveEntity(); - } - }, - - /** - * Render the "Quick edit" contextual link. - */ - render: function () { - var activeEntity = this.model.get('activeEntity'); - var string = (activeEntity !== this.entity) ? Drupal.t('Quick edit') : Drupal.t('Stop quick edit'); - this.$el.html('' + string + ''); - return this; - }, - - /** - * Model change handler; hides the contextual links if an editor is active. - * - * @param Drupal.edit.models.EditAppModel model - * An EditAppModel model. - * @param jQuery|null activeEditor - * The active in-place editor (jQuery object) or, if none, null. - */ - toggleContextualLinksVisibility: function (model, activeEditor) { - this.$el.parents('.contextual').toggle(activeEditor === null); - } - -}); - -})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/modal-view.js b/core/modules/edit/js/views/modal-view.js deleted file mode 100644 index b98c876..0000000 --- a/core/modules/edit/js/views/modal-view.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @file - * A Backbone View that provides an interactive modal. - */ -(function($, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ModalView = Backbone.View.extend({ - - message: null, - buttons: null, - callback: null, - $elementsToHide: null, - - events: { - 'click button': 'onButtonClick' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - message: a message to show in the modal. - * - buttons: a set of buttons with 'action's defined, ready to be passed to - * Drupal.theme.editButtons(). - * - callback: a callback that will receive the 'action' of the clicked - * button. - * - * @see Drupal.theme.editModal() - * @see Drupal.theme.editButtons() - */ - initialize: function(options) { - this.message = options.message; - this.buttons = options.buttons; - this.callback = options.callback; - }, - - /** - * Implements Backbone Views' render() function. - */ - render: function() { - this.setElement(Drupal.theme('editModal', {})); - this.$el.appendTo('body'); - // Template. - this.$('.main p').text(this.message); - var $actions = $(Drupal.theme('editButtons', { 'buttons' : this.buttons})); - this.$('.actions').append($actions); - - // Show the modal with an animation. - var that = this; - setTimeout(function() { - that.$el.removeClass('edit-animate-invisible'); - }, 0); - }, - - /** - * When the user clicks on any of the buttons, the modal should be removed - * and the result should be passed to the callback. - * - * @param event - */ - onButtonClick: function(event) { - event.stopPropagation(); - event.preventDefault(); - - // Remove after animation. - var that = this; - this.$el - .addClass('edit-animate-invisible') - .on(Drupal.edit.util.constants.transitionEnd, function(e) { - that.remove(); - }); - - var action = $(event.target).attr('data-edit-modal-action'); - return this.callback(action); - } -}); - -})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js deleted file mode 100644 index bee33d1..0000000 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ /dev/null @@ -1,363 +0,0 @@ -/** - * @file - * A Backbone View that decorates a Property Editor widget. - * - * It listens to state changes of the property editor. - */ -(function($, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ - - toolbarId: null, - - _widthAttributeIsEmpty: null, - - events: { - 'mouseenter.edit' : 'onMouseEnter', - 'mouseleave.edit' : 'onMouseLeave', - 'click': 'onClick', - 'tabIn.edit': 'onMouseEnter', - 'tabOut.edit': 'onMouseLeave' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - editor: the editor object with an 'options' object that has these keys: - * * entity: the VIE entity for the property. - * * property: the predicate of the property. - * * widget: the parent EditableEntity widget. - * * editorName: the name of the PropertyEditor widget - * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. - */ - initialize: function(options) { - this.editor = options.editor; - this.toolbarId = options.toolbarId; - - this.predicate = this.editor.options.property; - this.editorName = this.editor.options.editorName; - - // Only start listening to events as soon as we're no longer in the 'inactive' state. - this.undelegateEvents(); - }, - - /** - * Listens to editor state changes. - */ - stateChange: function(from, to) { - switch (to) { - case 'inactive': - if (from !== null) { - this.undecorate(); - if (from === 'invalid') { - this._removeValidationErrors(); - } - } - break; - case 'candidate': - this.decorate(); - if (from !== 'inactive') { - this.stopHighlight(); - if (from !== 'highlighted') { - this.stopEdit(); - if (from === 'invalid') { - this._removeValidationErrors(); - } - } - } - break; - case 'highlighted': - this.startHighlight(); - break; - case 'activating': - // NOTE: this state is not used by every editor! It's only used by those - // that need to interact with the server. - this.prepareEdit(); - break; - case 'active': - if (from !== 'activating') { - this.prepareEdit(); - } - this.startEdit(); - break; - case 'changed': - break; - case 'saving': - if (from === 'invalid') { - this._removeValidationErrors(); - } - break; - case 'saved': - break; - case 'invalid': - break; - } - }, - - /** - * Starts hover: transition to 'highlight' state. - * - * @param event - */ - onMouseEnter: function(event) { - var that = this; - this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { - var editableEntity = that.editor.options.widget; - editableEntity.setState('highlighted', that.predicate); - event.stopPropagation(); - }); - }, - - /** - * Stops hover: back to 'candidate' state. - * - * @param event - */ - onMouseLeave: function(event) { - var that = this; - this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { - var editableEntity = that.editor.options.widget; - editableEntity.setState('candidate', that.predicate, { reason: 'mouseleave' }); - event.stopPropagation(); - }); - }, - - /** - * Clicks: transition to 'activating' stage. - * - * @param event - */ - onClick: function(event) { - var editableEntity = this.editor.options.widget; - editableEntity.setState('activating', this.predicate); - event.preventDefault(); - event.stopPropagation(); - }, - - decorate: function () { - this.$el.addClass('edit-animate-fast edit-candidate edit-editable'); - this.delegateEvents(); - }, - - undecorate: function () { - this.$el - .removeClass('edit-candidate edit-editable edit-highlighted edit-editing'); - this.undelegateEvents(); - }, - - startHighlight: function () { - // Animations. - var that = this; - setTimeout(function() { - that.$el.addClass('edit-highlighted'); - }, 0); - }, - - stopHighlight: function() { - this.$el - .removeClass('edit-highlighted'); - }, - - prepareEdit: function() { - this.$el.addClass('edit-editing'); - - // While editing, don't show *any* other editors. - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Revisit this. - $('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); - }, - - startEdit: function() { - if (this.getEditUISetting('padding')) { - this._pad(); - } - }, - - stopEdit: function() { - this.$el.removeClass('edit-highlighted edit-editing'); - - // Make the other editors show up again. - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Revisit this. - $('.edit-candidate').addClass('edit-editable'); - - if (this.getEditUISetting('padding')) { - this._unpad(); - } - }, - - /** - * Retrieves a setting of the editor-specific Edit UI integration. - * - * @see Drupal.edit.util.getEditUISetting(). - */ - getEditUISetting: function(setting) { - return Drupal.edit.util.getEditUISetting(this.editor, setting); - }, - - _pad: function () { - var self = this; - - // Add 5px padding for readability. This means we'll freeze the current - // width and *then* add 5px padding, hence ensuring the padding is added "on - // the outside". - // 1) Freeze the width (if it's not already set); don't use animations. - if (this.$el[0].style.width === "") { - this._widthAttributeIsEmpty = true; - this.$el - .addClass('edit-animate-disable-width') - .css('width', this.$el.width()) - .css('background-color', this._getBgColor(this.$el)); - } - - // 2) Add padding; use animations. - var posProp = this._getPositionProperties(this.$el); - setTimeout(function() { - // Re-enable width animations (padding changes affect width too!). - self.$el.removeClass('edit-animate-disable-width'); - - // Pad the editable. - self.$el - .css({ - 'position': 'relative', - 'top': posProp.top - 5 + 'px', - 'left': posProp.left - 5 + 'px', - 'padding-top' : posProp['padding-top'] + 5 + 'px', - 'padding-left' : posProp['padding-left'] + 5 + 'px', - 'padding-right' : posProp['padding-right'] + 5 + 'px', - 'padding-bottom': posProp['padding-bottom'] + 5 + 'px', - 'margin-bottom': posProp['margin-bottom'] - 10 + 'px' - }); - }, 0); - }, - - _unpad: function () { - var self = this; - - // 1) Set the empty width again. - if (this._widthAttributeIsEmpty) { - this.$el - .addClass('edit-animate-disable-width') - .css('width', '') - .css('background-color', ''); - } - - // 2) Remove padding; use animations (these will run simultaneously with) - // the fading out of the toolbar as its gets removed). - var posProp = this._getPositionProperties(this.$el); - setTimeout(function() { - // Re-enable width animations (padding changes affect width too!). - self.$el.removeClass('edit-animate-disable-width'); - - // Unpad the editable. - self.$el - .css({ - 'position': 'relative', - 'top': posProp.top + 5 + 'px', - 'left': posProp.left + 5 + 'px', - 'padding-top' : posProp['padding-top'] - 5 + 'px', - 'padding-left' : posProp['padding-left'] - 5 + 'px', - 'padding-right' : posProp['padding-right'] - 5 + 'px', - 'padding-bottom': posProp['padding-bottom'] - 5 + 'px', - 'margin-bottom': posProp['margin-bottom'] + 10 + 'px' - }); - }, 0); - }, - - /** - * Gets the background color of an element (or the inherited one). - * - * @param $e - * A DOM element. - */ - _getBgColor: function($e) { - var c; - - if ($e === null || $e[0].nodeName === 'HTML') { - // Fallback to white. - return 'rgb(255, 255, 255)'; - } - c = $e.css('background-color'); - // TRICKY: edge case for Firefox' "transparent" here; this is a - // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724 - if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') { - return this._getBgColor($e.parent()); - } - return c; - }, - - /** - * Gets the top and left properties of an element and convert extraneous - * values and information into numbers ready for subtraction. - * - * @param $e - * A DOM element. - */ - _getPositionProperties: function($e) { - var p, - r = {}, - props = [ - 'top', 'left', 'bottom', 'right', - 'padding-top', 'padding-left', 'padding-right', 'padding-bottom', - 'margin-bottom' - ]; - - var propCount = props.length; - for (var i = 0; i < propCount; i++) { - p = props[i]; - r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10); - } - return r; - }, - - /** - * Replaces blank or 'auto' CSS "position: " values with "0px". - * - * @param pos - * The value for a CSS position declaration. - */ - _replaceBlankPosition: function(pos) { - if (pos === 'auto' || !pos) { - pos = '0px'; - } - return pos; - }, - - /** - * Ignores hovering to/from the given closest element, but as soon as a hover - * occurs to/from *another* element, then call the given callback. - */ - _ignoreHoveringVia: function(event, closest, callback) { - if ($(event.relatedTarget).closest(closest).length > 0) { - event.stopPropagation(); - } - else { - callback(); - } - }, - - /** - * Removes validation errors' markup changes, if any. - * - * Note: this only needs to happen for type=direct, because for type=direct, - * the property DOM element itself is modified; this is not the case for - * type=form. - */ - _removeValidationErrors: function() { - if (this.editorName !== 'form') { - this.$el - .removeClass('edit-validation-error') - .next('.edit-validation-errors') - .remove(); - } - } - -}); - -})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js deleted file mode 100644 index f4b2123..0000000 --- a/core/modules/edit/js/views/toolbar-view.js +++ /dev/null @@ -1,490 +0,0 @@ -/** - * @file - * A Backbone View that provides an interactive toolbar (1 per property editor). - * - * It listens to state changes of the property editor. It also triggers state - * changes in response to user interactions with the toolbar, including saving. - */ -(function ($, _, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ToolbarView = Backbone.View.extend({ - - editor: null, - $storageWidgetEl: null, - - entity: null, - predicate : null, - editorName: null, - - _loader: null, - _loaderVisibleStart: 0, - - _id: null, - - events: { - 'click.edit button.label': 'onClickInfoLabel', - 'mouseleave.edit': 'onMouseLeave', - 'click.edit button.field-save': 'onClickSave', - 'click.edit button.field-close': 'onClickClose' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - editor: the editor object with an 'options' object that has these keys: - * * entity: the VIE entity for the property. - * * property: the predicate of the property. - * * editorName: the editor name. - * * element: the jQuery-wrapped editor DOM element - * - $storageWidgetEl: the DOM element on which the Create Storage widget is - * initialized. - */ - initialize: function(options) { - this.editor = options.editor; - this.$storageWidgetEl = options.$storageWidgetEl; - - this.entity = this.editor.options.entity; - this.predicate = this.editor.options.property; - this.editorName = this.editor.options.editorName; - - this._loader = null; - this._loaderVisibleStart = 0; - - // Generate a DOM-compatible ID for the toolbar DOM element. - this._id = Drupal.edit.util.calcPropertyID(this.entity, this.predicate).replace(/\//g, '_'); - }, - - /** - * Listens to editor state changes. - */ - stateChange: function(from, to) { - switch (to) { - case 'inactive': - if (from) { - this.remove(); - if (this.editorName !== 'form') { - Backbone.syncDirectCleanUp(); - } - } - break; - case 'candidate': - if (from === 'inactive') { - this.render(); - } - else { - if (this.editorName !== 'form') { - Backbone.syncDirectCleanUp(); - } - // Remove all toolgroups; they're no longer necessary. - this.$el - .removeClass('edit-highlighted edit-editing') - .find('.edit-toolbar .edit-toolgroup').remove(); - if (from !== 'highlighted' && this.getEditUISetting('padding')) { - this._unpad(); - } - } - break; - case 'highlighted': - // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title). - this.startHighlight(); - break; - case 'activating': - this.setLoadingIndicator(true); - break; - case 'active': - this.startEdit(); - this.setLoadingIndicator(false); - if (this.getEditUISetting('fullWidthToolbar')) { - this.$el.addClass('edit-toolbar-fullwidth'); - } - - if (this.getEditUISetting('padding')) { - this._pad(); - } - if (this.getEditUISetting('unifiedToolbar')) { - this.insertWYSIWYGToolGroups(); - } - break; - case 'changed': - this.$el - .find('button.save') - .addClass('blue-button') - .removeClass('gray-button'); - break; - case 'saving': - this.setLoadingIndicator(true); - this.save(); - break; - case 'saved': - this.setLoadingIndicator(false); - break; - case 'invalid': - this.setLoadingIndicator(false); - break; - } - }, - - /** - * Saves a property. - * - * This method deals with the complexity of the editor-dependent ways of - * inserting updated content and showing validation error messages. - * - * One might argue that this does not belong in a view. However, there is no - * actual "save" logic here, that lives in Backbone.sync. This is just some - * glue code, along with the logic for inserting updated content as well as - * showing validation error messages, the latter of which is certainly okay. - */ - save: function() { - var that = this; - var editor = this.editor; - var editableEntity = editor.options.widget; - var entity = editor.options.entity; - var predicate = editor.options.property; - - // Use Create.js' Storage widget to handle saving. (Uses Backbone.sync.) - this.$storageWidgetEl.createStorage('saveRemote', entity, { - editor: editor, - - // Successfully saved without validation errors. - success: function (model) { - editableEntity.setState('saved', predicate); - - // Now that the changes to this property have been saved, the saved - // attributes are now the "original" attributes. - entity._originalAttributes = entity._previousAttributes = _.clone(entity.attributes); - - // Get data necessary to rerender property before it is unavailable. - var updatedProperty = entity.get(predicate + '/rendered'); - var $propertyWrapper = editor.element.closest('.edit-field'); - var $context = $propertyWrapper.parent(); - - editableEntity.setState('candidate', predicate); - // Unset the property, because it will be parsed again from the DOM, iff - // its new value causes it to still be rendered. - entity.unset(predicate, { silent: true }); - entity.unset(predicate + '/rendered', { silent: true }); - // Trigger event to allow for proper clean-up of editor-specific views. - editor.element.trigger('destroyedPropertyEditor.edit', editor); - - // Replace the old content with the new content. - $propertyWrapper.replaceWith(updatedProperty); - Drupal.attachBehaviors($context); - }, - - // Save attempted but failed due to validation errors. - error: function (validationErrorMessages) { - editableEntity.setState('invalid', predicate); - - if (that.editorName === 'form') { - editor.$formContainer - .find('.edit-form') - .addClass('edit-validation-error') - .find('form') - .prepend(validationErrorMessages); - } - else { - var $errors = $('
    ') - .append(validationErrorMessages); - editor.element - .addClass('edit-validation-error') - .after($errors); - } - } - }); - }, - - /** - * When the user clicks the info label, nothing should happen. - * @note currently redirects the click.edit-event to the editor DOM element. - * - * @param event - */ - onClickInfoLabel: function(event) { - event.stopPropagation(); - event.preventDefault(); - // Redirects the event to the editor DOM element. - this.editor.element.trigger('click.edit'); - }, - - /** - * A mouseleave to the editor doesn't matter; a mouseleave to something else - * counts as a mouseleave on the editor itself. - * - * @param event - */ - onMouseLeave: function(event) { - var el = this.editor.element[0]; - if (event.relatedTarget != el && !$.contains(el, event.relatedTarget)) { - this.editor.element.trigger('mouseleave.edit'); - } - event.stopPropagation(); - }, - - /** - * Upon clicking "Save", trigger a custom event to save this property. - * - * @param event - */ - onClickSave: function(event) { - event.stopPropagation(); - event.preventDefault(); - this.editor.options.widget.setState('saving', this.predicate); - }, - - /** - * Upon clicking "Close", trigger a custom event to stop editing. - * - * @param event - */ - onClickClose: function(event) { - event.stopPropagation(); - event.preventDefault(); - this.editor.options.widget.setState('candidate', this.predicate, { reason: 'cancel' }); - }, - - /** - * Indicates in the 'info' toolgroup that we're waiting for a server reponse. - * - * Prevents flickering loading indicator by only showing it after 0.6 seconds - * and if it is shown, only hiding it after another 0.6 seconds. - * - * @param bool enabled - * Whether the loading indicator should be displayed or not. - */ - setLoadingIndicator: function(enabled) { - var that = this; - if (enabled) { - this._loader = setTimeout(function() { - that.addClass('info', 'loading'); - that._loaderVisibleStart = new Date().getTime(); - }, 600); - } - else { - var currentTime = new Date().getTime(); - clearTimeout(this._loader); - if (this._loaderVisibleStart) { - setTimeout(function() { - that.removeClass('info', 'loading'); - }, this._loaderVisibleStart + 600 - currentTime); - } - this._loader = null; - this._loaderVisibleStart = 0; - } - }, - - startHighlight: function() { - // We get the label to show for this property from VIE's type system. - var label = this.predicate; - var attributeDef = this.entity.get('@type').attributes.get(this.predicate); - if (attributeDef && attributeDef.metadata) { - label = attributeDef.metadata.label; - } - - this.$el - .addClass('edit-highlighted') - .find('.edit-toolbar') - // Append the "info" toolgroup into the toolbar. - .append(Drupal.theme('editToolgroup', { - classes: 'info edit-animate-only-background-and-padding', - buttons: [ - { label: label, classes: 'blank-button label' } - ] - })); - - // Animations. - var that = this; - setTimeout(function () { - that.show('info'); - }, 0); - }, - - startEdit: function() { - this.$el - .addClass('edit-editing') - .find('.edit-toolbar') - // Append the "ops" toolgroup into the toolbar. - .append(Drupal.theme('editToolgroup', { - classes: 'ops', - buttons: [ - { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' }, - { label: '' + Drupal.t('Close') + '', classes: 'field-close close gray-button' } - ] - })); - this.show('ops'); - }, - - /** - * Retrieves a setting of the editor-specific Edit UI integration. - * - * @see Drupal.edit.util.getEditUISetting(). - */ - getEditUISetting: function(setting) { - return Drupal.edit.util.getEditUISetting(this.editor, setting); - }, - - /** - * Adjusts the toolbar to accomodate padding on the PropertyEditor widget. - * - * @see PropertyEditorDecorationView._pad(). - */ - _pad: function() { - // The whole toolbar must move to the top when the property's DOM element - // is displayed inline. - if (this.editor.element.css('display') === 'inline') { - this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); - } - - // The toolbar must move to the top and the left. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '6px', left: '-5px' }); - - if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: this.editor.element.width() + 10 }); - } - }, - - /** - * Undoes the changes made by _pad(). - * - * @see PropertyEditorDecorationView._unpad(). - */ - _unpad: function() { - // Move the toolbar back to its original position. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '1px', left: '' }); - - if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: '' }); - } - }, - - insertWYSIWYGToolGroups: function() { - this.$el - .find('.edit-toolbar') - .append(Drupal.theme('editToolgroup', { - id: this.getFloatedWysiwygToolgroupId(), - classes: 'wysiwyg-floated', - buttons: [] - })) - .append(Drupal.theme('editToolgroup', { - id: this.getMainWysiwygToolgroupId(), - classes: 'wysiwyg-main', - buttons: [] - })); - - // Animate the toolgroups into visibility. - var that = this; - setTimeout(function () { - that.show('wysiwyg-floated'); - that.show('wysiwyg-main'); - }, 0); - }, - - /** - * Renders the Toolbar's markup into the DOM. - * - * Note: depending on whether the 'display' property of the $el for which a - * toolbar is being inserted into the DOM, it will be inserted differently. - */ - render: function () { - // Render toolbar. - this.setElement($(Drupal.theme('editToolbarContainer', { - id: this.getId() - }))); - - // Insert in DOM. - if (this.editor.element.css('display') === 'inline') { - this.$el.prependTo(this.editor.element.offsetParent()); - var pos = this.editor.element.position(); - this.$el.css('left', pos.left).css('top', pos.top); - } - else { - this.$el.insertBefore(this.editor.element); - } - }, - - /** - * Retrieves the ID for this toolbar's container. - * - * Only used to make sane hovering behavior possible. - * - * @return string - * A string that can be used as the ID for this toolbar's container. - */ - getId: function () { - return 'edit-toolbar-for-' + this._id; - }, - - /** - * Retrieves the ID for this toolbar's floating WYSIWYG toolgroup. - * - * Used to provide an abstraction for any WYSIWYG editor to plug in. - * - * @return string - * A string that can be used as the ID. - */ - getFloatedWysiwygToolgroupId: function () { - return 'edit-wysiwyg-floated-toolgroup-for-' + this._id; - }, - - /** - * Retrieves the ID for this toolbar's main WYSIWYG toolgroup. - * - * Used to provide an abstraction for any WYSIWYG editor to plug in. - * - * @return string - * A string that can be used as the ID. - */ - getMainWysiwygToolgroupId: function () { - return 'edit-wysiwyg-main-toolgroup-for-' + this._id; - }, - - /** - * Shows a toolgroup. - * - * @param string toolgroup - * A toolgroup name. - */ - show: function (toolgroup) { - this._find(toolgroup).removeClass('edit-animate-invisible'); - }, - - /** - * Adds classes to a toolgroup. - * - * @param string toolgroup - * A toolgroup name. - */ - addClass: function (toolgroup, classes) { - this._find(toolgroup).addClass(classes); - }, - - /** - * Removes classes from a toolgroup. - * - * @param string toolgroup - * A toolgroup name. - */ - removeClass: function (toolgroup, classes) { - this._find(toolgroup).removeClass(classes); - }, - - /** - * Finds a toolgroup. - * - * @param string toolgroup - * A toolgroup name. - */ - _find: function (toolgroup) { - return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup); - } -}); - -})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/editor/js/editor.createjs.js b/core/modules/editor/js/editor.createjs.js index de15c77..0b734fb 100644 --- a/core/modules/editor/js/editor.createjs.js +++ b/core/modules/editor/js/editor.createjs.js @@ -12,13 +12,11 @@ * - Drupal.editors.magical.onChange() * - Drupal.editors.magical.detach() */ -(function (jQuery, Drupal, drupalSettings) { +(function ($, Drupal, drupalSettings) { "use strict"; -// @todo D8: use jQuery UI Widget bridging. -// @see http://drupal.org/node/1874934#comment-7124904 -jQuery.widget('Midgard.editor', jQuery.Midgard.direct, { +Drupal.edit.editors.editor = Backbone.View.extend({ textFormat: null, textFormatHasTransformations: null, @@ -32,29 +30,28 @@ jQuery.widget('Midgard.editor', jQuery.Midgard.direct, { }, /** - * Implements jQuery.widget._init. - * - * @todo D8: Remove this. - * @see http://drupal.org/node/1874934 - */ - _init: function () {}, - - /** * Implements Create.editWidget._initialize. */ - _initialize: function () { - var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); - var metadata = Drupal.edit.metadataCache[propertyID].custom; - + initialize: function () { + var editID = this.model.get('editID'); + var metadata = Drupal.edit.metadataCache[editID].custom; this.textFormat = drupalSettings.editor.formats[metadata.format]; this.textFormatHasTransformations = metadata.formatHasTransformations; this.textEditor = Drupal.editors[this.textFormat.editor]; + + this.model.on('change:state', this.stateChange, this); }, /** - * Implements Create.editWidget.stateChange. + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. */ - stateChange: function (from, to) { + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; var that = this; switch (to) { case 'inactive': @@ -64,7 +61,7 @@ jQuery.widget('Midgard.editor', jQuery.Midgard.direct, { // Detach the text editor when entering the 'candidate' state from one // of the states where it could have been attached. if (from !== 'inactive' && from !== 'highlighted') { - this.textEditor.detach(this.element.get(0), this.textFormat); + this.textEditor.detach(this.$el.get(0), this.textFormat); } break; @@ -76,29 +73,40 @@ jQuery.widget('Midgard.editor', jQuery.Midgard.direct, { // text of this field, then we'll need to load a re-processed version of // it without the transformation filters. if (this.textFormatHasTransformations) { - var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); - this._getUntransformedText(propertyID, this.element, function (untransformedText) { + var editID = this.model.get('editID'); + this._getUntransformedText(editID, this.$el, function (untransformedText) { + // @todo update this + debugger; that.element.html(untransformedText); - that.options.activated(); + that.model.set('state', 'active'); }); } // When no transformation filters have been applied: start WYSIWYG // editing immediately! else { - this.options.activated(); + // As soon as the current state change has propagated, apply this one. + // @see http://jsfiddle.net/5MVzp/2/ vs. http://jsfiddle.net/5MVzp/3/ + _.defer(function() { + that.model.set('state', 'active'); + }); } break; case 'active': this.textEditor.attachInlineEditor( - this.element.get(0), + this.$el.get(0), this.textFormat, - this.toolbarView.getMainWysiwygToolgroupId(), - this.toolbarView.getFloatedWysiwygToolgroupId() + this.model.get('toolbarView').getMainWysiwygToolgroupId(), + this.model.get('toolbarView').getFloatedWysiwygToolgroupId() ); // Set the state to 'changed' whenever the content has changed. - this.textEditor.onChange(this.element.get(0), function (html) { - that.options.changed(html); + this.textEditor.onChange(this.$el.get(0), function (value) { + that.model.set('state', 'changed'); + + // @todo we have yet to set this value originally (before the editing + // starts) AND we have to handle the reverting aspect when editing is + // canceled, see editorStateChange(). + that.model.set('value', value); }); break; diff --git a/core/modules/editor/js/editor.js b/core/modules/editor/js/editor.js index 6483170..f07aa73 100644 --- a/core/modules/editor/js/editor.js +++ b/core/modules/editor/js/editor.js @@ -83,8 +83,12 @@ Drupal.behaviors.editor = { var $this = $(this); var activeFormatID = $this.val(); var field = behavior.findFieldForFormatSelector($this); - - Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger); + if ('activeFormatID' in settings.editor.formats) { + Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger); + } + else { + console.log('%c editor.js: The format ' + activeFormatID + ' does not have an editor.', 'background-color: red; color: white;'); + } }); }, diff --git a/core/modules/system/system.module b/core/modules/system/system.module index eee53b1..4185d43 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2108,20 +2108,6 @@ function system_library_info() { ), ); - // Create. - $libraries['create.editonly'] = array( - 'title' => 'Create.js edit-only (editing features only)', - 'website' => 'http://backbonejs.org/', - 'version' => '1.0.0-dev', - 'js' => array( - 'core/misc/create/create-editonly.js' => array('group' => JS_LIBRARY), - ), - 'dependencies' => array( - array('system', 'vie.core'), - array('system', 'jquery.ui.widget'), - ), - ); - // Cookie. $libraries['jquery.cookie'] = array( 'title' => 'Cookie', -- 1.7.10.4