From b526c36051cf8d109c3184c2bd10170bcbb99d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Sat, 20 Apr 2013 20:04:36 -0400 Subject: [PATCH] Issue #1678002-71 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 176fb1e3e5c903e67ef1999b45d39f4ba9d7ea49 Author: J. Renée Beach Date: Sat Apr 20 20:00:15 2013 -0400 Entities and Editables turn on and off now. Signed-off-by: J. Renée Beach commit 0c1b1663cadbadc369179e379487278d2dae7c51 Author: J. Renée Beach Date: Sat Apr 20 19:31:07 2013 -0400 Editor View is registering isActive changes. Signed-off-by: J. Renée Beach commit c9c9ab07acc2ae91d35648523d1092bc8e31ccf1 Author: J. Renée Beach Date: Sat Apr 20 19:08:44 2013 -0400 Building Editors Signed-off-by: J. Renée Beach commit 14b60ad4765e83b3a6ed6c88258e14e359ace8c5 Author: J. Renée Beach Date: Sat Apr 20 18:20:42 2013 -0400 Building Toolbars and Decorators. Signed-off-by: J. Renée Beach commit 4a9814af5e4e8c55e2be2e9345f4efd0dbf03d3d Author: J. Renée Beach Date: Sat Apr 20 17:42:05 2013 -0400 Editables are being created correctly now for each predicate. Next to the toolbars and editors. Signed-off-by: J. Renée Beach commit 6b5029fae764a12bd3f1001dfbefcbfa8a38e2c0 Author: J. Renée Beach Date: Sat Apr 20 00:51:50 2013 -0400 some debugging lines. Signed-off-by: J. Renée Beach commit 12fdb4d6146bdd0a5bc3422afeb2f058d0ec9db7 Author: J. Renée Beach Date: Sat Apr 20 00:51:39 2013 -0400 Basic state running through the Editable model. Signed-off-by: J. Renée Beach commit ea1811ebb59fb2ac780206c81ac03b7e0766c575 Author: J. Renée Beach Date: Sat Apr 20 00:20:12 2013 -0400 Changed 'active' to 'isActive' Signed-off-by: J. Renée Beach commit 4f1635174da153be8f9c17dcbc08d5177dfd2f69 Author: J. Renée Beach Date: Sat Apr 20 00:14:22 2013 -0400 Entity and contextual are now running off the same model. Signed-off-by: J. Renée Beach commit 3dfbf3495911d16e052ef4d423de1cdd22c3b640 Author: J. Renée Beach Date: Fri Apr 19 18:27:06 2013 -0400 Entities and Contextual links are running off the same model. Signed-off-by: J. Renée Beach commit fe544fe04c13c086877099ac043b3a1d3c53fdac Author: J. Renée Beach Date: Fri Apr 19 18:00:50 2013 -0400 Added a collection of editables and entities. Signed-off-by: J. Renée Beach commit b7c781c4a0dbcbfb5ba060f0031ac5b8efa3e3f9 Author: J. Renée Beach Date: Fri Apr 19 17:13:19 2013 -0400 Moved the annotateField function Signed-off-by: J. Renée Beach commit 4ec512207596c1fbb61cfdb948d58ad6bae992dc Author: J. Renée Beach Date: Fri Apr 19 17:07:48 2013 -0400 Added a collection of entity models. Signed-off-by: J. Renée Beach commit 8caf38a3917c519e071c5c9f5621066f036c92a7 Author: J. Renée Beach Date: Fri Apr 19 16:35:08 2013 -0400 recontexted edit.js functions. Signed-off-by: J. Renée Beach commit 0cdd137cb23bc2f018ed7676703eae20b11599d7 Author: J. Renée Beach Date: Fri Apr 19 16:07:04 2013 -0400 Moved EditAppView to Drupal.AppView Signed-off-by: J. Renée Beach commit fd2604ccaf2efef4bb1ec4d0787524cb4a7a3164 Author: J. Renée Beach Date: Fri Apr 19 16:02:43 2013 -0400 Moved EditAppModel to Drupal.AppModel Signed-off-by: J. Renée Beach commit 872ac1bd93ee210a9b2157e9869ee0fa7523cb1e Author: J. Renée Beach Date: Fri Apr 19 15:58:53 2013 -0400 Removed ModalView and PropertyEditorDecorationView Signed-off-by: J. Renée Beach commit d254b392066ed0f04c4f681d0739dc8946cdd6d5 Author: J. Renée Beach Date: Fri Apr 19 15:54:07 2013 -0400 Moved contextualLinkView into Drupal.edit Signed-off-by: J. Renée Beach commit 29c2670700aa3876602059081fa0ab46639434d9 Author: J. Renée Beach Date: Fri Apr 19 15:47:19 2013 -0400 Moved ToolbarView into Drupal.edit Signed-off-by: J. Renée Beach commit 9307ffd545732aae8c523d1a75457a87140a5f8b Author: J. Renée Beach Date: Fri Apr 19 15:44:58 2013 -0400 Moved EntityView into Drupal.edit Signed-off-by: J. Renée Beach commit 56dd61e57a891952a58a240d862de4607e0c7078 Author: J. Renée Beach Date: Fri Apr 19 15:41:28 2013 -0400 1678002-64 Signed-off-by: J. Renée Beach Signed-off-by: J. Renée Beach --- core/misc/create/create-editonly.js | 761 +-------- core/modules/edit/css/edit.css | 4 + core/modules/edit/edit.module | 8 - core/modules/edit/js/app.js | 391 ----- core/modules/edit/js/createjs/editable.js | 14 +- .../editingWidgets/drupalcontenteditablewidget.js | 135 +- .../edit/js/createjs/editingWidgets/formwidget.js | 277 +-- core/modules/edit/js/edit.js | 1768 +++++++++++++++++++- core/modules/edit/js/models/edit-app-model.js | 21 - 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 | 16 +- 14 files changed, 2005 insertions(+), 2435 deletions(-) delete mode 100644 core/modules/edit/js/app.js delete mode 100644 core/modules/edit/js/models/edit-app-model.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 index aed84a4..7bf4c2a 100644 --- a/core/misc/create/create-editonly.js +++ b/core/misc/create/create-editonly.js @@ -1,408 +1,99 @@ -// 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(); - }, +(function ($, drupalSettings) { - 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'; + Drupal.edit = Drupal.edit || {}; + Drupal.editor = Drupal.editor || {}; + // 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', + Drupal.edit.Editable = function (options) { + $.extend(this, { + editableModel: null, + entityModel: null, 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, + domService: 'edit', + predicateSelector: '*', //'.edit-field.edit-allowed' + predicateModel: null, // Loaded from VIE. + // The Create.js PropertyEditor widget configuration is not hardcoded; it + // is generated by the server. + propertyEditorWidgetsConfiguration: drupalSettings.edit.editors, // 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, + propertyEditors: {}, // 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, + decoratePropertyEditor: null + }, options); - // Deprecated. - editables: [], // Now `propertyEditors`. - editors: {}, // Now `propertyEditorWidgetsConfiguration`. - widgets: {} // Now `propertyEditorW - }, + this.initialize(); + }; + $.extend(Drupal.edit.Editable.prototype, { // Aids in consistently passing parameters to events and callbacks. _params: function(predicate, extended) { var entityParams = { - entity: this.options.model, + entity: this.model, editableEntity: this, - entityElement: this.element, - - // Deprecated. - editable: this, - element: this.element, - instance: this.options.model + entityElement: this.element }; var propertyParams = (predicate) ? { predicate: predicate, - propertyEditor: this.options.propertyEditors[predicate], - propertyElement: this.options.propertyEditors[predicate].element, + propertyEditor: this.propertyEditors[predicate], + propertyElement: this.propertyEditors[predicate].element, // Deprecated. property: predicate, - element: this.options.propertyEditors[predicate].element + element: this.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); + initialize: function () { + var that = this; + if (this.widgets) { + this.propertyEditorWidgets = _.extend(this.propertyEditorWidgets, this.widgets); } - if (this.options.editors) { - this.options.propertyEditorWidgetsConfiguration = _.extend(this.options.propertyEditorWidgetsConfiguration, this.options.editors); + if (this.editors) { + this.propertyEditorWidgetsConfiguration = _.extend(this.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({ + this.domService = this.vie.service(this.domService); + this.vie + .load({ element: this.element - }).from(this.options.domService).execute().done(function (entities) { - widget.options.model = entities[0]; + }) + .from(this.domService) + .execute() + .done(function (entities) { + that.predicateModel = entities[0]; }); + if (_.isFunction(this.decorateEditableEntity)) { + this.decorateEditableEntity(this._params()); } - 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; - } + this.predicate = this.domService.getElementPredicate(this.element); + }, - if (this.options.disabled === false && this.options.state === 'inactive') { - this.setState('candidate'); - return; + 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); } - this.options.disabled = false; + }, - if (this.options.state) { - this.setState(this.options.state); - return; - } - this.setState('candidate'); + get: function (property) { + return this.options[property]; }, // Method used for cycling between the different states of the Editable widget: @@ -463,7 +154,8 @@ this.enable(); } - this._trigger('statechange', null, this._params(predicate, { + // Huge hack. + this.options.stateChange.fire(this._params(predicate, { previous: previous, current: current, context: context @@ -488,7 +180,8 @@ editableEntity._enablePropertyEditor(jQuery(this)); }); - this._trigger('enable', null, this._params()); + // @jessebeach: Probably just a hook; not needed. + // this.options.enable.fire(this._params()); if (!this.vie.view || !this.vie.view.Collection) { return; @@ -541,7 +234,7 @@ }, this); this.options.collections = []; - this._trigger('disable', null, this._params()); + this.options.disable.fire(this._params()); }, _enablePropertyEditor: function (element) { @@ -572,18 +265,18 @@ silent: true }); - widget._trigger('changed', null, widget._params(predicate)); + widget.options.changed.fire(widget._params(predicate)); }, activating: function () { widget.setState('activating', predicate); }, activated: function () { widget.setState('active', predicate); - widget._trigger('activated', null, widget._params(predicate)); + jQuery(document).trigger('editStateChange', 'active', predicate); }, deactivated: function () { widget.setState('candidate', predicate); - widget._trigger('deactivated', null, widget._params(predicate)); + widget.options.deactivated.fire(widget._params(predicate)); } }); @@ -592,11 +285,6 @@ } 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 @@ -726,13 +414,13 @@ } } }); -})(jQuery); +})(jQuery, drupalSettings); // 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) { +(function ($, drupalSettings) { // Run JavaScript in strict mode /*global jQuery:false _:false document:false */ 'use strict'; @@ -749,11 +437,9 @@ // jQuery.widget('Namespace.MyWidget', jQuery.Create.editWidget, { // // override any properties // }); - jQuery.widget('Midgard.editWidget', { - options: { - disabled: false, - vie: null - }, + Drupal.editor.Editor = function () {}; + + jQuery.extend(Drupal.editor.Editor.prototype, { // override to enable the widget enable: function () { this.element.attr('contenteditable', 'true'); @@ -824,298 +510,13 @@ 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); +})(jQuery, drupalSettings); // 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) { +(function (jQuery, drupalSettings) { // Run JavaScript in strict mode /*global jQuery:false _:false window:false */ 'use strict'; @@ -1618,34 +1019,4 @@ 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' -}; +})(jQuery, drupalSettings); diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css index 6a5ac83..1ceea82 100644 --- a/core/modules/edit/css/edit.css +++ b/core/modules/edit/css/edit.css @@ -121,6 +121,10 @@ */ } +[data-edit-entity].edit-active { + outline: 2px dotted red; +} + diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index a6ee046..99e110c 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -74,14 +74,6 @@ 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, - // 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, // Backbone.sync implementation on top of Drupal forms. $path . '/js/backbone.drupalform.js' => $options, // VIE service. 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/createjs/editable.js b/core/modules/edit/js/createjs/editable.js index 1316023..47564e3 100644 --- a/core/modules/edit/js/createjs/editable.js +++ b/core/modules/edit/js/createjs/editable.js @@ -6,19 +6,7 @@ "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); - }, + jQuery.extend(Drupal.edit.Editable.prototype, { _propertyEditorName: function(data) { // Pick a PropertyEditor widget for a property depending on its metadata. diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js index cde6163..be05a57 100644 --- a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js +++ b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js @@ -2,82 +2,83 @@ * @file * Override of Create.js' default "base" (plain contentEditable) widget. */ -(function (jQuery, Drupal) { +(function ($, 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, { +Drupal.editor = Drupal.editor || {}; - /** - * Implements getEditUISettings() method. - */ - getEditUISettings: function() { - return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; - }, +Drupal.editor.DirectEditor = function () {}; - /** - * 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() {}, +$.extend(Drupal.editor.DirectEditor.prototype, Drupal.editor.Editor.prototpye, { - /** - * Implements Create's _initialize() method. - */ - _initialize: function() { - var that = this; + /** + * Implements getEditUISettings() method. + */ + getEditUISettings: function() { + return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; + }, - // 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); - } - }); - }, + /** + * 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() {}, - /** - * 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; + /** + * 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 index aa2dd0a..a232d76 100644 --- a/core/modules/edit/js/createjs/editingWidgets/formwidget.js +++ b/core/modules/edit/js/createjs/editingWidgets/formwidget.js @@ -6,147 +6,148 @@ "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(); +Drupal.editor = Drupal.editor || {}; + +Drupal.editor.FormEditor = function () { + this.id = null; + this.$formContainer = null; +}; + +$.extend(Drupal.editor.FormEditor.prototype, Drupal.editor.Editor.prototype, { + /** + * 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; } - 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; + // 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/edit.js b/core/modules/edit/js/edit.js index f924e7b..aafe960 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -13,29 +13,57 @@ Drupal.edit.metadataCache = Drupal.edit.metadataCache || {}; * Attach toggling behavior and in-place editing. */ Drupal.behaviors.edit = { + + views: { + contextualLinks: {}, + decorators: {}, + editables: {}, + entities: {}, + toolbars: {} + }, + + collections: { + entities: null, + editables: null + }, + + controllers: { + editables: {} + }, + 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); - - var 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')); - } + // Store a collection of entity models. + if (!this.collections.entities) { + Collection = Backbone.Collection.extend({ + model: Drupal.edit.EntityModel + }); + this.collections.entities = new Collection(); + } - return true; - } - return false; - }; + // Store a collection of editables models. + if (!this.collections.editables) { + Collection = Backbone.Collection.extend({ + model: Drupal.edit.EditableModel + }); + this.collections.editables = new Collection(); + } + + // Respond to entity model change events. + this.collections.entities + .on('change:isActive', this.onEntityActiveChange, this); + + // Respond to editable model change events. + this.collections.editables + .on('change:isActive', this.onEditableActiveChange, this); + + // Initialize the Edit app. + $('body').once('edit-init', $.proxy(this.init, this, options)); // Find all fields in the context without metadata. var fieldsToAnnotate = _.map($fields.not('.edit-allowed, .edit-disallowed'), function(el) { @@ -46,14 +74,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 +98,1682 @@ 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); } }); }); } - } -}; + }, + + 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 + }); + 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({ + uri: id + }); + that.collections.entities.add(entityModel); + + // Create a View for the entity. + that.views.entities[id] = new Drupal.edit.EntityView($.extend({ + el: this, + model: entityModel + }, options)); + + // 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 + }, 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, options) { + var that = this; + var app = Drupal.edit.app; + var model = app.model; + var activeEntity = model.get('activeEntity'); + options = options || {}; + + Drupal.edit.app.domService.findSubjectElements($context).each(function() { + var $element = $(this); + var entityId = $element.closest('[data-edit-entity]').data('edit-entity'); + + // The EditableModel stores the state of each Editable. + var editableModel = new Drupal.edit.EditableModel({ + state: 'inactive', + uri: entityId, + propertyId: null + }); + that.collections.editables.add(editableModel); + + var editable = new Drupal.edit.Editable({ + element: this, + editableModel: editableModel, + entityModel: that.collections.entities.where({uri: entityId})[0], + vie: Drupal.edit.app.vie, + domService: 'edit', + predicateSelector: '*', //'.edit-field.edit-allowed' + // The Create.js PropertyEditor widget configuration is not hardcoded; it + // is generated by the server. + propertyEditorWidgetsConfiguration: drupalSettings.edit.editors, + // Callback function for validating changes between states. Receives the previous state, new state, possibly property, and a callback + acceptStateChange: true, + propertyEditors: {}, + // 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 + }); + + var fullId = entityId + '/' + editable.predicate; + that.controllers.editables[fullId] = editable; + var editor = Drupal.edit.metadataCache[fullId]; + editableModel.set('predicate', editable.predicate); + editableModel.set('propertyId', fullId); + + var editableView = new Drupal.edit.EditableView($.extend({ + el: this, + model: editableModel, + entityModel: that.collections.entities.where({uri: entityId})[0], + uri: entityId + }, options)); + that.views.editables[fullId] = editableView; -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), - model: appModel, - entity: $editContextualLink.parents('[data-edit-entity]').attr('data-edit-entity') + // Create a new Editor. + var editorName = Drupal.edit.metadataCache[fullId].editor; + var editor = new Drupal.editor[that.getEditorType(editorName)]({ + element: this + }); + + // 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({ + el: this, + model: editableModel, + predicateModel: editable.predicateModel, + $storageWidgetEl: this.$el, + uri: entityId, + editor: editor + }); + that.views.toolbars[fullId] = toolbarView; + + var editorView = new Drupal.edit.EditorView({ + el: this, + model: editableModel, + editor: editor + }); + + // Decorate the editor's DOM element depending on its state. + var decorationView = new Drupal.edit.PropertyEditorDecorationView({ + el: this, + model: editableModel, + predicateModel: editable.predicateModel, + editor: editor, + toolbarId: toolbarView.getId(), + uri: entityId + }); + that.views.decorators[fullId] = decorationView; + + return; + + // 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 = editable.invoke('get', 'model'); + if (entityOfProperty.getSubjectUri() === activeEntity) { + editable.invoke('setState', '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; + }, + + /** + * + */ + onEntityActiveChange: function (changedModel, index, options) { + var that = this; + var uri = changedModel.get('uri'); + _.each(changedModel.collection.models, function (model, index, collection, options) { + // Don't set the state of the changed model, just the others. + if (model.get('uri') !== uri && !('isActive' in model.changed)) { + model.set({'isActive': false}); + // Turn off all the editables for this entity as well. + _.each(that.collections.editables.where({'uri': uri}), function (model) { + model.set('isActive', false); + }); + } }); - }); + }, - // 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; + /** + * + */ + onEditableActiveChange: function (changedModel, index, options) { + var id = changedModel.get('propertyId'); + _.each(changedModel.collection.models, function (model, index, collection, options) { + // Don't set the state of the changed model, just the others. + if (model.get('propertyId') !== id && !('isActive' in model.changed)) { + model.set({'isActive': false}); + } + }); + }, + + /** + * @todo This is a hack the maps the editor name to the name of the class for + * that editor type. We should just make these equivalent in the code rather + * than map them. That probably means changing some PHP, but I can't find at + * the moment where the values are set in configuration. + */ + getEditorType: function (name) { + var type = ''; + switch (name) { + case 'direct': + type = 'DirectEditor'; + break; + case 'form': + type = 'FormEditor'; + break + default: + type = 'FormEditor'; + } + return type; + }, + + defaults: { + strings: { + quickEdit: Drupal.t('Quick edit'), + stopQuickEdit: Drupal.t('Stop quick edit') + } + } }; +$.extend(Drupal.edit, { + + /** + * + */ + AppModel: Backbone.Model.extend({ + defaults: { + activeEntity: null, + highlightedEditor: null, + activeEditor: null, + // Reference to a ModalView-instance if a transition requires confirmation. + activeModal: null + } + }), + + /** + * + */ + AppView: Backbone.View.extend({ + vie: null, + domService: null, + + // Configuration for state handling. + states: [], + activeEditorStates: [], + singleEditorStates: [], + + // State. + $entityElements: null, + editables: [], + entityViews: [], + + /** + * 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); + }, + + /** + * 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 editables = this.editables; + var activeEntity = this.model.get('activeEntity'); + + // First, change the status of all PropertyEditor widgets to 'inactive'. + for (var i = 0, il = editables.length; i < il; i++) { + var editable = editables[i]; + editable.invoke('setState', 'inactive', null, {reason: 'stop'}); + // Then, change the status of PropertyEditor widgets of the currently + // active entity to 'candidate'. + var entityOfProperty = editable.invoke('get', 'model'); + // 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.) + if (entityOfProperty.getSubjectUri() === activeEntity) { + editable.invoke('setState', 'candidate'); + } + } + }, + + /** + * 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.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); + } + }), + + /** + * + */ + EntityModel: Backbone.Model.extend({ + defaults: { + isActive: false + } + }), + + /** + * + */ + EntityView: Backbone.View.extend({ + + events: {}, + + /** + * Implements Backbone Views' initialize() function. + */ + initialize: function (options) { + this.strings = this.options.strings; + this.model.on('change:isActive', this.render, this); + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function (model, value, options) { + var isActive = this.model.get('isActive'); + this.$el.toggleClass('edit-active', isActive); + + return this; + }, + + /** + * Listens to editor state changes. + */ + stateChange: function (from, to) { + switch (to) { + case 'inactive': + console.log(to); + break; + case 'candidate': + console.log(to); + break; + case 'highlighted': + console.log(to); + break; + case 'activating': + console.log(to); + break; + case 'active': + console.log(to); + break; + case 'changed': + console.log(to); + break; + case 'saving': + console.log(to); + break; + case 'saved': + console.log(to); + break; + case 'invalid': + console.log(to); + break; + } + } + }), + + /** + * + */ + EditableModel: Backbone.Model.extend({ + defaults: { + isActive: false, + isDirty: false + } + }), + + /** + * + */ + EditableView: Backbone.View.extend({ + events: {}, + initialize: function () { + this.entityModel = this.options.entityModel; + this.entityModel.on('change:isActive', this.render, this); + }, + render: function () { + var isEntityActive = this.entityModel.get('isActive'); + this.$el.toggleClass('edit-editable', isEntityActive); + } + }), + + /** + * + */ + 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.model.on('change:state', this.stateChange, this); + + this.editor = options.editor; + this.$storageWidgetEl = options.$storageWidgetEl; + // !HACK! + this.entity = options.predicateModel; + this.predicate = this.model.get('predicate'); + + // 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, '_'); + }, + + /** + * 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 () { + var $toolbar = $(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); + } + + return this; + }, + + /** + * 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) { + return false; + 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); + }, + + /** + * 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); + } + }), + + /** + * + */ + ContextualLinkView: Backbone.View.extend({ + + entity: null, + + events: { + 'click .quick-edit a': '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; + this.strings = options.strings; + + // Build the DOM elements. + this.$el + .find('li.node-edit, li.taxonomy-edit, li.comment-edit, li.custom-block-edit') + .before('
  • '); + + // Initial render. + this.render(); + + // Re-render whenever the app state's active entity changes. + this.model.on('change:isActive', 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; + + this.model.set('isActive', !this.model.get('isActive')); + + return; + + 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 isActive = this.model.get('isActive'); + this.$el.find('.quick-edit a').text((!isActive) ? this.strings.quickEdit : this.strings.stopQuickEdit); + 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); + } + }), + + /** + * + */ + 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); + } + }), + + EditorView: Backbone.View.extend({ + events: {}, + initialize: function (options) { + + this.model.on('change:isActive', this.render, this); + + this.editor = options.editor; + }, + render: function () { + var isActive = this.model.get('isActive'); + if (isActive) { + this.$el.css({'background-color': 'yellow'}); + } + else { + this.$el.css({'background-color': 'transparent'}); + } + return this; + }, + stateChange: function (model, value, options) { + + } + }), + + /** + * + */ + 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) { + + $(document) + .on('editStateChange', $.proxy(this.stateChange, this)); + + this.editor = options.editor; + this.toolbarId = options.toolbarId; + + this.predicate = options.predicate; + //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) { + this.model.set('isActive', true); + }, + + 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, drupalSettings); 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/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..d9e1c2e 100644 --- a/core/modules/editor/js/editor.createjs.js +++ b/core/modules/editor/js/editor.createjs.js @@ -12,17 +12,21 @@ * - Drupal.editors.magical.onChange() * - Drupal.editors.magical.detach() */ -(function (jQuery, Drupal, drupalSettings) { +(function ($, Drupal, drupalSettings) { "use strict"; +Drupal.editor = Drupal.editor || {}; + +Drupal.editor.TextEditor = function () { + this.textFormat = null; + this.textFormatHasTransformations = null; + this.textEditor = null; +}; + // @todo D8: use jQuery UI Widget bridging. // @see http://drupal.org/node/1874934#comment-7124904 -jQuery.widget('Midgard.editor', jQuery.Midgard.direct, { - - textFormat: null, - textFormatHasTransformations: null, - textEditor: null, +$.extend(Drupal.editor.TextEditor.prototype, Drupal.editor.Editor, { /** * Implements Create.editWidget.getEditUISettings. -- 1.7.10.4