core/misc/create/create-editonly.js | 1651 -------------------- core/modules/contextual/contextual-old.js | 243 +++ core/modules/edit/css/edit.css | 4 + core/modules/edit/edit.module | 18 +- core/modules/edit/js/app.js | 391 ----- core/modules/edit/js/backbone.drupalform.js | 2 +- core/modules/edit/js/createjs/editable.js | 30 - .../editingWidgets/drupalcontenteditablewidget.js | 83 - .../edit/js/createjs/editingWidgets/formwidget.js | 152 -- core/modules/edit/js/createjs/storage.js | 11 - core/modules/edit/js/edit.js | 721 ++++++++- core/modules/edit/js/editors/directEditor.js | 84 + core/modules/edit/js/editors/editor.js | 81 + core/modules/edit/js/editors/formEditor.js | 230 +++ core/modules/edit/js/models/edit-app-model.js | 21 - core/modules/edit/js/storage.js | 518 ++++++ core/modules/edit/js/viejs/EditService.js | 289 ---- core/modules/edit/js/views/contextuallink-view.js | 43 +- core/modules/edit/js/views/modal-view.js | 4 +- .../edit/js/views/propertyeditordecoration-view.js | 29 +- core/modules/edit/js/views/toolbar-view.js | 171 +- core/modules/editor/js/editor.createjs.js | 16 +- core/modules/system/system.module | 14 - 23 files changed, 1925 insertions(+), 2881 deletions(-) diff --git a/core/misc/create/create-editonly.js b/core/misc/create/create-editonly.js deleted file mode 100644 index aed84a4..0000000 --- a/core/misc/create/create-editonly.js +++ /dev/null @@ -1,1651 +0,0 @@ -// Create.js - On-site web editing interface -// (c) 2011-2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false window:false console:false */ - 'use strict'; - - // # Widget for adding items to a collection - jQuery.widget('Midgard.midgardCollectionAdd', { - options: { - editingWidgets: null, - collection: null, - model: null, - definition: null, - view: null, - disabled: false, - vie: null, - editableOptions: null, - templates: { - button: '' - } - }, - - _create: function () { - this.addButtons = []; - var widget = this; - if (!widget.options.collection.localStorage) { - try { - widget.options.collection.url = widget.options.model.url(); - } catch (e) { - if (window.console) { - console.log(e); - } - } - } - - widget.options.collection.on('add', function (model) { - model.primaryCollection = widget.options.collection; - widget.options.vie.entities.add(model); - model.collection = widget.options.collection; - }); - - // Re-check collection constraints - widget.options.collection.on('add remove reset', widget.checkCollectionConstraints, widget); - - widget._bindCollectionView(widget.options.view); - }, - - _bindCollectionView: function (view) { - var widget = this; - view.on('add', function (itemView) { - itemView.$el.effect('slide', function () { - widget._makeEditable(itemView); - }); - }); - }, - - _makeEditable: function (itemView) { - this.options.editableOptions.disabled = this.options.disabled; - this.options.editableOptions.model = itemView.model; - itemView.$el.midgardEditable(this.options.editableOptions); - }, - - _init: function () { - if (this.options.disabled) { - this.disable(); - return; - } - this.enable(); - }, - - hideButtons: function () { - _.each(this.addButtons, function (button) { - button.hide(); - }); - }, - - showButtons: function () { - _.each(this.addButtons, function (button) { - button.show(); - }); - }, - - checkCollectionConstraints: function () { - if (this.options.disabled) { - return; - } - - if (!this.options.view.canAdd()) { - this.hideButtons(); - return; - } - - if (!this.options.definition) { - // We have now information on the constraints applying to this collection - this.showButtons(); - return; - } - - if (!this.options.definition.max || this.options.definition.max === -1) { - // No maximum constraint - this.showButtons(); - return; - } - - if (this.options.collection.length < this.options.definition.max) { - this.showButtons(); - return; - } - // Collection is already full by its definition - this.hideButtons(); - }, - - enable: function () { - var widget = this; - - var addButton = jQuery(_.template(this.options.templates.button, { - icon: 'plus', - label: this.options.editableOptions.localize('Add', this.options.editableOptions.language) - })).button(); - addButton.addClass('midgard-create-add'); - addButton.click(function () { - widget.addItem(addButton); - }); - jQuery(widget.options.view.el).after(addButton); - - widget.addButtons.push(addButton); - widget.checkCollectionConstraints(); - }, - - disable: function () { - _.each(this.addButtons, function (button) { - button.remove(); - }); - this.addButtons = []; - }, - - _getTypeActions: function (options) { - var widget = this; - var actions = []; - _.each(this.options.definition.range, function (type) { - var nsType = widget.options.collection.vie.namespaces.uri(type); - if (!widget.options.view.canAdd(nsType)) { - return; - } - actions.push({ - name: type, - label: type, - cb: function () { - widget.options.collection.add({ - '@type': type - }, options); - }, - className: 'create-ui-btn' - }); - }); - return actions; - }, - - addItem: function (button, options) { - if (options === undefined) { - options = {}; - } - var addOptions = _.extend({}, options, { validate: false }); - - var itemData = {}; - if (this.options.definition && this.options.definition.range) { - if (this.options.definition.range.length === 1) { - // Items can be of single type, add that - itemData['@type'] = this.options.definition.range[0]; - } else { - // Ask user which type to add - jQuery('body').midgardNotifications('create', { - bindTo: button, - gravity: 'L', - body: this.options.editableOptions.localize('Choose type to add', this.options.editableOptions.language), - timeout: 0, - actions: this._getTypeActions(addOptions) - }); - return; - } - } else { - // Check the view templates for possible non-Thing type to use - var keys = _.keys(this.options.view.templates); - if (keys.length == 2) { - itemData['@type'] = keys[0]; - } - } - this.options.collection.add(itemData, addOptions); - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2011-2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false window:false console:false */ - 'use strict'; - - // # Widget for adding items anywhere inside a collection - jQuery.widget('Midgard.midgardCollectionAddBetween', jQuery.Midgard.midgardCollectionAdd, { - _bindCollectionView: function (view) { - var widget = this; - view.on('add', function (itemView) { - //itemView.el.effect('slide'); - widget._makeEditable(itemView); - widget._refreshButtons(); - }); - view.on('remove', function () { - widget._refreshButtons(); - }); - }, - - _refreshButtons: function () { - var widget = this; - window.setTimeout(function () { - widget.disable(); - widget.enable(); - }, 1); - }, - - prepareButton: function (index) { - var widget = this; - var addButton = jQuery(_.template(this.options.templates.button, { - icon: 'plus', - label: '' - })).button(); - addButton.addClass('midgard-create-add'); - addButton.click(function () { - widget.addItem(addButton, { - at: index - }); - }); - return addButton; - }, - - enable: function () { - var widget = this; - - var firstAddButton = widget.prepareButton(0); - jQuery(widget.options.view.el).prepend(firstAddButton); - widget.addButtons.push(firstAddButton); - jQuery.each(widget.options.view.entityViews, function (cid, view) { - var index = widget.options.collection.indexOf(view.model); - var addButton = widget.prepareButton(index + 1); - jQuery(view.el).append(addButton); - widget.addButtons.push(addButton); - }); - - this.checkCollectionConstraints(); - }, - - disable: function () { - var widget = this; - jQuery.each(widget.addButtons, function (idx, button) { - button.remove(); - }); - widget.addButtons = []; - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2011-2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false window:false VIE:false */ - 'use strict'; - - // Define Create's EditableEntity widget. - jQuery.widget('Midgard.midgardEditable', { - options: { - propertyEditors: {}, - collections: [], - model: null, - // the configuration (mapping and options) of property editor widgets - propertyEditorWidgetsConfiguration: { - hallo: { - widget: 'halloWidget', - options: {} - } - }, - // the available property editor widgets by data type - propertyEditorWidgets: { - 'default': 'hallo' - }, - collectionWidgets: { - 'default': 'midgardCollectionAdd' - }, - toolbarState: 'full', - vie: null, - domService: 'rdfa', - predicateSelector: '[property]', - disabled: false, - localize: function (id, language) { - return window.midgardCreate.localize(id, language); - }, - language: null, - // Current state of the Editable - state: null, - // Callback function for validating changes between states. Receives the previous state, new state, possibly property, and a callback - acceptStateChange: true, - // Callback function for listening (and reacting) to state changes. - stateChange: null, - // Callback function for decorating the full editable. Will be called on instantiation - decorateEditableEntity: null, - // Callback function for decorating a single property editor widget. Will - // be called on editing widget instantiation. - decoratePropertyEditor: null, - - // Deprecated. - editables: [], // Now `propertyEditors`. - editors: {}, // Now `propertyEditorWidgetsConfiguration`. - widgets: {} // Now `propertyEditorW - }, - - // Aids in consistently passing parameters to events and callbacks. - _params: function(predicate, extended) { - var entityParams = { - entity: this.options.model, - editableEntity: this, - entityElement: this.element, - - // Deprecated. - editable: this, - element: this.element, - instance: this.options.model - }; - var propertyParams = (predicate) ? { - predicate: predicate, - propertyEditor: this.options.propertyEditors[predicate], - propertyElement: this.options.propertyEditors[predicate].element, - - // Deprecated. - property: predicate, - element: this.options.propertyEditors[predicate].element - } : {}; - - return _.extend(entityParams, propertyParams, extended); - }, - - _create: function () { - // Backwards compatibility: - // - this.options.propertyEditorWidgets used to be this.options.widgets - // - this.options.propertyEditorWidgetsConfiguration used to be - // this.options.editors - if (this.options.widgets) { - this.options.propertyEditorWidgets = _.extend(this.options.propertyEditorWidgets, this.options.widgets); - } - if (this.options.editors) { - this.options.propertyEditorWidgetsConfiguration = _.extend(this.options.propertyEditorWidgetsConfiguration, this.options.editors); - } - - this.vie = this.options.vie; - this.domService = this.vie.service(this.options.domService); - if (!this.options.model) { - var widget = this; - this.vie.load({ - element: this.element - }).from(this.options.domService).execute().done(function (entities) { - widget.options.model = entities[0]; - }); - } - if (_.isFunction(this.options.decorateEditableEntity)) { - this.options.decorateEditableEntity(this._params()); - } - }, - - _init: function () { - // Backwards compatibility: - // - this.options.propertyEditorWidgets used to be this.options.widgets - // - this.options.propertyEditorWidgetsConfiguration used to be - // this.options.editors - if (this.options.widgets) { - this.options.propertyEditorWidgets = _.extend(this.options.propertyEditorWidgets, this.options.widgets); - } - if (this.options.editors) { - this.options.propertyEditorWidgetsConfiguration = _.extend(this.options.propertyEditorWidgetsConfiguration, this.options.editors); - } - - // Old way of setting the widget inactive - if (this.options.disabled === true) { - this.setState('inactive'); - return; - } - - if (this.options.disabled === false && this.options.state === 'inactive') { - this.setState('candidate'); - return; - } - this.options.disabled = false; - - if (this.options.state) { - this.setState(this.options.state); - return; - } - this.setState('candidate'); - }, - - // Method used for cycling between the different states of the Editable widget: - // - // * Inactive: editable is loaded but disabled - // * Candidate: editable is enabled but not activated - // * Highlight: user is hovering over the editable (not set by Editable widget directly) - // * Activating: an editor widget is being activated for user to edit with it (skipped for editors that activate instantly) - // * Active: user is actually editing something inside the editable - // * Changed: user has made changes to the editable - // * Invalid: the contents of the editable have validation errors - // - // In situations where state changes are triggered for a particular property editor, the `predicate` - // argument will provide the name of that property. - // - // State changes may carry optional context information in a JavaScript object. The payload of these context objects is not - // standardized, and is meant to be set and used by the application controller - // - // The callback parameter is optional and will be invoked after a state change has been accepted (after the 'statechange' - // event) or rejected. - setState: function (state, predicate, context, callback) { - var previous = this.options.state; - var current = state; - if (current === previous) { - return; - } - - if (this.options.acceptStateChange === undefined || !_.isFunction(this.options.acceptStateChange)) { - // Skip state transition validation - this._doSetState(previous, current, predicate, context); - if (_.isFunction(callback)) { - callback(true); - } - return; - } - - var widget = this; - this.options.acceptStateChange(previous, current, predicate, context, function (accepted) { - if (accepted) { - widget._doSetState(previous, current, predicate, context); - } - if (_.isFunction(callback)) { - callback(accepted); - } - return; - }); - }, - - getState: function () { - return this.options.state; - }, - - _doSetState: function (previous, current, predicate, context) { - this.options.state = current; - if (current === 'inactive') { - this.disable(); - } else if ((previous === null || previous === 'inactive') && current !== 'inactive') { - this.enable(); - } - - this._trigger('statechange', null, this._params(predicate, { - previous: previous, - current: current, - context: context - })); - }, - - findEditablePredicateElements: function (callback) { - this.domService.findPredicateElements(this.options.model.id, jQuery(this.options.predicateSelector, this.element), false).each(callback); - }, - - getElementPredicate: function (element) { - return this.domService.getElementPredicate(element); - }, - - enable: function () { - var editableEntity = this; - if (!this.options.model) { - return; - } - - this.findEditablePredicateElements(function () { - editableEntity._enablePropertyEditor(jQuery(this)); - }); - - this._trigger('enable', null, this._params()); - - if (!this.vie.view || !this.vie.view.Collection) { - return; - } - - _.each(this.domService.views, function (view) { - if (view instanceof this.vie.view.Collection && this.options.model === view.owner) { - var predicate = view.collection.predicate; - var editableOptions = _.clone(this.options); - editableOptions.state = null; - var collection = this.enableCollection({ - model: this.options.model, - collection: view.collection, - property: predicate, - definition: this.getAttributeDefinition(predicate), - view: view, - element: view.el, - vie: editableEntity.vie, - editableOptions: editableOptions - }); - editableEntity.options.collections.push(collection); - } - }, this); - }, - - disable: function () { - _.each(this.options.propertyEditors, function (editable) { - this.disablePropertyEditor({ - widget: this, - editable: editable, - entity: this.options.model, - element: editable.element - }); - }, this); - this.options.propertyEditors = {}; - - // Deprecated. - this.options.editables = []; - - _.each(this.options.collections, function (collectionWidget) { - var editableOptions = _.clone(this.options); - editableOptions.state = 'inactive'; - this.disableCollection({ - widget: this, - model: this.options.model, - element: collectionWidget, - vie: this.vie, - editableOptions: editableOptions - }); - }, this); - this.options.collections = []; - - this._trigger('disable', null, this._params()); - }, - - _enablePropertyEditor: function (element) { - var widget = this; - var predicate = this.getElementPredicate(element); - if (!predicate) { - return true; - } - if (this.options.model.get(predicate) instanceof Array) { - // For now we don't deal with multivalued properties in the editable - return true; - } - - var propertyElement = this.enablePropertyEditor({ - widget: this, - element: element, - entity: this.options.model, - property: predicate, - vie: this.vie, - decorate: this.options.decoratePropertyEditor, - decorateParams: _.bind(this._params, this), - changed: function (content) { - widget.setState('changed', predicate); - - var changedProperties = {}; - changedProperties[predicate] = content; - widget.options.model.set(changedProperties, { - silent: true - }); - - widget._trigger('changed', null, widget._params(predicate)); - }, - activating: function () { - widget.setState('activating', predicate); - }, - activated: function () { - widget.setState('active', predicate); - widget._trigger('activated', null, widget._params(predicate)); - }, - deactivated: function () { - widget.setState('candidate', predicate); - widget._trigger('deactivated', null, widget._params(predicate)); - } - }); - - if (!propertyElement) { - return; - } - var widgetType = propertyElement.data('createWidgetName'); - this.options.propertyEditors[predicate] = propertyElement.data('Midgard-' + widgetType); - - // Deprecated. - this.options.editables.push(propertyElement); - - this._trigger('enableproperty', null, this._params(predicate)); - }, - - // returns the name of the property editor widget to use for the given property - _propertyEditorName: function (data) { - if (this.options.propertyEditorWidgets[data.property] !== undefined) { - // Property editor widget configuration set for specific RDF predicate - return this.options.propertyEditorWidgets[data.property]; - } - - // Load the property editor widget configuration for the data type - var propertyType = 'default'; - var attributeDefinition = this.getAttributeDefinition(data.property); - if (attributeDefinition) { - propertyType = attributeDefinition.range[0]; - } - if (this.options.propertyEditorWidgets[propertyType] !== undefined) { - return this.options.propertyEditorWidgets[propertyType]; - } - return this.options.propertyEditorWidgets['default']; - }, - - _propertyEditorWidget: function (editor) { - return this.options.propertyEditorWidgetsConfiguration[editor].widget; - }, - - _propertyEditorOptions: function (editor) { - return this.options.propertyEditorWidgetsConfiguration[editor].options; - }, - - getAttributeDefinition: function (property) { - var type = this.options.model.get('@type'); - if (!type) { - return; - } - if (!type.attributes) { - return; - } - return type.attributes.get(property); - }, - - // Deprecated. - enableEditor: function (data) { - return this.enablePropertyEditor(data); - }, - - enablePropertyEditor: function (data) { - var editorName = this._propertyEditorName(data); - if (editorName === null) { - return; - } - - var editorWidget = this._propertyEditorWidget(editorName); - - data.editorOptions = this._propertyEditorOptions(editorName); - data.toolbarState = this.options.toolbarState; - data.disabled = false; - // Pass metadata that could be useful for some implementations. - data.editorName = editorName; - data.editorWidget = editorWidget; - - if (typeof jQuery(data.element)[editorWidget] !== 'function') { - throw new Error(editorWidget + ' widget is not available'); - } - - jQuery(data.element)[editorWidget](data); - jQuery(data.element).data('createWidgetName', editorWidget); - return jQuery(data.element); - }, - - // Deprecated. - disableEditor: function (data) { - return this.disablePropertyEditor(data); - }, - - disablePropertyEditor: function (data) { - data.element[data.editable.widgetName]({ - disabled: true - }); - jQuery(data.element).removeClass('ui-state-disabled'); - - if (data.element.is(':focus')) { - data.element.blur(); - } - }, - - collectionWidgetName: function (data) { - if (this.options.collectionWidgets[data.property] !== undefined) { - // Widget configuration set for specific RDF predicate - return this.options.collectionWidgets[data.property]; - } - - var propertyType = 'default'; - var attributeDefinition = this.getAttributeDefinition(data.property); - if (attributeDefinition) { - propertyType = attributeDefinition.range[0]; - } - if (this.options.collectionWidgets[propertyType] !== undefined) { - return this.options.collectionWidgets[propertyType]; - } - return this.options.collectionWidgets['default']; - }, - - enableCollection: function (data) { - var widgetName = this.collectionWidgetName(data); - if (widgetName === null) { - return; - } - data.disabled = false; - if (typeof jQuery(data.element)[widgetName] !== 'function') { - throw new Error(widgetName + ' widget is not available'); - } - jQuery(data.element)[widgetName](data); - jQuery(data.element).data('createCollectionWidgetName', widgetName); - return jQuery(data.element); - }, - - disableCollection: function (data) { - var widgetName = jQuery(data.element).data('createCollectionWidgetName'); - if (widgetName === null) { - return; - } - data.disabled = true; - if (widgetName) { - // only if there has been an editing widget registered - jQuery(data.element)[widgetName](data); - jQuery(data.element).removeClass('ui-state-disabled'); - } - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Tobias Herrmann, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false */ - 'use strict'; - - // # Base property editor widget - // - // This property editor widget provides a very simplistic `contentEditable` - // property editor that can be used as standalone, but should more usually be - // used as the base class for other property editor widgets. - // This property editor widget is only useful for textual properties! - // - // Subclassing this base property editor widget is easy: - // - // jQuery.widget('Namespace.MyWidget', jQuery.Create.editWidget, { - // // override any properties - // }); - jQuery.widget('Midgard.editWidget', { - options: { - disabled: false, - vie: null - }, - // override to enable the widget - enable: function () { - this.element.attr('contenteditable', 'true'); - }, - // override to disable the widget - disable: function (disable) { - this.element.attr('contenteditable', 'false'); - }, - // called by the jQuery UI plugin factory when creating the property editor - // widget instance - _create: function () { - this._registerWidget(); - this._initialize(); - - if (_.isFunction(this.options.decorate) && _.isFunction(this.options.decorateParams)) { - // TRICKY: we can't use this.options.decorateParams()'s 'propertyName' - // parameter just yet, because it will only be available after this - // object has been created, but we're currently in the constructor! - // Hence we have to duplicate part of its logic here. - this.options.decorate(this.options.decorateParams(null, { - propertyName: this.options.property, - propertyEditor: this, - propertyElement: this.element, - // Deprecated. - editor: this, - predicate: this.options.property, - element: this.element - })); - } - }, - // called every time the property editor widget is called - _init: function () { - if (this.options.disabled) { - this.disable(); - return; - } - this.enable(); - }, - // override this function to initialize the property editor widget functions - _initialize: function () { - var self = this; - this.element.on('focus', function () { - if (self.options.disabled) { - return; - } - self.options.activated(); - }); - this.element.on('blur', function () { - if (self.options.disabled) { - return; - } - self.options.deactivated(); - }); - var before = this.element.html(); - this.element.on('keyup paste', function (event) { - if (self.options.disabled) { - return; - } - var current = jQuery(this).html(); - if (before !== current) { - before = current; - self.options.changed(current); - } - }); - }, - // used to register the property editor widget name with the DOM element - _registerWidget: function () { - this.element.data("createWidgetName", this.widgetName); - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Tobias Herrmann, IKS Consortium -// (c) 2011 Rene Kapusta, Evo42 -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false Aloha:false */ - 'use strict'; - - // # Aloha editing widget - // - // This widget allows editing textual contents using the - // [Aloha](http://aloha-editor.org) rich text editor. - // - // Due to licensing incompatibilities, Aloha Editor needs to be installed - // and configured separately. - jQuery.widget('Midgard.alohaWidget', jQuery.Midgard.editWidget, { - _initialize: function () {}, - enable: function () { - var options = this.options; - var editable; - var currentElement = Aloha.jQuery(options.element.get(0)).aloha(); - _.each(Aloha.editables, function (aloha) { - // Find the actual editable instance so we can hook to the events - // correctly - if (aloha.obj.get(0) === currentElement.get(0)) { - editable = aloha; - } - }); - if (!editable) { - return; - } - editable.vieEntity = options.entity; - - // Subscribe to activation and deactivation events - Aloha.bind('aloha-editable-activated', function (event, data) { - if (data.editable !== editable) { - return; - } - options.activated(); - }); - Aloha.bind('aloha-editable-deactivated', function (event, data) { - if (data.editable !== editable) { - return; - } - options.deactivated(); - }); - - Aloha.bind('aloha-smart-content-changed', function (event, data) { - if (data.editable !== editable) { - return; - } - if (!data.editable.isModified()) { - return true; - } - options.changed(data.editable.getContents()); - data.editable.setUnmodified(); - }); - this.options.disabled = false; - }, - disable: function () { - Aloha.jQuery(this.options.element.get(0)).mahalo(); - this.options.disabled = true; - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Tobias Herrmann, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false CKEDITOR:false */ - 'use strict'; - - // # CKEditor editing widget - // - // This widget allows editing textual content areas with the - // [CKEditor](http://ckeditor.com/) rich text editor. - jQuery.widget('Midgard.ckeditorWidget', jQuery.Midgard.editWidget, { - enable: function () { - this.element.attr('contentEditable', 'true'); - this.editor = CKEDITOR.inline(this.element.get(0)); - this.options.disabled = false; - - var widget = this; - this.editor.on('focus', function () { - widget.options.activated(); - }); - this.editor.on('blur', function () { - widget.options.activated(); - widget.options.changed(widget.editor.getData()); - }); - this.editor.on('key', function () { - widget.options.changed(widget.editor.getData()); - }); - this.editor.on('paste', function () { - widget.options.changed(widget.editor.getData()); - }); - this.editor.on('afterCommandExec', function () { - widget.options.changed(widget.editor.getData()); - }); - this.editor.on('configLoaded', function() { - jQuery.each(widget.options.editorOptions, function(optionName, option) { - widget.editor.config[optionName] = option; - }); - }); - }, - - disable: function () { - if (!this.editor) { - return; - } - this.element.attr('contentEditable', 'false'); - this.editor.destroy(); - this.editor = null; - }, - - _initialize: function () { - CKEDITOR.disableAutoInline = true; - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Tobias Herrmann, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false */ - 'use strict'; - - // # Hallo editing widget - // - // This widget allows editing textual content areas with the - // [Hallo](http://hallojs.org) rich text editor. - jQuery.widget('Midgard.halloWidget', jQuery.Midgard.editWidget, { - options: { - editorOptions: {}, - disabled: true, - toolbarState: 'full', - vie: null, - entity: null - }, - enable: function () { - jQuery(this.element).hallo({ - editable: true - }); - this.options.disabled = false; - }, - - disable: function () { - jQuery(this.element).hallo({ - editable: false - }); - this.options.disabled = true; - }, - - _initialize: function () { - jQuery(this.element).hallo(this.getHalloOptions()); - var self = this; - jQuery(this.element).on('halloactivated', function (event, data) { - self.options.activated(); - }); - jQuery(this.element).on('hallodeactivated', function (event, data) { - self.options.deactivated(); - }); - jQuery(this.element).on('hallomodified', function (event, data) { - self.options.changed(data.content); - data.editable.setUnmodified(); - }); - - jQuery(document).on('midgardtoolbarstatechange', function(event, data) { - // Switch between Hallo configurations when toolbar state changes - if (data.display === self.options.toolbarState) { - return; - } - self.options.toolbarState = data.display; - if (!self.element.data('IKS-hallo')) { - // Hallo not yet instantiated - return; - } - var newOptions = self.getHalloOptions(); - self.element.hallo('changeToolbar', newOptions.parentElement, newOptions.toolbar, true); - }); - }, - - getHalloOptions: function() { - var defaults = { - plugins: { - halloformat: {}, - halloblock: {}, - hallolists: {}, - hallolink: {}, - halloimage: { - entity: this.options.entity - } - }, - buttonCssClass: 'create-ui-btn-small', - placeholder: '[' + this.options.property + ']' - }; - - if (typeof this.element.annotate === 'function' && this.options.vie.services.stanbol) { - // Enable Hallo Annotate plugin by default if user has annotate.js - // loaded and VIE has Stanbol enabled - defaults.plugins.halloannotate = { - vie: this.options.vie - }; - } - - if (this.options.toolbarState === 'full') { - // Use fixed toolbar in the Create tools area - defaults.parentElement = jQuery('.create-ui-toolbar-dynamictoolarea .create-ui-tool-freearea'); - defaults.toolbar = 'halloToolbarFixed'; - } else { - // Tools area minimized, use floating toolbar - defaults.parentElement = 'body'; - defaults.toolbar = 'halloToolbarContextual'; - } - return _.extend(defaults, this.options.editorOptions); - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false document:false */ - 'use strict'; - - // # Redactor editing widget - // - // This widget allows editing textual content areas with the - // [Redactor](http://redactorjs.com/) rich text editor. - jQuery.widget('Midgard.redactorWidget', jQuery.Midgard.editWidget, { - editor: null, - - options: { - editorOptions: {}, - disabled: true - }, - - enable: function () { - jQuery(this.element).redactor(this.getRedactorOptions()); - this.options.disabled = false; - }, - - disable: function () { - jQuery(this.element).destroyEditor(); - this.options.disabled = true; - }, - - _initialize: function () { - var self = this; - jQuery(this.element).on('focus', function (event) { - self.options.activated(); - }); - /* - jQuery(this.element).on('blur', function (event) { - self.options.deactivated(); - }); - */ - }, - - getRedactorOptions: function () { - var self = this; - var overrides = { - keyupCallback: function (obj, event) { - self.options.changed(jQuery(self.element).getCode()); - }, - execCommandCallback: function (obj, command) { - self.options.changed(jQuery(self.element).getCode()); - } - }; - - return _.extend(self.options.editorOptions, overrides); - } - }); -})(jQuery); -// Create.js - On-site web editing interface -// (c) 2011-2012 Henri Bergius, IKS Consortium -// Create may be freely distributed under the MIT license. -// For all details and documentation: -// http://createjs.org/ -(function (jQuery, undefined) { - // Run JavaScript in strict mode - /*global jQuery:false _:false window:false */ - 'use strict'; - - jQuery.widget('Midgard.midgardStorage', { - saveEnabled: true, - options: { - // Whether to use localstorage - localStorage: false, - removeLocalstorageOnIgnore: true, - // VIE instance to use for storage handling - vie: null, - // URL callback for Backbone.sync - url: '', - // Whether to enable automatic saving - autoSave: false, - // How often to autosave in milliseconds - autoSaveInterval: 5000, - // Whether to save entities that are referenced by entities - // we're saving to the server. - saveReferencedNew: false, - saveReferencedChanged: false, - // Namespace used for events from midgardEditable-derived widget - editableNs: 'midgardeditable', - // CSS selector for the Edit button, leave to null to not bind - // notifications to any element - editSelector: '#midgardcreate-edit a', - localize: function (id, language) { - return window.midgardCreate.localize(id, language); - }, - language: null - }, - - _create: function () { - var widget = this; - this.changedModels = []; - - if (window.localStorage) { - this.options.localStorage = true; - } - - this.vie = this.options.vie; - - this.vie.entities.on('add', function (model) { - // Add the back-end URL used by Backbone.sync - model.url = widget.options.url; - model.toJSON = model.toJSONLD; - }); - - widget._bindEditables(); - if (widget.options.autoSave) { - widget._autoSave(); - } - }, - - _autoSave: function () { - var widget = this; - widget.saveEnabled = true; - - var doAutoSave = function () { - if (!widget.saveEnabled) { - return; - } - - if (widget.changedModels.length === 0) { - return; - } - - widget.saveRemoteAll({ - // We make autosaves silent so that potential changes from server - // don't disrupt user while writing. - silent: true - }); - }; - - var timeout = window.setInterval(doAutoSave, widget.options.autoSaveInterval); - - this.element.on('startPreventSave', function () { - if (timeout) { - window.clearInterval(timeout); - timeout = null; - } - widget.disableAutoSave(); - }); - this.element.on('stopPreventSave', function () { - if (!timeout) { - timeout = window.setInterval(doAutoSave, widget.options.autoSaveInterval); - } - widget.enableAutoSave(); - }); - - }, - - enableAutoSave: function () { - this.saveEnabled = true; - }, - - disableAutoSave: function () { - this.saveEnabled = false; - }, - - _bindEditables: function () { - var widget = this; - this.restorables = []; - var restorer; - - widget.element.on(widget.options.editableNs + 'changed', function (event, options) { - if (_.indexOf(widget.changedModels, options.instance) === -1) { - widget.changedModels.push(options.instance); - } - widget._saveLocal(options.instance); - }); - - widget.element.on(widget.options.editableNs + 'disable', function (event, options) { - widget.revertChanges(options.instance); - }); - - widget.element.on(widget.options.editableNs + 'enable', function (event, options) { - if (!options.instance._originalAttributes) { - options.instance._originalAttributes = _.clone(options.instance.attributes); - } - - if (!options.instance.isNew() && widget._checkLocal(options.instance)) { - // We have locally-stored modifications, user needs to be asked - widget.restorables.push(options.instance); - } - - /*_.each(options.instance.attributes, function (attributeValue, property) { - if (attributeValue instanceof widget.vie.Collection) { - widget._readLocalReferences(options.instance, property, attributeValue); - } - });*/ - }); - - widget.element.on('midgardcreatestatechange', function (event, options) { - if (options.state === 'browse' || widget.restorables.length === 0) { - widget.restorables = []; - if (restorer) { - restorer.close(); - } - return; - } - restorer = widget.checkRestore(); - }); - - widget.element.on('midgardstorageloaded', function (event, options) { - if (_.indexOf(widget.changedModels, options.instance) === -1) { - widget.changedModels.push(options.instance); - } - }); - }, - - checkRestore: function () { - var widget = this; - if (widget.restorables.length === 0) { - return; - } - - var message; - var restorer; - if (widget.restorables.length === 1) { - message = _.template(widget.options.localize('localModification', widget.options.language), { - label: widget.restorables[0].getSubjectUri() - }); - } else { - message = _.template(widget.options.localize('localModifications', widget.options.language), { - number: widget.restorables.length - }); - } - - var doRestore = function (event, notification) { - widget.restoreLocalAll(); - restorer.close(); - }; - - var doIgnore = function (event, notification) { - widget.ignoreLocal(); - restorer.close(); - }; - - restorer = jQuery('body').midgardNotifications('create', { - bindTo: widget.options.editSelector, - gravity: 'TR', - body: message, - timeout: 0, - actions: [ - { - name: 'restore', - label: widget.options.localize('Restore', widget.options.language), - cb: doRestore, - className: 'create-ui-btn' - }, - { - name: 'ignore', - label: widget.options.localize('Ignore', widget.options.language), - cb: doIgnore, - className: 'create-ui-btn' - } - ], - callbacks: { - beforeShow: function () { - if (!window.Mousetrap) { - return; - } - window.Mousetrap.bind(['command+shift+r', 'ctrl+shift+r'], function (event) { - event.preventDefault(); - doRestore(); - }); - window.Mousetrap.bind(['command+shift+i', 'ctrl+shift+i'], function (event) { - event.preventDefault(); - doIgnore(); - }); - }, - afterClose: function () { - if (!window.Mousetrap) { - return; - } - window.Mousetrap.unbind(['command+shift+r', 'ctrl+shift+r']); - window.Mousetrap.unbind(['command+shift+i', 'ctrl+shift+i']); - } - } - }); - return restorer; - }, - - restoreLocalAll: function () { - _.each(this.restorables, function (instance) { - this.readLocal(instance); - }, this); - this.restorables = []; - }, - - ignoreLocal: function () { - if (this.options.removeLocalstorageOnIgnore) { - _.each(this.restorables, function (instance) { - this._removeLocal(instance); - }, this); - } - this.restorables = []; - }, - - saveReferences: function (model) { - _.each(model.attributes, function (value, property) { - if (!value || !value.isCollection) { - return; - } - - value.each(function (referencedModel) { - if (this.changedModels.indexOf(referencedModel) !== -1) { - // The referenced model is already in the save queue - return; - } - - if (referencedModel.isNew() && this.options.saveReferencedNew) { - return referencedModel.save(); - } - - if (referencedModel.hasChanged() && this.options.saveReferencedChanged) { - return referencedModel.save(); - } - }, this); - }, this); - }, - - saveRemote: function (model, options) { - // Optionally handle entities referenced in this model first - this.saveReferences(model); - - this._trigger('saveentity', null, { - entity: model, - options: options - }); - - var widget = this; - model.save(null, _.extend({}, options, { - success: function (m, response) { - // From now on we're going with the values we have on server - model._originalAttributes = _.clone(model.attributes); - widget._removeLocal(model); - window.setTimeout(function () { - // Remove the model from the list of changed models after saving - widget.changedModels.splice(widget.changedModels.indexOf(model), 1); - }, 0); - if (_.isFunction(options.success)) { - options.success(m, response); - } - widget._trigger('savedentity', null, { - entity: model, - options: options - }); - }, - error: function (m, response) { - if (_.isFunction(options.error)) { - options.error(m, response); - } - } - })); - }, - - saveRemoteAll: function (options) { - var widget = this; - if (widget.changedModels.length === 0) { - return; - } - - widget._trigger('save', null, { - entities: widget.changedModels, - options: options, - // Deprecated - models: widget.changedModels - }); - - var notification_msg; - var needed = widget.changedModels.length; - if (needed > 1) { - notification_msg = _.template(widget.options.localize('saveSuccessMultiple', widget.options.language), { - number: needed - }); - } else { - notification_msg = _.template(widget.options.localize('saveSuccess', widget.options.language), { - label: widget.changedModels[0].getSubjectUri() - }); - } - - widget.disableAutoSave(); - _.each(widget.changedModels, function (model) { - this.saveRemote(model, { - success: function (m, response) { - needed--; - if (needed <= 0) { - // All models were happily saved - widget._trigger('saved', null, { - options: options - }); - if (options && _.isFunction(options.success)) { - options.success(m, response); - } - jQuery('body').midgardNotifications('create', { - body: notification_msg - }); - widget.enableAutoSave(); - } - }, - error: function (m, err) { - if (options && _.isFunction(options.error)) { - options.error(m, err); - } - jQuery('body').midgardNotifications('create', { - body: _.template(widget.options.localize('saveError', widget.options.language), { - error: err.responseText || '' - }), - timeout: 0 - }); - - widget._trigger('error', null, { - instance: model - }); - } - }); - }, this); - }, - - _saveLocal: function (model) { - if (!this.options.localStorage) { - return; - } - - if (model.isNew()) { - // Anonymous object, save as refs instead - if (!model.primaryCollection) { - return; - } - return this._saveLocalReferences(model.primaryCollection.subject, model.primaryCollection.predicate, model); - } - window.localStorage.setItem(model.getSubjectUri(), JSON.stringify(model.toJSONLD())); - }, - - _getReferenceId: function (model, property) { - return model.id + ':' + property; - }, - - _saveLocalReferences: function (subject, predicate, model) { - if (!this.options.localStorage) { - return; - } - - if (!subject || !predicate) { - return; - } - - var widget = this; - var identifier = subject + ':' + predicate; - var json = model.toJSONLD(); - if (window.localStorage.getItem(identifier)) { - var referenceList = JSON.parse(window.localStorage.getItem(identifier)); - var index = _.pluck(referenceList, '@').indexOf(json['@']); - if (index !== -1) { - referenceList[index] = json; - } else { - referenceList.push(json); - } - window.localStorage.setItem(identifier, JSON.stringify(referenceList)); - return; - } - window.localStorage.setItem(identifier, JSON.stringify([json])); - }, - - _checkLocal: function (model) { - if (!this.options.localStorage) { - return false; - } - - var local = window.localStorage.getItem(model.getSubjectUri()); - if (!local) { - return false; - } - - return true; - }, - - hasLocal: function (model) { - if (!this.options.localStorage) { - return false; - } - - if (!window.localStorage.getItem(model.getSubjectUri())) { - return false; - } - return true; - }, - - readLocal: function (model) { - if (!this.options.localStorage) { - return; - } - - var local = window.localStorage.getItem(model.getSubjectUri()); - if (!local) { - return; - } - if (!model._originalAttributes) { - model._originalAttributes = _.clone(model.attributes); - } - var parsed = JSON.parse(local); - var entity = this.vie.entities.addOrUpdate(parsed, { - overrideAttributes: true - }); - - this._trigger('loaded', null, { - instance: entity - }); - }, - - _readLocalReferences: function (model, property, collection) { - if (!this.options.localStorage) { - return; - } - - var identifier = this._getReferenceId(model, property); - var local = window.localStorage.getItem(identifier); - if (!local) { - return; - } - collection.add(JSON.parse(local)); - }, - - revertChanges: function (model) { - var widget = this; - - // Remove unsaved collection members - if (!model) { return; } - _.each(model.attributes, function (attributeValue, property) { - if (attributeValue instanceof widget.vie.Collection) { - var removables = []; - attributeValue.forEach(function (model) { - if (model.isNew()) { - removables.push(model); - } - }); - attributeValue.remove(removables); - } - }); - - // Restore original object properties - if (!model.changedAttributes()) { - if (model._originalAttributes) { - model.set(model._originalAttributes); - } - return; - } - - model.set(model.previousAttributes()); - }, - - _removeLocal: function (model) { - if (!this.options.localStorage) { - return; - } - - window.localStorage.removeItem(model.getSubjectUri()); - } - }); -})(jQuery); -if (window.midgardCreate === undefined) { - window.midgardCreate = {}; -} -if (window.midgardCreate.locale === undefined) { - window.midgardCreate.locale = {}; -} - -window.midgardCreate.locale.en = { - // Session-state buttons for the main toolbar - 'Save': 'Save', - 'Saving': 'Saving', - 'Cancel': 'Cancel', - 'Edit': 'Edit', - // Storage status messages - 'localModification': 'Item "<%= label %>" has local modifications', - 'localModifications': '<%= number %> items on this page have local modifications', - 'Restore': 'Restore', - 'Ignore': 'Ignore', - 'saveSuccess': 'Item "<%= label %>" saved successfully', - 'saveSuccessMultiple': '<%= number %> items saved successfully', - 'saveError': 'Error occurred while saving
<%= error %>', - // Tagging - 'Item tags': 'Item tags', - 'Suggested tags': 'Suggested tags', - 'Tags': 'Tags', - 'add a tag': 'add a tag', - // Collection widgets - 'Add': 'Add', - 'Choose type to add': 'Choose type to add' -}; diff --git a/core/modules/contextual/contextual-old.js b/core/modules/contextual/contextual-old.js new file mode 100644 index 0000000..ea51af2 --- /dev/null +++ b/core/modules/contextual/contextual-old.js @@ -0,0 +1,243 @@ +/** + * @file + * Attaches behaviors for the Contextual module. + */ + +(function ($, Drupal) { + +"use strict"; + +var contextuals = []; + +/** + * Attaches outline behavior for regions associated with contextual links. + */ +Drupal.behaviors.contextual = { + attach: function (context) { + var that = this; + $('ul.contextual-links', context).once('contextual', function () { + var $this = $(this); + var contextual = new Drupal.contextual($this, $this.closest('.contextual-region')); + contextuals.push(contextual); + $this.data('drupal-contextual', contextual); + that._adjustIfNestedAndOverlapping(this); + }); + + // Bind to edit mode changes. + $('body').once('contextual', function () { + $(document).on('drupalEditModeChanged.contextual', toggleEditMode); + }); + }, + + /** + * Determines if a contextual link is nested & overlapping, if so: adjusts it. + * + * This only deals with two levels of nesting; deeper levels are not touched. + * + * @param DOM contextualLink + * A contextual link DOM element. + */ + _adjustIfNestedAndOverlapping: function (contextualLink) { + var $contextuals = $(contextualLink) + .parents('.contextual-region').eq(-1) + .find('.contextual'); + + // Early-return when there's no nesting. + if ($contextuals.length === 1) { + return; + } + + // If the two contextual links overlap, then we move the second one. + var firstTop = $contextuals.eq(0).offset().top; + var secondTop = $contextuals.eq(1).offset().top; + if (firstTop === secondTop) { + var $nestedContextual = $contextuals.eq(1); + + // Retrieve height of nested contextual link. + var height = 0; + var $trigger = $nestedContextual.find('.trigger'); + // Elements with the .element-invisible class have no dimensions, so this + // class must be temporarily removed to the calculate the height. + $trigger.removeClass('element-invisible'); + height = $nestedContextual.height(); + $trigger.addClass('element-invisible'); + + // Adjust nested contextual link's position. + $nestedContextual.css({ top: $nestedContextual.position().top + height }); + } + } +}; + +/** + * Contextual links object. + */ +Drupal.contextual = function($links, $region) { + this.$links = $links; + this.$region = $region; + + this.init(); +}; + +/** + * Initiates a contextual links object. + */ +Drupal.contextual.prototype.init = function() { + // Wrap the links to provide positioning and behavior attachment context. + this.$wrapper = $(Drupal.theme.contextualWrapper()) + .insertBefore(this.$links) + .append(this.$links); + + // Mark the links as hidden. Use aria-role form so that the number of items + // in the list is spoken. + this.$links + .prop('hidden', true) + .attr('role', 'form'); + + // Create and append the contextual links trigger. + var action = Drupal.t('Open'); + + var parentBlock = this.$region.find('h2').first().text(); + this.$trigger = $(Drupal.theme.contextualTrigger()) + .text(Drupal.t('@action @parent configuration options', {'@action': action, '@parent': parentBlock})) + // Set the aria-pressed state. + .prop('aria-pressed', false) + .prependTo(this.$wrapper); + + // The trigger behaviors are never detached or mutated. + this.$region + .on('click.contextual', '.contextual .trigger:first', $.proxy(this.triggerClickHandler, this)) + .on('mouseleave.contextual', '.contextual', {show: false}, $.proxy(this.triggerLeaveHandler, this)); + // Attach highlight behaviors. + this.attachHighlightBehaviors(); +}; + +/** + * Attaches highlight-on-mouseenter behaviors. + */ +Drupal.contextual.prototype.attachHighlightBehaviors = function () { + // Bind behaviors through delegation. + var highlightRegion = $.proxy(this.highlightRegion, this); + this.$region + .on('mouseenter.contextual.highlight', {highlight: true}, highlightRegion) + .on('mouseleave.contextual.highlight', {highlight: false}, highlightRegion) + .on('click.contextual.highlight', '.contextual-links a', {highlight: false}, highlightRegion) + .on('focus.contextual.highlight', '.contextual-links a, .contextual .trigger', {highlight: true}, highlightRegion) + .on('blur.contextual.highlight', '.contextual-links a, .contextual .trigger', {highlight: false}, highlightRegion); +}; + +/** + * Detaches unhighlight-on-mouseleave behaviors. + */ +Drupal.contextual.prototype.detachHighlightBehaviors = function () { + this.$region.off('.contextual.highlight'); +}; + +/** + * Toggles the highlighting of a contextual region. + * + * @param {Object} event + * jQuery Event object. + */ +Drupal.contextual.prototype.highlightRegion = function(event) { + // Set up a timeout to delay the dismissal of the region highlight state. + if (!event.data.highlight && this.timer === undefined) { + return this.timer = window.setTimeout($.proxy($.fn.trigger, $(event.target), 'mouseleave.contextual'), 100); + } + // Clear the timeout to prevent an infinite loop of mouseleave being + // triggered. + if (this.timer) { + window.clearTimeout(this.timer); + delete this.timer; + } + // Toggle active state of the contextual region based on the highlight value. + this.$region.toggleClass('contextual-region-active', event.data.highlight); + // Hide the links if the contextual region is inactive. + var state = this.$region.hasClass('contextual-region-active'); + if (!state) { + this.showLinks(state); + } +}; + +/** + * Handles click on the contextual links trigger. + * + * @param {Object} event + * jQuery Event object. + */ +Drupal.contextual.prototype.triggerClickHandler = function (event) { + event.preventDefault(); + // Hide all nested contextual triggers while the links are shown for this one. + this.$region.find('.contextual .trigger:not(:first)').hide(); + this.showLinks(); +}; + +/** + * Handles mouseleave on the contextual links trigger. + * + * @param {Object} event + * jQuery Event object. + */ +Drupal.contextual.prototype.triggerLeaveHandler = function (event) { + var show = event && event.data && event.data.show; + // Show all nested contextual triggers when the links are hidden for this one. + this.$region.find('.contextual .trigger:not(:first)').show(); + this.showLinks(show); +}; + +/** + * Toggles the active state of the contextual links. + * + * @param {Boolean} show + * (optional) True if the links should be shown. False is the links should be + * hidden. + */ +Drupal.contextual.prototype.showLinks = function(show) { + this.$wrapper.toggleClass('contextual-links-active', show); + var isOpen = this.$wrapper.hasClass('contextual-links-active'); + var action = (isOpen) ? Drupal.t('Close') : Drupal.t('Open'); + var parentBlock = this.$region.find('h2').first().text(); + this.$trigger + .text(Drupal.t('@action @parent configuration options', {'@action': action, '@parent': parentBlock})) + // Set the aria-pressed state. + .prop('aria-pressed', isOpen); + // Mark the links as hidden if they are. + if (isOpen) { + this.$links.prop('hidden', false); + } + else { + this.$links.prop('hidden', true); + } + +}; + +/** + * Shows or hides all pencil icons and corresponding contextual regions. + */ +function toggleEditMode (event, data) { + for (var i = contextuals.length - 1; i >= 0; i--) { + contextuals[i][(data.status) ? 'detachHighlightBehaviors' : 'attachHighlightBehaviors'](); + contextuals[i].$region.toggleClass('contextual-region-active', data.status); + } +} + +/** + * Wraps contextual links. + * + * @return {String} + * A string representing a DOM fragment. + */ +Drupal.theme.contextualWrapper = function () { + return '
'; +}; + +/** + * A trigger is an interactive element often bound to a click handler. + * + * @return {String} + * A string representing a DOM fragment. + */ +Drupal.theme.contextualTrigger = function () { + return ''; +}; + +})(jQuery, Drupal); 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..3cec929 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -65,7 +65,6 @@ function edit_library_info() { $path = drupal_get_path('module', 'edit'); $options = array( 'scope' => 'footer', - 'attributes' => array('defer' => TRUE), ); $libraries['edit'] = array( 'title' => 'Edit: in-place editing', @@ -73,10 +72,8 @@ function edit_library_info() { 'version' => VERSION, 'js' => array( // Core. + $path . '/js/editors/editor.js' => $options, $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, @@ -84,11 +81,8 @@ function edit_library_info() { $path . '/js/views/toolbar-view.js' => $options, // Backbone.sync implementation on top of Drupal forms. $path . '/js/backbone.drupalform.js' => $options, - // VIE service. - $path . '/js/viejs/EditService.js' => $options, - // Create.js subclasses. - $path . '/js/createjs/editable.js' => $options, - $path . '/js/createjs/storage.js' => $options, + // Storage manager. + $path . '/js/storage.js' => $options, // Other. $path . '/js/util.js' => $options, $path . '/js/theme.js' => $options, @@ -109,8 +103,6 @@ function edit_library_info() { array('system', 'jquery'), array('system', 'underscore'), array('system', 'backbone'), - array('system', 'vie.core'), - array('system', 'create.editonly'), array('system', 'jquery.form'), array('system', 'drupal.form'), array('system', 'drupal.ajax'), @@ -121,7 +113,7 @@ function edit_library_info() { 'title' => '"Form" Create.js PropertyEditor widget', 'version' => VERSION, 'js' => array( - $path . '/js/createjs/editingWidgets/formwidget.js' => $options, + $path . '/js/editors/formEditor.js' => $options, ), 'dependencies' => array( array('edit', 'edit'), @@ -131,7 +123,7 @@ function edit_library_info() { 'title' => '"Direct" Create.js PropertyEditor widget', 'version' => VERSION, 'js' => array( - $path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => $options, + $path . '/js/editors/directEditor.js' => $options, ), 'dependencies' => array( array('edit', 'edit'), diff --git a/core/modules/edit/js/app.js b/core/modules/edit/js/app.js deleted file mode 100644 index 14d76a0..0000000 --- a/core/modules/edit/js/app.js +++ /dev/null @@ -1,391 +0,0 @@ -/** - * @file - * A Backbone View that is the central app controller. - */ -(function ($, _, Backbone, Drupal, VIE) { - -"use strict"; - - Drupal.edit = Drupal.edit || {}; - Drupal.edit.EditAppView = Backbone.View.extend({ - vie: null, - domService: null, - - // Configuration for state handling. - states: [], - activeEditorStates: [], - singleEditorStates: [], - - // State. - $entityElements: null, - - /** - * Implements Backbone Views' initialize() function. - */ - initialize: function() { - _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange'); - - // VIE instance for Edit. - this.vie = new VIE(); - // Use our custom DOM parsing service until RDFa is available. - this.vie.use(new this.vie.EditService()); - this.domService = this.vie.service('edit'); - - // Instantiate configuration for state handling. - this.states = [ - null, 'inactive', 'candidate', 'highlighted', - 'activating', 'active', 'changed', 'saving', 'saved', 'invalid' - ]; - this.activeEditorStates = ['activating', 'active']; - this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates); - - this.$entityElements = $([]); - - // Use Create's Storage widget. - this.$el.createStorage({ - vie: this.vie, - editableNs: 'createeditable' - }); - - // When view/edit mode is toggled in the menu, update the editor widgets. - this.model.on('change:activeEntity', this.appStateChange); - }, - - /** - * Finds editable properties within a given context. - * - * Finds editable properties, registers them with the app, updates their - * state to match the current app state. - * - * @param $context - * A jQuery-wrapped context DOM element within which will be searched. - */ - findEditableProperties: function($context) { - var that = this; - var activeEntity = this.model.get('activeEntity'); - - this.domService.findSubjectElements($context).each(function() { - var $element = $(this); - - // Ignore editable properties for which we've already set up Create.js. - if (that.$entityElements.index($element) !== -1) { - return; - } - - $element - // Instantiate an EditableEntity widget. - .createEditable({ - vie: that.vie, - disabled: true, - state: 'inactive', - acceptStateChange: that.acceptEditorStateChange, - statechange: function(event, data) { - that.editorStateChange(data.previous, data.current, data.propertyEditor); - }, - decoratePropertyEditor: function(data) { - that.decorateEditor(data.propertyEditor); - } - }) - // This event is triggered just before Edit removes an EditableEntity - // widget, so that we can do proper clean-up. - .on('destroyedPropertyEditor.edit', function(event, editor) { - that.undecorateEditor(editor); - that.$entityElements = that.$entityElements.not($(this)); - }) - // Transition the new PropertyEditor into the default state. - .createEditable('setState', 'inactive'); - - // If the new PropertyEditor is for the entity that's currently being - // edited, then transition it to the 'candidate' state. - // (This happens when a field was modified and is re-rendered.) - var entityOfProperty = $element.createEditable('option', 'model'); - if (entityOfProperty.getSubjectUri() === activeEntity) { - $element.createEditable('setState', 'candidate'); - } - - // Add this new EditableEntity widget element to the list. - that.$entityElements = that.$entityElements.add($element); - }); - }, - - /** - * Sets the state of PropertyEditor widgets when edit mode begins or ends. - * - * Should be called whenever EditAppModel's "activeEntity" changes. - */ - appStateChange: function() { - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133, https://github.com/bergie/create/issues/140) - // We're currently setting the state on EditableEntity widgets instead of - // PropertyEditor widgets, because of - // https://github.com/bergie/create/issues/133. - - var activeEntity = this.model.get('activeEntity'); - var $editableFieldsForEntity = $('[data-edit-id^="' + activeEntity + '/"]'); - - // First, change the status of all PropertyEditor widgets to 'inactive'. - this.$entityElements.each(function() { - $(this).createEditable('setState', 'inactive', null, {reason: 'stop'}); - }); - - // Then, change the status of PropertyEditor widgets of the currently - // active entity to 'candidate'. - $editableFieldsForEntity.each(function() { - $(this).createEditable('setState', 'candidate'); - }); - - // Manage the page's tab indexes. - }, - - /** - * Accepts or reject editor (PropertyEditor) state changes. - * - * This is what ensures that the app is in control of what happens. - * - * @param from - * The previous state. - * @param to - * The new state. - * @param predicate - * The predicate of the property for which the state change is happening. - * @param context - * The context that is trying to trigger the state change. - * @param callback - * The callback function that should receive the state acceptance result. - */ - acceptEditorStateChange: function(from, to, predicate, context, callback) { - var accept = true; - - // If the app is in view mode, then reject all state changes except for - // those to 'inactive'. - if (context && context.reason === 'stop') { - if (from === 'candidate' && to === 'inactive') { - accept = true; - } - } - // Handling of edit mode state changes is more granular. - else { - // In general, enforce the states sequence. Disallow going back from a - // "later" state to an "earlier" state, except in explicitly allowed - // cases. - if (_.indexOf(this.states, from) > _.indexOf(this.states, to)) { - accept = false; - // Allow: activating/active -> candidate. - // Necessary to stop editing a property. - if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { - accept = true; - } - // Allow: changed/invalid -> candidate. - // Necessary to stop editing a property when it is changed or invalid. - else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { - accept = true; - } - // Allow: highlighted -> candidate. - // Necessary to stop highlighting a property. - else if (from === 'highlighted' && to === 'candidate') { - accept = true; - } - // Allow: saved -> candidate. - // Necessary when successfully saved a property. - else if (from === 'saved' && to === 'candidate') { - accept = true; - } - // Allow: invalid -> saving. - // Necessary to be able to save a corrected, invalid property. - else if (from === 'invalid' && to === 'saving') { - accept = true; - } - } - - // If it's not against the general principle, then here are more - // disallowed cases to check. - if (accept) { - // Ensure only one editor (field) at a time may be higlighted or active. - if (from === 'candidate' && _.indexOf(this.singleEditorStates, to) !== -1) { - if (this.model.get('highlightedEditor') || this.model.get('activeEditor')) { - accept = false; - } - } - // Reject going from activating/active to candidate because of a - // mouseleave. - else if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { - if (context && context.reason === 'mouseleave') { - accept = false; - } - } - // When attempting to stop editing a changed/invalid property, ask for - // confirmation. - else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { - if (context && context.reason === 'mouseleave') { - accept = false; - } - else { - // Check whether the transition has been confirmed? - if (context && context.confirmed) { - accept = true; - } - // Confirm this transition. - else { - // The callback will be called from the helper function. - this._confirmStopEditing(callback); - return; - } - } - } - } - } - - callback(accept); - }, - - /** - * Asks the user to confirm whether he wants to stop editing via a modal. - * - * @param acceptCallback - * The callback function as passed to acceptEditorStateChange(). This - * callback function will be called with the user's choice. - * - * @see acceptEditorStateChange() - */ - _confirmStopEditing: function(acceptCallback) { - // Only instantiate if there isn't a modal instance visible yet. - if (!this.model.get('activeModal')) { - var that = this; - var modal = new Drupal.edit.views.ModalView({ - model: this.model, - message: Drupal.t('You have unsaved changes'), - buttons: [ - { action: 'discard', classes: 'gray-button', label: Drupal.t('Discard changes') }, - { action: 'save', type: 'submit', classes: 'blue-button', label: Drupal.t('Save') } - ], - callback: function(action) { - // The active modal has been removed. - that.model.set('activeModal', null); - if (action === 'discard') { - acceptCallback(true); - } - else { - acceptCallback(false); - var editor = that.model.get('activeEditor'); - editor.options.widget.setState('saving', editor.options.property); - } - } - }); - this.model.set('activeModal', modal); - // The modal will set the activeModal property on the model when rendering - // to prevent multiple modals from being instantiated. - modal.render(); - } - else { - // Reject as there is still an open transition waiting for confirmation. - acceptCallback(false); - } - }, - - /** - * Reacts to editor (PropertyEditor) state changes; tracks global state. - * - * @param from - * The previous state. - * @param to - * The new state. - * @param editor - * The PropertyEditor widget object. - */ - editorStateChange: function(from, to, editor) { - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Get rid of this once that issue is solved. - if (!editor) { - return; - } - else { - editor.stateChange(from, to); - } - - // Keep track of the highlighted editor in the global state. - if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== editor) { - this.model.set('highlightedEditor', editor); - } - else if (this.model.get('highlightedEditor') === editor && to === 'candidate') { - this.model.set('highlightedEditor', null); - } - - // Keep track of the active editor in the global state. - if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== editor) { - this.model.set('activeEditor', editor); - } - else if (this.model.get('activeEditor') === editor && to === 'candidate') { - // Discarded if it transitions from a changed state to 'candidate'. - if (from === 'changed' || from === 'invalid') { - // Retrieve the storage widget from DOM. - var createStorageWidget = this.$el.data('DrupalCreateStorage'); - // Revert changes in the model, this will trigger the direct editable - // content to be reset and redrawn. - createStorageWidget.revertChanges(editor.options.entity); - } - this.model.set('activeEditor', null); - } - - // Propagate the state change to the decoration and toolbar views. - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Uncomment this once that issue is solved. - // editor.decorationView.stateChange(from, to); - // editor.toolbarView.stateChange(from, to); - }, - - /** - * Decorates an editor (PropertyEditor). - * - * Upon the page load, all appropriate editors are initialized and decorated - * (i.e. even before anything of the editing UI becomes visible; even before - * edit mode is enabled). - * - * @param editor - * The PropertyEditor widget object. - */ - decorateEditor: function(editor) { - // Toolbars are rendered "on-demand" (highlighting or activating). - // They are a sibling element before the editor's DOM element. - editor.toolbarView = new Drupal.edit.views.ToolbarView({ - editor: editor, - $storageWidgetEl: this.$el - }); - - // Decorate the editor's DOM element depending on its state. - editor.decorationView = new Drupal.edit.views.PropertyEditorDecorationView({ - el: editor.element, - editor: editor, - toolbarId: editor.toolbarView.getId() - }); - - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Get rid of this once that issue is solved. - editor.options.widget.element.on('createeditablestatechange', function(event, data) { - editor.decorationView.stateChange(data.previous, data.current); - editor.toolbarView.stateChange(data.previous, data.current); - }); - }, - - /** - * Undecorates an editor (PropertyEditor). - * - * Whenever a property has been updated, the old HTML will be replaced by - * the new (re-rendered) HTML. The EditableEntity widget will be destroyed, - * as will be the PropertyEditor widget. This method ensures Edit's editor - * views also are removed properly. - * - * @param editor - * The PropertyEditor widget object. - */ - undecorateEditor: function(editor) { - editor.toolbarView.undelegateEvents(); - editor.toolbarView.remove(); - delete editor.toolbarView; - editor.decorationView.undelegateEvents(); - // Don't call .remove() on the decoration view, because that would remove - // a potentially rerendered field. - delete editor.decorationView; - } - - }); - -})(jQuery, _, Backbone, Drupal, VIE); diff --git a/core/modules/edit/js/backbone.drupalform.js b/core/modules/edit/js/backbone.drupalform.js index 750d16f..170f4a9 100644 --- a/core/modules/edit/js/backbone.drupalform.js +++ b/core/modules/edit/js/backbone.drupalform.js @@ -105,7 +105,7 @@ Backbone.syncDirect = function(method, model, options) { if (jQuery('#edit_backstage form').length === 0) { var formOptions = { propertyID: Drupal.edit.util.calcPropertyID(entity, predicate), - $editorElement: options.editor.element, + $editorElement: options.editor.$el, nocssjs: true }; Drupal.edit.util.form.load(formOptions, function(form, ajax) { diff --git a/core/modules/edit/js/createjs/editable.js b/core/modules/edit/js/createjs/editable.js deleted file mode 100644 index 1316023..0000000 --- a/core/modules/edit/js/createjs/editable.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @file - * Determines which editor (Create.js PropertyEditor widget) to use. - */ -(function (jQuery, Drupal, drupalSettings) { - -"use strict"; - - jQuery.widget('Drupal.createEditable', jQuery.Midgard.midgardEditable, { - _create: function() { - this.vie = this.options.vie; - - this.options.domService = 'edit'; - this.options.predicateSelector = '*'; //'.edit-field.edit-allowed'; - - // The Create.js PropertyEditor widget configuration is not hardcoded; it - // is generated by the server. - this.options.propertyEditorWidgetsConfiguration = drupalSettings.edit.editors; - - jQuery.Midgard.midgardEditable.prototype._create.call(this); - }, - - _propertyEditorName: function(data) { - // Pick a PropertyEditor widget for a property depending on its metadata. - var propertyID = Drupal.edit.util.calcPropertyID(data.entity, data.property); - return Drupal.edit.metadataCache[propertyID].editor; - } - }); - -})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js deleted file mode 100644 index cde6163..0000000 --- a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @file - * Override of Create.js' default "base" (plain contentEditable) widget. - */ -(function (jQuery, Drupal) { - -"use strict"; - - // @todo D8: use jQuery UI Widget bridging. - // @see http://drupal.org/node/1874934#comment-7124904 - jQuery.widget('Midgard.direct', jQuery.Midgard.editWidget, { - - /** - * Implements getEditUISettings() method. - */ - getEditUISettings: function() { - return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; - }, - - /** - * Implements jQuery UI widget factory's _init() method. - * - * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142) - * Get rid of this once that issue is solved. - */ - _init: function() {}, - - /** - * Implements Create's _initialize() method. - */ - _initialize: function() { - var that = this; - - // Sets the state to 'changed' whenever the content has changed. - var before = jQuery.trim(this.element.text()); - this.element.on('keyup paste', function (event) { - if (that.options.disabled) { - return; - } - var current = jQuery.trim(that.element.text()); - if (before !== current) { - before = current; - that.options.changed(current); - } - }); - }, - - /** - * Makes this PropertyEditor widget react to state changes. - */ - stateChange: function(from, to) { - switch (to) { - case 'inactive': - break; - case 'candidate': - if (from !== 'inactive') { - // Removes the "contenteditable" attribute. - this.disable(); - } - break; - case 'highlighted': - break; - case 'activating': - this.options.activated(); - break; - case 'active': - // Sets the "contenteditable" attribute to "true". - this.enable(); - break; - case 'changed': - break; - case 'saving': - break; - case 'saved': - break; - case 'invalid': - break; - } - } - - }); - -})(jQuery, Drupal); diff --git a/core/modules/edit/js/createjs/editingWidgets/formwidget.js b/core/modules/edit/js/createjs/editingWidgets/formwidget.js deleted file mode 100644 index aa2dd0a..0000000 --- a/core/modules/edit/js/createjs/editingWidgets/formwidget.js +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @file - * Form-based Create.js widget for structured content in Drupal. - */ -(function ($, Drupal) { - -"use strict"; - - // @todo D8: change the name to "form" + use jQuery UI Widget bridging. - // @see http://drupal.org/node/1874934#comment-7124904 - $.widget('Midgard.formEditEditor', $.Midgard.editWidget, { - - id: null, - $formContainer: null, - - /** - * Implements getEditUISettings() method. - */ - getEditUISettings: function() { - return { padding: false, unifiedToolbar: false, fullWidthToolbar: false }; - }, - - /** - * Implements jQuery UI widget factory's _init() method. - * - * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142) - * Get rid of this once that issue is solved. - */ - _init: function() {}, - - /** - * Implements Create's _initialize() method. - */ - _initialize: function() {}, - - /** - * Makes this PropertyEditor widget react to state changes. - */ - stateChange: function(from, to) { - switch (to) { - case 'inactive': - break; - case 'candidate': - if (from !== 'inactive') { - this.disable(); - } - break; - case 'highlighted': - break; - case 'activating': - this.enable(); - break; - case 'active': - break; - case 'changed': - break; - case 'saving': - break; - case 'saved': - break; - case 'invalid': - break; - } - }, - - /** - * Enables the widget. - */ - enable: function () { - var $editorElement = $(this.options.widget.element); - var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); - - // Generate a DOM-compatible ID for the form container DOM element. - this.id = 'edit-form-for-' + propertyID.replace(/\//g, '_'); - - // Render form container. - this.$formContainer = $(Drupal.theme('editFormContainer', { - id: this.id, - loadingMsg: Drupal.t('Loading…')} - )); - this.$formContainer - .find('.edit-form') - .addClass('edit-editable edit-highlighted edit-editing') - .attr('role', 'dialog'); - - // Insert form container in DOM. - if ($editorElement.css('display') === 'inline') { - // @todo: POSTPONED_ON(Drupal core, title/author/date as Entity Properties) - // This is untested in Drupal 8, because in Drupal 8 we don't yet - // have the ability to edit the node title/author/date, because they - // haven't been converted into Entity Properties yet, and they're the - // only examples in core of "display: inline" properties. - this.$formContainer.prependTo($editorElement.offsetParent()); - - var pos = $editorElement.position(); - this.$formContainer.css('left', pos.left).css('top', pos.top); - } - else { - this.$formContainer.insertBefore($editorElement); - } - - // Load form, insert it into the form container and attach event handlers. - var widget = this; - var formOptions = { - propertyID: propertyID, - $editorElement: $editorElement, - nocssjs: false - }; - Drupal.edit.util.form.load(formOptions, function(form, ajax) { - Drupal.ajax.prototype.commands.insert(ajax, { - data: form, - selector: '#' + widget.id + ' .placeholder' - }); - - var $submit = widget.$formContainer.find('.edit-form-submit'); - Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); - widget.$formContainer - .on('formUpdated.edit', ':input', function () { - // Sets the state to 'changed'. - widget.options.changed(); - }) - .on('keypress.edit', 'input', function (event) { - if (event.keyCode === 13) { - return false; - } - }); - - // Sets the state to 'activated'. - widget.options.activated(); - }); - }, - - /** - * Disables the widget. - */ - disable: function () { - if (this.$formContainer === null) { - return; - } - - Drupal.edit.util.form.unajaxifySaving(this.$formContainer.find('.edit-form-submit')); - // Allow form widgets to detach properly. - Drupal.detachBehaviors(this.$formContainer, null, 'unload'); - this.$formContainer - .off('change.edit', ':input') - .off('keypress.edit', 'input') - .remove(); - this.$formContainer = null; - } - }); - -})(jQuery, Drupal); diff --git a/core/modules/edit/js/createjs/storage.js b/core/modules/edit/js/createjs/storage.js deleted file mode 100644 index 580ff82..0000000 --- a/core/modules/edit/js/createjs/storage.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @file - * Subclasses jQuery.Midgard.midgardStorage to have consistent namespaces. - */ -(function(jQuery) { - -"use strict"; - - jQuery.widget('Drupal.createStorage', jQuery.Midgard.midgardStorage, {}); - -})(jQuery); diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index f924e7b..1f836c1 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -13,29 +13,55 @@ Drupal.edit.metadataCache = Drupal.edit.metadataCache || {}; * Attach toggling behavior and in-place editing. */ Drupal.behaviors.edit = { + + views: { + contextualLinks: {}, + decorators: {}, + editables: {}, + entities: {}, + propertyEditors: {}, + 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; + + // Store a collection of entity models. + if (!this.collections.entities) { + Collection = Backbone.Collection.extend({ + model: Drupal.edit.EntityModel + }); + this.collections.entities = new Collection(); + } + + // Store a collection of editables models. + if (!this.collections.editables) { + this.collections.editables = new Drupal.edit.EditablesCollection(); + } + + this.collections.editables + // Respond to editable model HTML representation change events. + .on('change:html', this.updateAllKnownInstances, this); // 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')); - } + $('body').once('edit-init', $.proxy(this.init, this, options)); - return true; - } - return false; - }; + // @todo currently must be after the call to init, because the app needs to be initialized + this.collections.editables + .on('change:state', Drupal.edit.app.editorStateChange, Drupal.edit.app); // Find all fields in the context without metadata. var fieldsToAnnotate = _.map($fields.not('.edit-allowed, .edit-disallowed'), function(el) { @@ -46,14 +72,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() { @@ -66,48 +93,644 @@ Drupal.behaviors.edit = { // Update the metadata cache. _.each(results, function(metadata, editID) { Drupal.edit.metadataCache[editID] = metadata; + // @temporary HACK: use the 'form' editor for all fields + Drupal.edit.metadataCache[editID].editor = 'form'; }); // Annotate the remaining fields based on the updated access cache. - _.each(remainingFieldsToAnnotate, annotateField); + _.each(remainingFieldsToAnnotate, $.proxy(that.annotateField, that)); - // Find editable fields, make them editable. - Drupal.edit.app.findEditableProperties($context); + // Make fields that could be annotated immediately available for editing. + that.findEditableProperties($context, options); } }); }); } - } -}; + }, -Drupal.edit.init = function() { - // Instantiate EditAppView, which is the controller of it all. EditAppModel - // instance tracks global state (viewing/editing in-place). - var appModel = new Drupal.edit.models.EditAppModel(); - var app = new Drupal.edit.EditAppView({ - el: $('body'), - model: appModel - }); - - // Add "Quick edit" links to all contextual menus where editing the full - // node is possible. - // @todo Generalize this to work for all entities. - $('ul.contextual-links li.node-edit') - .before('
  • ') - .each(function() { - // Instantiate ContextualLinkView. - var $editContextualLink = $(this).prev(); - var editContextualLinkView = new Drupal.edit.views.ContextualLinkView({ - el: $editContextualLink.get(0), + init: function (options) { + var that = this; + // Instantiate EditAppView, which is the controller of it all. EditAppModel + // instance tracks global state (viewing/editing in-place). + var appModel = new Drupal.edit.AppModel(); + var app = new Drupal.edit.AppView({ + el: $('body').get(0), model: appModel, - entity: $editContextualLink.parents('[data-edit-entity]').attr('data-edit-entity') + entitiesCollection: this.collections.entities }); - }); + var entityModel; + + // Create a view for the Entity just once. + $('[data-edit-entity]').once('editEntity', function (index) { + var $this = $(this); + var id = $this.data('edit-entity'); + + entityModel = new Drupal.edit.EntityModel({ + id: id + }); + that.collections.entities.add(entityModel); + + // Create a 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, + appModel: appModel + }, options)); + }); + }); + + // Add "Quick edit" links to all contextual menus where editing the full + // node is possible. + // @todo Generalize this to work for all entities. + + + // For now, we work with a singleton app, because for Drupal.behaviors to be + // able to discover new editable properties that get AJAXed in, it must know + // with which app instance they should be associated. + Drupal.edit.app = app; + }, + + /** + * Finds editable properties within a given context. + * + * Finds editable properties, registers them with the app, updates their + * state to match the current app state. + * + * @param $context + * A jQuery-wrapped context DOM element within which will be searched. + */ + findEditableProperties: function($context, options) { + var that = this; + var app = Drupal.edit.app; + var model = app.model; + var activeEntity = model.get('activeEntity'); + options = options || {}; + + $context.find('[data-edit-id]').each(function() { + var $element = $(this); + var editID = $element.attr('data-edit-id'); + + if (!_.has(Drupal.edit.metadataCache, editID)) { + return; + } + + var entityId = $element.closest('[data-edit-entity]').data('edit-entity'); + // @todo Note that 1) not every field that has a data-edit-id also has a + // surrounding data-edit-entity, 2) also note that whether the "Quick Edit" + // link should appear depends on whether the user has access to edit any + // of the entity's fields. So, it is blocked on the metadata callback to + // the server. This is not yet implemented in the D8 HEAD Edit, but it is + // in http://drupal.org/node/1971108. We should take this into account if + // possible. If not, then we'll just have to refactor later. + // For now, this assumption is acceptable. + var entity = that.collections.entities.where({id: entityId})[0]; + + // The EditableModel stores the state of an editable entity field. + var editableModel = new Drupal.edit.EditableModel({ + entity: entity, + $el: $element, + editID: editID, + label: Drupal.edit.metadataCache[editID].label, + editor: Drupal.edit.metadataCache[editID].editor, + html: $element[0].outerHTML, + acceptStateChange: _.bind(Drupal.edit.app.acceptEditorStateChange, Drupal.edit.app) + }); + + // Store this editable field model in the collection where we track all + // fields on the page. + that.collections.editables.add(editableModel); + + // @todo make the below work + return; - // 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; + // 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; + }, + + // Updates all known instances of a specific entity field whenever the HTML + // representation of one of them has changed. + // @todo: this is currently prototype-level code, test this. The principle is + // sound, but the tricky thing is that an editID includes the view mode, but + // we actually want to update the same field in other view modes too, which + // means this method will have to check if there are any such instances, and + // if so, go to the server and re-render those too. + updateAllKnownInstances: function (changedModel) { + changedModel.collection.where({ editID: changedModel.get('editID') }) + .set('html', changedModel.get('html')); + }, + + defaults: { + strings: { + quickEdit: Drupal.t('Quick edit'), + stopQuickEdit: Drupal.t('Stop quick edit') + } + } }; +$.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 (options) { + this.entitiesCollection = options.entitiesCollection; + + _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange'); + + // 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.entitiesCollection.on('change:isActive', this.appStateChange, this); + }, + + /** + * Handles setup/teardown and state changes when the active entity changes. + */ + appStateChange: function (entityModel, isActive) { + var app = this; + if (isActive) { + // Move all fields of this entity from the 'inactive' state to the + // 'candidate' state. + entityModel.get('fields').each(function (fieldModel) { + // First, set up decoration views. + app.decorate(fieldModel); + // Second, change the field's state. + fieldModel.setState('candidate'); + }); + } + else { + // Move all fields of this entity from whatever state they are in to + // the 'inactive' state. + entityModel.get('fields').each(function (fieldModel) { + // First, change the field's state. + fieldModel.setState('inactive', { reason: 'stop' }); + // Second, tear down decoration views. + app.undecorate(fieldModel); + }); + } + }, + + /** + * Accepts or reject editor (PropertyEditor) state changes. + * + * This is what ensures that the app is in control of what happens. + * + * @param String from + * The previous state. + * @param String to + * The new state. + * @param null|Object context + * The context that is trying to trigger the state change. + * @param Function callback + * The callback function that should receive the state acceptance result. + */ + acceptEditorStateChange: function(from, to, context, callback) { + var accept = true; + + console.log("accept? %s → %s (reason: %s)", from, to, (context && context.reason) ? context.reason : 'NONE'); + + // If the app is in view mode, then reject all state changes except for + // those to 'inactive'. + if (context && context.reason === 'stop') { + if (from === 'candidate' && to === 'inactive') { + accept = true; + } + } + // Handling of edit mode state changes is more granular. + else { + // In general, enforce the states sequence. Disallow going back from a + // "later" state to an "earlier" state, except in explicitly allowed + // cases. + if (_.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); + }, + + // @todo rename to decorateField + decorate: function (fieldModel) { + var editID = fieldModel.get('editID'); + var $el = fieldModel.get('$el'); + + // Create a new Editor. + var editorName = fieldModel.get('editor'); + var editorView = new Drupal.edit.editors[editorName]({ + el: $el, + model: fieldModel + }); + + // Toolbars are rendered "on-demand" (highlighting or activating). + // They are a sibling element before the editor's DOM element. + var toolbarView = new Drupal.edit.ToolbarView({ + model: fieldModel, + editor: editorView + }); + + // Decorate the editor's DOM element depending on its state. + var decorationView = new Drupal.edit.PropertyEditorDecorationView({ + el: $el, + model: fieldModel, + editor: editorView, + toolbarId: toolbarView.getId() + }); + + // Create references; necessary for undecorate(). + fieldModel.set('decorationViews', { + editorView: editorView, + toolbarView: toolbarView, + decorationView: decorationView + }); + }, + + // @todo rename to undecorateField + undecorate: function (fieldModel) { + var decorationViews = fieldModel.get('decorationViews'); + + // Unbind event handlers; remove toolbar element; delete toolbar view. + decorationViews.toolbarView.undelegateEvents(); + decorationViews.toolbarView.remove(); + delete decorationViews.toolbarView; + + // Unbind event handlers; delete decoration view. Don't remove the element + // because that would remove the field itself. + decorationViews.decorationView.undelegateEvents(); + delete decorationViews.decorationView; + + // Unbind event handlers; delete editor view. Don't remove the element + // because that would remove the field itself. + decorationViews.editorView.undelegateEvents(); + delete decorationViews.editorView; + }, + + /** + * 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(fieldModel, state) { + var from = fieldModel.previous('state'); + var to = state; + + console.log("%c %s → %s %s", "background-color: black; color: white", from, to, fieldModel.get('editID')); + + // Keep track of the highlighted editor in the global state. + if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== fieldModel) { + this.model.set('highlightedEditor', fieldModel); + } + else if (this.model.get('highlightedEditor') === fieldModel && to === 'candidate') { + this.model.set('highlightedEditor', null); + } + + // Keep track of the active editor in the global state. + if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== fieldModel) { + this.model.set('activeEditor', fieldModel); + } + else if (this.model.get('activeEditor') === fieldModel && to === 'candidate') { + // Discarded if it transitions from a changed state to 'candidate'. + if (from === 'changed' || from === 'invalid') { + // Retrieve the storage widget from DOM. + var createStorageWidget = this.$el.data('DrupalCreateStorage'); + // Revert changes in the model, this will trigger the direct editable + // content to be reset and redrawn. + createStorageWidget.revertChanges(fieldModel.options.entity); + } + this.model.set('activeEditor', null); + } + } + }), + + /** + * + */ + EntityModel: Backbone.Model.extend({ + defaults: { + // An entity ID, of the form "/", e.g. "node/1". + id: null, + // Indicates whether this instance of this entity is currently being + // edited. + isActive: false, + // A Drupal.edit.EditablesCollection for all fields of this entity. + fields: null + }, + initialize: function () { + this.set('fields', new Drupal.edit.EditablesCollection()); + } + }), + + /** + * + */ + 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; + } + }), + + /** + * + */ + // @todo rename to FieldModel? + EditableModel: Backbone.Model.extend({ + defaults: { + // @todo Try to get rid of this, but it's hard; see appStateChange() + $el: null, + + // Possible states: + // - inactive + // - candidate + // - highlighted + // - activating + // - active + // - changed + // - saving + // - saved + // - saved + // - invalid + // @see http://createjs.org/guide/#states + state: 'inactive', + // A Drupal.edit.EntityModel. Its "properties" attribute, which is an + // EditablesCollection, is automatically updated to include this + // EditableModel. + entity: null, + // A place to store any decoration views. + decorationViews: {}, + + + // + // Data set by the metadata callback. + // + // The ID of the in-place editor to use. + editor: null, + // The label to use. + label: null, + + // + // Data derived from the information in the DOM. + // + + // The edit ID, format: `::::`. + editID: null, + // The full HTML representation of this field (with the element that has + // the data-edit-id as the outer element). Used to propagate changes from + // this field instance to other instances of the same field. + html: null, + + // + // Callbacks. + // + + // Callback function for validating changes between states. Receives the + // previous state, new state, context, and a callback + acceptStateChange: null + }, + initialize: function () { + this.get('entity').get('fields').add(this); + }, + // @see VIE.prototype.EditService.prototype.getElementSubject() + // Parses the `/` part from the edit ID. + getEntityID: function() { + return this.get('editID').split('/').slice(0, 2).join('/'); + }, + // @see VIE.prototype.EditService.prototype.getElementPredicate() + // Parses the `//` part from the edit ID. + getFieldID: function() { + return this.get('editID').split('/').slice(2, 5).join('/'); + }, + // Always call this method, never call .set('state'), because we need to call + // the acceptStateChange callback first + // @see Create.js' setState() + setState: function (state, context, callback) { + // @todo propagate to the entity in some way? I don't think this is + // actually necessary; the entity can listen on its collection of fields + // for changes to the state of any of its fields! + var current = this.get('state'); + var next = state; + // Only attempt to change the state if it's a different state. + if (current !== next) { + var acceptStateChange = this.get('acceptStateChange'); + var that = this; + acceptStateChange(current, next, context, function (accepted) { + if (accepted) { + that.set('state', next); + } + // @todo can't we get rid of this? + if (_.isFunction(callback)) { + callback(accepted); + } + }); + } + return this; + } + }), + + // A collection of EditableModels. + // @todo link back to the entity at the collection level (not the collection element level)? + EditablesCollection: Backbone.Collection.extend({ + model: Drupal.edit.EditableModel + }), + + // @todo provide a base class for formEditor/directEditor/…, migrate them away + // from editor.js + EditorView: Backbone.View.extend({ + + }) +}); + })(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/edit/js/editors/directEditor.js b/core/modules/edit/js/editors/directEditor.js new file mode 100644 index 0000000..ebf1650 --- /dev/null +++ b/core/modules/edit/js/editors/directEditor.js @@ -0,0 +1,84 @@ +/** + * @file + * Override of Create.js' default "base" (plain contentEditable) widget. + */ +(function ($, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.editors = Drupal.edit.editors || {}; + +Drupal.edit.editors.direct = Backbone.View.extend({ + + /** + * Implements getEditUISettings() method. + */ + getEditUISettings: function() { + return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** + * Implements jQuery UI widget factory's _init() method. + * + * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142) + * Get rid of this once that issue is solved. + */ + _init: function() {}, + + /** + * Implements Create's _initialize() method. + */ + _initialize: function() { + var that = this; + + // Sets the state to 'changed' whenever the content has changed. + var before = jQuery.trim(this.element.text()); + this.element.on('keyup paste', function (event) { + if (that.options.disabled) { + return; + } + var current = jQuery.trim(that.element.text()); + if (before !== current) { + before = current; + that.options.changed(current); + } + }); + }, + + /** + * Makes this PropertyEditor widget react to state changes. + */ + stateChange: function(model, value) { + switch (value) { + 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': + this.save(); + break; + case 'saved': + break; + case 'invalid': + break; + } + } +}); + +})(jQuery, Drupal); diff --git a/core/modules/edit/js/editors/editor.js b/core/modules/edit/js/editors/editor.js new file mode 100644 index 0000000..e9a3696 --- /dev/null +++ b/core/modules/edit/js/editors/editor.js @@ -0,0 +1,81 @@ +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + Drupal.edit = Drupal.edit || {}; + Drupal.edit.editors = Drupal.edit.editors || {}; + + Drupal.edit.editors.editor = function () {}; + + $.extend(Drupal.edit.editors.editor.prototype, { + // override to enable the widget + enable: function () { + this.element.attr('contenteditable', 'true'); + }, + // override to disable the widget + disable: function (disable) { + this.element.attr('contenteditable', 'false'); + }, + // called by the jQuery UI plugin factory when creating the property editor + // widget instance + _create: function () { + this._registerWidget(); + this._initialize(); + + if (_.isFunction(this.options.decorate) && _.isFunction(this.options.decorateParams)) { + // TRICKY: we can't use this.options.decorateParams()'s 'propertyName' + // parameter just yet, because it will only be available after this + // object has been created, but we're currently in the constructor! + // Hence we have to duplicate part of its logic here. + this.options.decorate(this.options.decorateParams(null, { + propertyName: this.options.property, + propertyEditor: this, + propertyElement: this.element, + // Deprecated. + editor: this, + predicate: this.options.property, + element: this.element + })); + } + }, + // called every time the property editor widget is called + _init: function () { + if (this.options.disabled) { + this.disable(); + return; + } + this.enable(); + }, + // override this function to initialize the property editor widget functions + _initialize: function () { + var self = this; + this.element.on('focus', function () { + if (self.options.disabled) { + return; + } + self.options.activated(); + }); + this.element.on('blur', function () { + if (self.options.disabled) { + return; + } + self.options.deactivated(); + }); + var before = this.element.html(); + this.element.on('keyup paste', function (event) { + if (self.options.disabled) { + return; + } + var current = jQuery(this).html(); + if (before !== current) { + before = current; + self.options.changed(current); + } + }); + }, + // used to register the property editor widget name with the DOM element + _registerWidget: function () { + this.element.data("createWidgetName", this.widgetName); + } + }); +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js new file mode 100644 index 0000000..11b4fbb --- /dev/null +++ b/core/modules/edit/js/editors/formEditor.js @@ -0,0 +1,230 @@ +/** + * @file + * Form-based Create.js widget for structured content in Drupal. + */ +(function ($, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.editors = Drupal.edit.editors || {}; + +Drupal.edit.editors.form = Backbone.View.extend({ + id: null, + $formContainer: null, + + /** + * Implements getEditUISettings() method. + */ + getEditUISettings: function() { + return { padding: false, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** + * + */ + initialize: function (options) { + this.model.on('change:state', this.stateChange, this); + + // Generate a DOM-compatible ID for the form container DOM element. + this.elementId = 'edit-form-for-' + this.model.get('editID').replace(/\//g, '_'); + }, + + + render: function () { + var isActive = this.model.get('isActive'); + if (isActive) { + this.enable(); + } + else { + this.disable(); + } + return this; + }, + + /** + * Enables the widget. + */ + enable: function () { + // Render form container. + this.$formContainer = $(Drupal.theme('editFormContainer', { + id: this.elementId, + loadingMsg: Drupal.t('Loading…')} + )); + this.$formContainer + .find('.edit-form') + .addClass('edit-editable edit-highlighted edit-editing') + .attr('role', 'dialog'); + + // Insert form container in DOM. + if (false && $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(this.$el); + } + + // Load form, insert it into the form container and attach event handlers. + var that = this; + var formOptions = { + propertyID: this.model.get('propertyId'), + $editorElement: this.$el, + nocssjs: false + }; + Drupal.edit.util.form.load(formOptions, function(form, ajax) { + Drupal.ajax.prototype.commands.insert(ajax, { + data: form, + selector: '#' + that.elementId + ' .placeholder' + }); + + var $submit = that.$formContainer.find('.edit-form-submit'); + Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); + that.$formContainer + .on('formUpdated.edit', ':input', function () { + // Sets the state to 'changed'. + that.model.set('isDirty', true); + }) + .on('keypress.edit', 'input', function (event) { + if (event.keyCode === 13) { + return false; + } + }); + }); + }, + + /** + * Disables the widget. + */ + disable: function () { + if (this.$formContainer === null) { + return; + } + + Drupal.edit.util.form.unajaxifySaving(this.$formContainer.find('.edit-form-submit')); + // Allow form widgets to detach properly. + Drupal.detachBehaviors(this.$formContainer, null, 'unload'); + this.$formContainer + .off('change.edit', ':input') + .off('keypress.edit', 'input') + .remove(); + this.$formContainer = null; + }, + + /** + * Saves a property. + * + * This method deals with the complexity of the editor-dependent ways of + * inserting updated content and showing validation error messages. + * + * One might argue that this does not belong in a view. However, there is no + * actual "save" logic here, that lives in Backbone.sync. This is just some + * glue code, along with the logic for inserting updated content as well as + * showing validation error messages, the latter of which is certainly okay. + */ + save: function() { + var that = this; + var editor = this; + var editableEntity = this.el; + var entity = this.entity; + var predicate = this.predicate; + + // Use Create.js' Storage widget to handle saving. (Uses Backbone.sync.) + var storage = new Drupal.edit.Storage({ + vie: that.vie, + editableNs: 'createeditable', + element: this.$el + }); + storage.invoke('saveRemote', entity, { + editor: that, + + // Successfully saved without validation errors. + success: function (model) { + //editableEntity.setState('saved', predicate); + + // Now that the changes to this property have been saved, the saved + // attributes are now the "original" attributes. + entity._originalAttributes = entity._previousAttributes = _.clone(entity.attributes); + + // Get data necessary to rerender property before it is unavailable. + var updatedProperty = entity.get(predicate + '/rendered'); + var $propertyWrapper = editor.$el.closest('.edit-field'); + var $context = $propertyWrapper.parent(); + + //editableEntity.setState('candidate', predicate); + // Unset the property, because it will be parsed again from the DOM, iff + // its new value causes it to still be rendered. + entity.unset(predicate, { silent: true }); + entity.unset(predicate + '/rendered', { silent: true }); + // Trigger event to allow for proper clean-up of editor-specific views. + editor.$el.trigger('destroyedPropertyEditor.edit', editor); + + // Replace the old content with the new content. + $propertyWrapper.replaceWith(updatedProperty); + Drupal.attachBehaviors($context); + }, + + // Save attempted but failed due to validation errors. + error: function (validationErrorMessages) { + editableEntity.setState('invalid', predicate); + + if (that.editorName === 'form') { + editor.$formContainer + .find('.edit-form') + .addClass('edit-validation-error') + .find('form') + .prepend(validationErrorMessages); + } + else { + var $errors = $('
    ') + .append(validationErrorMessages); + editor.element + .addClass('edit-validation-error') + .after($errors); + } + } + }); + }, + + /** + * Makes this PropertyEditor widget react to state changes. + */ + stateChange: function(model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + case 'candidate': + if (from !== 'inactive') { + this.disable(); + } + break; + case 'highlighted': + break; + case 'activating': + this.enable(); + break; + case 'active': + break; + case 'changed': + break; + case 'saving': + this.save(); + break; + case 'saved': + break; + case 'invalid': + break; + } + } +}); + +})(jQuery, Drupal); diff --git a/core/modules/edit/js/models/edit-app-model.js b/core/modules/edit/js/models/edit-app-model.js deleted file mode 100644 index 0c90fd0..0000000 --- a/core/modules/edit/js/models/edit-app-model.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @file - * A Backbone Model that models the current Edit application state. - */ -(function(Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.models = Drupal.edit.models || {}; -Drupal.edit.models.EditAppModel = Backbone.Model.extend({ - defaults: { - activeEntity: null, - highlightedEditor: null, - activeEditor: null, - // Reference to a ModalView-instance if a transition requires confirmation. - activeModal: null - } -}); - -})(Backbone, Drupal); diff --git a/core/modules/edit/js/storage.js b/core/modules/edit/js/storage.js new file mode 100644 index 0000000..196337e --- /dev/null +++ b/core/modules/edit/js/storage.js @@ -0,0 +1,518 @@ +/** + * @file + * Subclasses jQuery.Midgard.midgardStorage to have consistent namespaces. + */ +(function($, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +Drupal.edit.Storage = function (options) { + $.extend(this, { + saveEnabled: true, + // VIE instance to use for storage handling + vie: null, + // Whether to use localstorage + localStorage: false, + removeLocalstorageOnIgnore: true, + // URL callback for Backbone.sync + url: '', + // Whether to enable automatic saving + autoSave: false, + // How often to autosave in milliseconds + autoSaveInterval: 5000, + // Whether to save entities that are referenced by entities + // we're saving to the server. + saveReferencedNew: false, + saveReferencedChanged: false, + // Namespace used for events from midgardEditable-derived widget + editableNs: 'midgardeditable', + // CSS selector for the Edit button, leave to null to not bind + // notifications to any element + editSelector: '#midgardcreate-edit a', + localize: function (id, language) { + return window.midgardCreate.localize(id, language); + }, + language: null + }, options); + this._create(); +}; + +$.extend(Drupal.edit.Storage.prototype, { + + invoke: function(method) { + var args = Array.prototype.slice.call(arguments, 1); + if (method in this && typeof this[method] === 'function') { + return this[method].apply(this, args); + } + }, + _create: function () { + var widget = this; + this.changedModels = []; + + if (window.localStorage) { + this.localStorage = true; + } + + this.vie.entities.on('add', function (model) { + // Add the back-end URL used by Backbone.sync + model.url = widget.url; + model.toJSON = model.toJSONLD; + }); + + widget._bindEditables(); + if (widget.autoSave) { + widget._autoSave(); + } + }, + + _autoSave: function () { + var widget = this; + widget.saveEnabled = true; + + var doAutoSave = function () { + if (!widget.saveEnabled) { + return; + } + + if (widget.changedModels.length === 0) { + return; + } + + widget.saveRemoteAll({ + // We make autosaves silent so that potential changes from server + // don't disrupt user while writing. + silent: true + }); + }; + + var timeout = window.setInterval(doAutoSave, widget.options.autoSaveInterval); + + this.element.on('startPreventSave', function () { + if (timeout) { + window.clearInterval(timeout); + timeout = null; + } + widget.disableAutoSave(); + }); + this.element.on('stopPreventSave', function () { + if (!timeout) { + timeout = window.setInterval(doAutoSave, widget.options.autoSaveInterval); + } + widget.enableAutoSave(); + }); + + }, + + enableAutoSave: function () { + this.saveEnabled = true; + }, + + disableAutoSave: function () { + this.saveEnabled = false; + }, + + _bindEditables: function () { + var widget = this; + this.restorables = []; + var restorer; + + widget.element.on(widget.editableNs + 'changed', function (event, options) { + if (_.indexOf(widget.changedModels, options.instance) === -1) { + widget.changedModels.push(options.instance); + } + widget._saveLocal(options.instance); + }); + + widget.element.on(widget.editableNs + 'disable', function (event, options) { + widget.revertChanges(options.instance); + }); + + widget.element.on(widget.editableNs + 'enable', function (event, options) { + if (!options.instance._originalAttributes) { + options.instance._originalAttributes = _.clone(options.instance.attributes); + } + + if (!options.instance.isNew() && widget._checkLocal(options.instance)) { + // We have locally-stored modifications, user needs to be asked + widget.restorables.push(options.instance); + } + + /*_.each(options.instance.attributes, function (attributeValue, property) { + if (attributeValue instanceof widget.vie.Collection) { + widget._readLocalReferences(options.instance, property, attributeValue); + } + });*/ + }); + + widget.element.on('midgardcreatestatechange', function (event, options) { + if (options.state === 'browse' || widget.restorables.length === 0) { + widget.restorables = []; + if (restorer) { + restorer.close(); + } + return; + } + restorer = widget.checkRestore(); + }); + + widget.element.on('midgardstorageloaded', function (event, options) { + if (_.indexOf(widget.changedModels, options.instance) === -1) { + widget.changedModels.push(options.instance); + } + }); + }, + + checkRestore: function () { + var widget = this; + if (widget.restorables.length === 0) { + return; + } + + var message; + var restorer; + if (widget.restorables.length === 1) { + message = _.template(widget.options.localize('localModification', widget.options.language), { + label: widget.restorables[0].getSubjectUri() + }); + } else { + message = _.template(widget.options.localize('localModifications', widget.options.language), { + number: widget.restorables.length + }); + } + + var doRestore = function (event, notification) { + widget.restoreLocalAll(); + restorer.close(); + }; + + var doIgnore = function (event, notification) { + widget.ignoreLocal(); + restorer.close(); + }; + + restorer = jQuery('body').midgardNotifications('create', { + bindTo: widget.options.editSelector, + gravity: 'TR', + body: message, + timeout: 0, + actions: [ + { + name: 'restore', + label: widget.options.localize('Restore', widget.options.language), + cb: doRestore, + className: 'create-ui-btn' + }, + { + name: 'ignore', + label: widget.options.localize('Ignore', widget.options.language), + cb: doIgnore, + className: 'create-ui-btn' + } + ], + callbacks: { + beforeShow: function () { + if (!window.Mousetrap) { + return; + } + window.Mousetrap.bind(['command+shift+r', 'ctrl+shift+r'], function (event) { + event.preventDefault(); + doRestore(); + }); + window.Mousetrap.bind(['command+shift+i', 'ctrl+shift+i'], function (event) { + event.preventDefault(); + doIgnore(); + }); + }, + afterClose: function () { + if (!window.Mousetrap) { + return; + } + window.Mousetrap.unbind(['command+shift+r', 'ctrl+shift+r']); + window.Mousetrap.unbind(['command+shift+i', 'ctrl+shift+i']); + } + } + }); + return restorer; + }, + + restoreLocalAll: function () { + _.each(this.restorables, function (instance) { + this.readLocal(instance); + }, this); + this.restorables = []; + }, + + ignoreLocal: function () { + if (this.options.removeLocalstorageOnIgnore) { + _.each(this.restorables, function (instance) { + this._removeLocal(instance); + }, this); + } + this.restorables = []; + }, + + saveReferences: function (model) { + _.each(model.attributes, function (value, property) { + if (!value || !value.isCollection) { + return; + } + + value.each(function (referencedModel) { + if (this.changedModels.indexOf(referencedModel) !== -1) { + // The referenced model is already in the save queue + return; + } + + if (referencedModel.isNew() && this.options.saveReferencedNew) { + return referencedModel.save(); + } + + if (referencedModel.hasChanged() && this.options.saveReferencedChanged) { + return referencedModel.save(); + } + }, this); + }, this); + }, + + saveRemote: function (model, options) { + // Optionally handle entities referenced in this model first + this.saveReferences(model); + + // this._trigger('saveentity', null, { + // entity: model, + // options: options + // }); + + var widget = this; + model.save(null, _.extend({}, options, { + success: function (m, response) { + // From now on we're going with the values we have on server + model._originalAttributes = _.clone(model.attributes); + widget._removeLocal(model); + window.setTimeout(function () { + // Remove the model from the list of changed models after saving + widget.changedModels.splice(widget.changedModels.indexOf(model), 1); + }, 0); + if (_.isFunction(options.success)) { + options.success(m, response); + } + // widget._trigger('savedentity', null, { + // entity: model, + // options: options + // }); + }, + error: function (m, response) { + if (_.isFunction(options.error)) { + options.error(m, response); + } + } + })); + }, + + saveRemoteAll: function (options) { + var widget = this; + if (widget.changedModels.length === 0) { + return; + } + + widget._trigger('save', null, { + entities: widget.changedModels, + options: options, + // Deprecated + models: widget.changedModels + }); + + var notification_msg; + var needed = widget.changedModels.length; + if (needed > 1) { + notification_msg = _.template(widget.options.localize('saveSuccessMultiple', widget.options.language), { + number: needed + }); + } else { + notification_msg = _.template(widget.options.localize('saveSuccess', widget.options.language), { + label: widget.changedModels[0].getSubjectUri() + }); + } + + widget.disableAutoSave(); + _.each(widget.changedModels, function (model) { + this.saveRemote(model, { + success: function (m, response) { + needed--; + if (needed <= 0) { + // All models were happily saved + widget._trigger('saved', null, { + options: options + }); + if (options && _.isFunction(options.success)) { + options.success(m, response); + } + jQuery('body').midgardNotifications('create', { + body: notification_msg + }); + widget.enableAutoSave(); + } + }, + error: function (m, err) { + if (options && _.isFunction(options.error)) { + options.error(m, err); + } + jQuery('body').midgardNotifications('create', { + body: _.template(widget.options.localize('saveError', widget.options.language), { + error: err.responseText || '' + }), + timeout: 0 + }); + + widget._trigger('error', null, { + instance: model + }); + } + }); + }, this); + }, + + _saveLocal: function (model) { + if (!this.options.localStorage) { + return; + } + + if (model.isNew()) { + // Anonymous object, save as refs instead + if (!model.primaryCollection) { + return; + } + return this._saveLocalReferences(model.primaryCollection.subject, model.primaryCollection.predicate, model); + } + window.localStorage.setItem(model.getSubjectUri(), JSON.stringify(model.toJSONLD())); + }, + + _getReferenceId: function (model, property) { + return model.id + ':' + property; + }, + + _saveLocalReferences: function (subject, predicate, model) { + if (!this.options.localStorage) { + return; + } + + if (!subject || !predicate) { + return; + } + + var widget = this; + var identifier = subject + ':' + predicate; + var json = model.toJSONLD(); + if (window.localStorage.getItem(identifier)) { + var referenceList = JSON.parse(window.localStorage.getItem(identifier)); + var index = _.pluck(referenceList, '@').indexOf(json['@']); + if (index !== -1) { + referenceList[index] = json; + } else { + referenceList.push(json); + } + window.localStorage.setItem(identifier, JSON.stringify(referenceList)); + return; + } + window.localStorage.setItem(identifier, JSON.stringify([json])); + }, + + _checkLocal: function (model) { + if (!this.options.localStorage) { + return false; + } + + var local = window.localStorage.getItem(model.getSubjectUri()); + if (!local) { + return false; + } + + return true; + }, + + hasLocal: function (model) { + if (!this.options.localStorage) { + return false; + } + + if (!window.localStorage.getItem(model.getSubjectUri())) { + return false; + } + return true; + }, + + readLocal: function (model) { + if (!this.options.localStorage) { + return; + } + + var local = window.localStorage.getItem(model.getSubjectUri()); + if (!local) { + return; + } + if (!model._originalAttributes) { + model._originalAttributes = _.clone(model.attributes); + } + var parsed = JSON.parse(local); + var entity = this.vie.entities.addOrUpdate(parsed, { + overrideAttributes: true + }); + + this._trigger('loaded', null, { + instance: entity + }); + }, + + _readLocalReferences: function (model, property, collection) { + if (!this.options.localStorage) { + return; + } + + var identifier = this._getReferenceId(model, property); + var local = window.localStorage.getItem(identifier); + if (!local) { + return; + } + collection.add(JSON.parse(local)); + }, + + revertChanges: function (model) { + var widget = this; + + // Remove unsaved collection members + if (!model) { return; } + _.each(model.attributes, function (attributeValue, property) { + if (attributeValue instanceof widget.vie.Collection) { + var removables = []; + attributeValue.forEach(function (model) { + if (model.isNew()) { + removables.push(model); + } + }); + attributeValue.remove(removables); + } + }); + + // Restore original object properties + if (!model.changedAttributes()) { + if (model._originalAttributes) { + model.set(model._originalAttributes); + } + return; + } + + model.set(model.previousAttributes()); + }, + + _removeLocal: function (model) { + if (!this.localStorage) { + return; + } + + window.localStorage.removeItem(model.getSubjectUri()); + } +}); + +})(jQuery, Drupal); diff --git a/core/modules/edit/js/viejs/EditService.js b/core/modules/edit/js/viejs/EditService.js deleted file mode 100644 index 00cb04b..0000000 --- a/core/modules/edit/js/viejs/EditService.js +++ /dev/null @@ -1,289 +0,0 @@ -/** - * @file - * VIE DOM parsing service for Edit. - */ -(function(jQuery, _, VIE, Drupal, drupalSettings) { - -"use strict"; - - VIE.prototype.EditService = function (options) { - var defaults = { - name: 'edit', - subjectSelector: '.edit-field.edit-allowed' - }; - this.options = _.extend({}, defaults, options); - - this.views = []; - this.vie = null; - this.name = this.options.name; - }; - - VIE.prototype.EditService.prototype = { - load: function (loadable) { - var correct = loadable instanceof this.vie.Loadable; - if (!correct) { - throw new Error('Invalid Loadable passed'); - } - - var element; - if (!loadable.options.element) { - if (typeof document === 'undefined') { - return loadable.resolve([]); - } else { - element = drupalSettings.edit.context; - } - } else { - element = loadable.options.element; - } - - var entities = this.readEntities(element); - loadable.resolve(entities); - }, - - _getViewForElement:function (element, collectionView) { - var viewInstance; - - jQuery.each(this.views, function () { - if (jQuery(this.el).get(0) === element.get(0)) { - if (collectionView && !this.template) { - return true; - } - viewInstance = this; - return false; - } - }); - return viewInstance; - }, - - _registerEntityView:function (entity, element, isNew) { - if (!element.length) { - return; - } - - // Let's only have this overhead for direct types. Form-based editors are - // handled in backbone.drupalform.js and the PropertyEditor instance. - if (jQuery(element).hasClass('edit-type-form')) { - return; - } - - var service = this; - var viewInstance = this._getViewForElement(element); - if (viewInstance) { - return viewInstance; - } - - viewInstance = new this.vie.view.Entity({ - model:entity, - el:element, - tagName:element.get(0).nodeName, - vie:this.vie, - service:this.name - }); - - this.views.push(viewInstance); - - return viewInstance; - }, - - save: function(saveable) { - var correct = saveable instanceof this.vie.Savable; - if (!correct) { - throw "Invalid Savable passed"; - } - - if (!saveable.options.element) { - // FIXME: we could find element based on subject - throw "Unable to write entity to edit.module-markup, no element given"; - } - - if (!saveable.options.entity) { - throw "Unable to write to edit.module-markup, no entity given"; - } - - var $element = jQuery(saveable.options.element); - this._writeEntity(saveable.options.entity, saveable.options.element); - saveable.resolve(); - }, - - _writeEntity:function (entity, element) { - var service = this; - this.findPredicateElements(this.getElementSubject(element), element, true).each(function () { - var predicateElement = jQuery(this); - var predicate = service.getElementPredicate(predicateElement); - if (!entity.has(predicate)) { - return true; - } - - var value = entity.get(predicate); - if (value && value.isCollection) { - // Handled by CollectionViews separately - return true; - } - if (value === service.readElementValue(predicate, predicateElement)) { - return true; - } - // Unlike in the VIE's RdfaService no (re-)mapping needed here. - predicateElement.html(value); - }); - return true; - }, - - // The edit-id data attribute contains the full identifier of - // each entity element in the format - // `::::`. - _getID: function (element) { - var id = jQuery(element).attr('data-edit-id'); - if (!id) { - id = jQuery(element).closest('[data-edit-id]').attr('data-edit-id'); - } - return id; - }, - - // Returns the "URI" of an entity of an element in format - // `/`. - getElementSubject: function (element) { - return this._getID(element).split('/').slice(0, 2).join('/'); - }, - - // Returns the field name for an element in format - // `//`. - // (Slashes instead of colons because the field name is no namespace.) - getElementPredicate: function (element) { - if (!this._getID(element)) { - throw new Error('Could not find predicate for element'); - } - return this._getID(element).split('/').slice(2, 5).join('/'); - }, - - getElementType: function (element) { - return this._getID(element).split('/').slice(0, 1)[0]; - }, - - // Reads all editable entities (currently each Drupal field is considered an - // entity, in the future Drupal entities should be mapped to VIE entities) - // from DOM and returns the VIE enties it found. - readEntities: function (element) { - var service = this; - var entities = []; - var entityElements = jQuery(this.options.subjectSelector, element); - entityElements = entityElements.add(jQuery(element).filter(this.options.subjectSelector)); - entityElements.each(function () { - var entity = service._readEntity(jQuery(this)); - if (entity) { - entities.push(entity); - } - }); - return entities; - }, - - // Returns a filled VIE Entity instance for a DOM element. The Entity - // is also registered in the VIE entities collection. - _readEntity: function (element) { - var subject = this.getElementSubject(element); - var type = this.getElementType(element); - var entity = this._readEntityPredicates(subject, element, false); - if (jQuery.isEmptyObject(entity)) { - return null; - } - entity['@subject'] = subject; - if (type) { - entity['@type'] = this._registerType(type, element); - } - - var entityInstance = new this.vie.Entity(entity); - entityInstance = this.vie.entities.addOrUpdate(entityInstance, { - updateOptions: { - silent: true, - ignoreChanges: true - } - }); - - this._registerEntityView(entityInstance, element); - return entityInstance; - }, - - _registerType: function (typeId, element) { - typeId = ''; - var type = this.vie.types.get(typeId); - if (!type) { - this.vie.types.add(typeId, []); - type = this.vie.types.get(typeId); - } - - var predicate = this.getElementPredicate(element); - if (type.attributes.get(predicate)) { - return type; - } - var range = predicate.split('/')[0]; - type.attributes.add(predicate, [range], 0, 1, { - label: element.data('edit-field-label') - }); - - return type; - }, - - _readEntityPredicates: function (subject, element, emptyValues) { - var entityPredicates = {}; - var service = this; - this.findPredicateElements(subject, element, true).each(function () { - var predicateElement = jQuery(this); - var predicate = service.getElementPredicate(predicateElement); - if (!predicate) { - return; - } - var value = service.readElementValue(predicate, predicateElement); - if (value === null && !emptyValues) { - return; - } - - entityPredicates[predicate] = value; - entityPredicates[predicate + '/rendered'] = predicateElement[0].outerHTML; - }); - return entityPredicates; - }, - - readElementValue : function(predicate, element) { - // Unlike in RdfaService there is parsing needed here. - if (element.hasClass('edit-type-form')) { - return undefined; - } - else { - return jQuery.trim(element.html()); - } - }, - - // Subject elements are the DOM elements containing a single or multiple - // editable fields. - findSubjectElements: function (element) { - if (!element) { - element = drupalSettings.edit.context; - } - return jQuery(this.options.subjectSelector, element); - }, - - // Predicate Elements are the actual DOM elements that users will be able - // to edit. - findPredicateElements: function (subject, element, allowNestedPredicates, stop) { - var predicates = jQuery(); - // Make sure that element is wrapped by jQuery. - var $element = jQuery(element); - - // Form-type predicates - predicates = predicates.add($element.filter('.edit-type-form')); - - // Direct-type predicates - var direct = $element.filter('.edit-type-direct'); - predicates = predicates.add(direct.find('.field-item')); - - if (!predicates.length && !stop) { - var parentElement = $element.parent(this.options.subjectSelector); - if (parentElement.length) { - return this.findPredicateElements(subject, parentElement, allowNestedPredicates, true); - } - } - - return predicates; - } - }; - -})(jQuery, _, VIE, Drupal, drupalSettings); diff --git a/core/modules/edit/js/views/contextuallink-view.js b/core/modules/edit/js/views/contextuallink-view.js index efe8ddd..7de0472 100644 --- a/core/modules/edit/js/views/contextuallink-view.js +++ b/core/modules/edit/js/views/contextuallink-view.js @@ -6,14 +6,12 @@ "use strict"; -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ +Drupal.edit.ContextualLinkView = Backbone.View.extend({ entity: null, events: { - 'click': 'onClick' + 'click .quick-edit a': 'onClick' }, /** @@ -21,19 +19,26 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ * * @param options * An object with the following keys: - * - entity: the entity ID (e.g. node/1) of the entity + * - appModel: the application state model + * - strings: the strings for the "Quick edit" link */ initialize: function (options) { - this.entity = options.entity; + this.appModel = options.appModel; + 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:activeEntity', this.render, this); + // Re-render whenever this entity's isActive attribute changes. + this.model.on('change:isActive', this.render, this); // Hide the contextual links whenever an in-place editor is active. - this.model.on('change:activeEditor', this.toggleContextualLinksVisibility, this); + this.appModel.on('change:activeEditor', this.toggleContextualLinksVisibility, this); }, /** @@ -49,13 +54,20 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ 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); + if (that.model.get('isActive') === true) { + that.model.set('isActive', false); } // 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); + // Stop editing the currently active entity, if any. + var currentlyActiveEntity = that.model.collection.where({ isActive: true }); + if (currentlyActiveEntity.length > 0) { + currentlyActiveEntity[0].set('isActive', false); + } + + // Start the editing of this entity. + that.model.set('isActive', true); } }; @@ -65,6 +77,8 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ // state, it is possible to stop editing it.) var activeEditor = this.model.get('activeEditor'); if (activeEditor) { + debugger; + // @todo this branch has not yet been updated. var editableEntity = activeEditor.options.widget; var predicate = activeEditor.options.property; editableEntity.setState('candidate', predicate, { reason: 'stop or switch' }, function(accepted) { @@ -86,9 +100,8 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ * 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 + ''); + var isActive = this.model.get('isActive'); + this.$el.find('.quick-edit a').text((!isActive) ? this.strings.quickEdit : this.strings.stopQuickEdit); return this; }, diff --git a/core/modules/edit/js/views/modal-view.js b/core/modules/edit/js/views/modal-view.js index b98c876..51ec5ce 100644 --- a/core/modules/edit/js/views/modal-view.js +++ b/core/modules/edit/js/views/modal-view.js @@ -6,9 +6,7 @@ "use strict"; -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ModalView = Backbone.View.extend({ +Drupal.edit.ModalView = Backbone.View.extend({ message: null, buttons: null, diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js index bee33d1..afef6c1 100644 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ b/core/modules/edit/js/views/propertyeditordecoration-view.js @@ -8,10 +8,7 @@ "use strict"; -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ - +Drupal.edit.PropertyEditorDecorationView = Backbone.View.extend({ toolbarId: null, _widthAttributeIsEmpty: null, @@ -37,20 +34,18 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. */ initialize: function(options) { + this.model.on('change:state', this.stateChange, this); + 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) { + stateChange: function(model, state) { + var from = model.previous('state'); + var to = state; switch (to) { case 'inactive': if (from !== null) { @@ -108,8 +103,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ onMouseEnter: function(event) { var that = this; this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { - var editableEntity = that.editor.options.widget; - editableEntity.setState('highlighted', that.predicate); + that.model.setState('highlighted'); event.stopPropagation(); }); }, @@ -122,8 +116,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ 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' }); + that.model.setState('candidate', { reason: 'mouseleave' }); event.stopPropagation(); }); }, @@ -134,10 +127,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ * @param event */ onClick: function(event) { - var editableEntity = this.editor.options.widget; - editableEntity.setState('activating', this.predicate); - event.preventDefault(); - event.stopPropagation(); + this.model.set('isActive', true); }, decorate: function () { @@ -148,7 +138,6 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ undecorate: function () { this.$el .removeClass('edit-candidate edit-editable edit-highlighted edit-editing'); - this.undelegateEvents(); }, startHighlight: function () { diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js index f4b2123..79455f1 100644 --- a/core/modules/edit/js/views/toolbar-view.js +++ b/core/modules/edit/js/views/toolbar-view.js @@ -9,16 +9,8 @@ "use strict"; -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ToolbarView = Backbone.View.extend({ - +Drupal.edit.ToolbarView = Backbone.View.extend({ editor: null, - $storageWidgetEl: null, - - entity: null, - predicate : null, - editorName: null, _loader: null, _loaderVisibleStart: 0, @@ -34,41 +26,56 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ /** * 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, '_'); + // Generate a DOM-compatible ID for the form container DOM element. + this.elementId = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_'); + + this.model.on('change:state', this.stateChange, this); + }, + + /** + * 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.elementId + }))); + + // Insert in DOM. + var $fieldElement = this.editor.$el; + if ($fieldElement.css('display') === 'inline') { + this.$el.prependTo($fieldElement.offsetParent()); + var pos = $fieldElement.position(); + this.$el.css('left', pos.left).css('top', pos.top); + } + else { + this.$el.insertBefore($fieldElement); + } + + return this; }, /** * Listens to editor state changes. */ - stateChange: function(from, to) { + stateChange: function(model, state) { + var from = model.previous('state'); + var to = state; switch (to) { case 'inactive': if (from) { this.remove(); - if (this.editorName !== 'form') { + if (this.model.get('editor') !== 'form') { Backbone.syncDirectCleanUp(); } } @@ -78,7 +85,7 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ this.render(); } else { - if (this.editorName !== 'form') { + if (this.model.get('editor') !== 'form') { Backbone.syncDirectCleanUp(); } // Remove all toolgroups; they're no longer necessary. @@ -119,7 +126,6 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ break; case 'saving': this.setLoadingIndicator(true); - this.save(); break; case 'saved': this.setLoadingIndicator(false); @@ -131,76 +137,6 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ }, /** - * 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. * @@ -233,9 +169,7 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ * @param event */ onClickSave: function(event) { - event.stopPropagation(); - event.preventDefault(); - this.editor.options.widget.setState('saving', this.predicate); + this.model.set('state', 'saving'); }, /** @@ -280,12 +214,8 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ }, 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; - } + // Retrieve the lavel to show for this field. + var label = this.model.get('label'); this.$el .addClass('edit-highlighted') @@ -388,29 +318,6 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ }, /** - * 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. 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. diff --git a/core/modules/system/system.module b/core/modules/system/system.module index eee53b1..4185d43 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2108,20 +2108,6 @@ function system_library_info() { ), ); - // Create. - $libraries['create.editonly'] = array( - 'title' => 'Create.js edit-only (editing features only)', - 'website' => 'http://backbonejs.org/', - 'version' => '1.0.0-dev', - 'js' => array( - 'core/misc/create/create-editonly.js' => array('group' => JS_LIBRARY), - ), - 'dependencies' => array( - array('system', 'vie.core'), - array('system', 'jquery.ui.widget'), - ), - ); - // Cookie. $libraries['jquery.cookie'] = array( 'title' => 'Cookie',