core/misc/create/create-editonly.js | 1651 --------- core/misc/vie/vie-core.js | 3719 -------------------- core/modules/edit/edit.module | 50 +- core/modules/edit/js/app.js | 391 -- core/modules/edit/js/backbone.drupalform.js | 184 - 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 | 436 ++- core/modules/edit/js/editors/directEditor.js | 107 + core/modules/edit/js/editors/formEditor.js | 187 + core/modules/edit/js/models/AppModel.js | 22 + core/modules/edit/js/models/EntityModel.js | 49 + core/modules/edit/js/models/FieldModel.js | 132 + core/modules/edit/js/models/edit-app-model.js | 21 - core/modules/edit/js/util.js | 6 +- core/modules/edit/js/viejs/EditService.js | 289 -- core/modules/edit/js/views/AppView.js | 384 ++ core/modules/edit/js/views/ContextualLinkView.js | 74 + core/modules/edit/js/views/EditorDecorationView.js | 372 ++ core/modules/edit/js/views/EditorView.js | 263 ++ core/modules/edit/js/views/FieldToolbarView.js | 408 +++ core/modules/edit/js/views/ModalView.js | 80 + core/modules/edit/js/views/contextuallink-view.js | 117 - core/modules/edit/js/views/modal-view.js | 83 - .../edit/js/views/propertyeditordecoration-view.js | 363 -- core/modules/edit/js/views/toolbar-view.js | 490 --- core/modules/edit/lib/Drupal/edit/EditorBase.php | 2 +- .../edit/lib/Drupal/edit/EditorSelector.php | 20 +- .../edit/lib/Drupal/edit/MetadataGenerator.php | 2 +- .../edit/lib/Drupal/edit/Plugin/EditorManager.php | 2 +- .../edit/Plugin/edit/editor/DirectEditor.php | 2 +- .../Drupal/edit/Plugin/edit/editor/FormEditor.php | 2 +- .../edit/lib/Drupal/edit/Tests/EditLoadingTest.php | 9 +- .../lib/Drupal/edit/Tests/EditorSelectionTest.php | 5 +- .../Drupal/edit/Tests/MetadataGeneratorTest.php | 5 +- .../edit_test/Plugin/edit/editor/WysiwygEditor.php | 2 +- core/modules/editor/editor.module | 8 +- core/modules/editor/js/editor.createjs.js | 157 - .../editor/js/editor.formattedTextEditor.js | 181 + core/modules/editor/js/editor.js | 8 +- .../Drupal/editor/Plugin/edit/editor/Editor.php | 6 +- .../Drupal/editor/Tests/EditIntegrationTest.php | 4 +- core/modules/system/system.module | 14 - 45 files changed, 2627 insertions(+), 7956 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/misc/vie/vie-core.js b/core/misc/vie/vie-core.js deleted file mode 100644 index 4e1526e..0000000 --- a/core/misc/vie/vie-core.js +++ /dev/null @@ -1,3719 +0,0 @@ -/*Copyright (c) 2011 Henri Bergius, IKS Consortium -Copyright (c) 2011 Sebastian Germesin, IKS Consortium - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. -*/(function(){// VIE - Vienna IKS Editables -// (c) 2011 Henri Bergius, IKS Consortium -// (c) 2011 Sebastian Germesin, IKS Consortium -// (c) 2011 Szaby Grünwald, IKS Consortium -// VIE may be freely distributed under the MIT license. -// For all details and documentation: -// http://viejs.org/ - -/*global console:false exports:false require:false */ - -var root = this, - jQuery = root.jQuery, - Backbone = root.Backbone, - _ = root._; - - -// ## VIE constructor -// -// The VIE constructor is the way to initialize VIE for your -// application. The instance of VIE handles all management of -// semantic interaction, including keeping track of entities, -// changes to them, the possible RDFa views on the page where -// the entities are displayed, and connections to external -// services like Stanbol and DBPedia. -// -// To get a VIE instance, simply run: -// -// var vie = new VIE(); -// -// You can also pass configurations to the VIE instance through -// the constructor. For example, to set a different default -// namespace to be used for names that don't have a namespace -// specified, do: -// -// var vie = new VIE({ -// baseNamespace: 'http://example.net' -// }); -// -// ### Differences with VIE 1.x -// -// VIE 1.x used singletons for managing entities and views loaded -// from a page. This has been changed with VIE 2.x, and now all -// data managed by VIE is tied to the instance of VIE being used. -// -// This means that VIE needs to be instantiated before using. So, -// when previously you could get entities from page with: -// -// VIE.RDFaEntities.getInstances(); -// -// Now you need to instantiate VIE first. This example uses the -// Classic API compatibility layer instead of the `load` method: -// -// var vie = new VIE(); -// vie.RDFaEntities.getInstances(); -// -// Currently the Classic API is enabled by default, but it is -// recommended to ensure it is enabled before using it. So: -// -// var vie = new VIE({classic: true}); -// vie.RDFaEntities.getInstances(); -var VIE = root.VIE = function(config) { - this.config = (config) ? config : {}; - this.services = {}; - this.jQuery = jQuery; - this.entities = new this.Collection([], { - vie: this - }); - - this.Entity.prototype.entities = this.entities; - this.Entity.prototype.entityCollection = this.Collection; - this.Entity.prototype.vie = this; - - this.Namespaces.prototype.vie = this; -// ### Namespaces in VIE -// VIE supports different ontologies and an easy use of them. -// Namespace prefixes reduce the amount of code you have to -// write. In VIE, it does not matter if you access an entitie's -// property with -// `entity.get('')` or -// `entity.get('dbprop:capitalOf')` or even -// `entity.get('capitalOf')` once the corresponding namespace -// is registered as *baseNamespace*. -// By default `"http://viejs.org/ns/"`is set as base namespace. -// For more information about how to set, get and list all -// registered namespaces, refer to the -// Namespaces documentation. - this.namespaces = new this.Namespaces( - (this.config.baseNamespace) ? this.config.baseNamespace : "http://viejs.org/ns/", - -// By default, VIE is shipped with common namespace prefixes: - -// + owl : "http://www.w3.org/2002/07/owl#" -// + rdfs : "http://www.w3.org/2000/01/rdf-schema#" -// + rdf : "http://www.w3.org/1999/02/22-rdf-syntax-ns#" -// + schema : 'http://schema.org/' -// + foaf : 'http://xmlns.com/foaf/0.1/' -// + geo : 'http://www.w3.org/2003/01/geo/wgs84_pos#' -// + dbpedia: "http://dbpedia.org/ontology/" -// + dbprop : "http://dbpedia.org/property/" -// + skos : "http://www.w3.org/2004/02/skos/core#" -// + xsd : "http://www.w3.org/2001/XMLSchema#" -// + sioc : "http://rdfs.org/sioc/ns#" -// + dcterms: "http://purl.org/dc/terms/" - { - owl : "http://www.w3.org/2002/07/owl#", - rdfs : "http://www.w3.org/2000/01/rdf-schema#", - rdf : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - schema : 'http://schema.org/', - foaf : 'http://xmlns.com/foaf/0.1/', - geo : 'http://www.w3.org/2003/01/geo/wgs84_pos#', - dbpedia: "http://dbpedia.org/ontology/", - dbprop : "http://dbpedia.org/property/", - skos : "http://www.w3.org/2004/02/skos/core#", - xsd : "http://www.w3.org/2001/XMLSchema#", - sioc : "http://rdfs.org/sioc/ns#", - dcterms: "http://purl.org/dc/terms/" - } - ); - - - this.Type.prototype.vie = this; - this.Types.prototype.vie = this; - this.Attribute.prototype.vie = this; - this.Attributes.prototype.vie = this; -// ### Type hierarchy in VIE -// VIE takes care about type hierarchy of entities -// (aka. *schema* or *ontology*). -// Once a type hierarchy is known to VIE, we can leverage -// this information, to easily ask, whether an entity -// is of type, e.g., *foaf:Person* or *schema:Place*. -// For more information about how to generate such a type -// hierarchy, refer to the -// Types documentation. - this.types = new this.Types(); -// By default, there is a parent type in VIE, called -// *owl:Thing*. All types automatically inherit from this -// type and all registered entities, are of this type. - this.types.add("owl:Thing"); - -// As described above, the Classic API of VIE 1.x is loaded -// by default. As this might change in the future, it is -// recommended to ensure it is enabled before using it. So: -// -// var vie = new VIE({classic: true}); -// vie.RDFaEntities.getInstances(); - if (this.config.classic === true) { - /* Load Classic API as well */ - this.RDFa = new this.ClassicRDFa(this); - this.RDFaEntities = new this.ClassicRDFaEntities(this); - this.EntityManager = new this.ClassicEntityManager(this); - - this.cleanup = function() { - this.entities.reset(); - }; - } -}; - -// ### use(service, name) -// This method registers services within VIE. -// **Parameters**: -// *{string|object}* **service** The service to be registered. -// *{string}* **name** An optional name to register the service with. If this -// is not set, the default name that comes with the service is taken. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE}* : The current VIE instance. -// **Example usage**: -// -// var vie = new VIE(); -// var conf1 = {...}; -// var conf2 = {...}; -// vie.use(new vie.StanbolService()); -// vie.use(new vie.StanbolService(conf1), "stanbol_1"); -// vie.use(new vie.StanbolService(conf2), "stanbol_2"); -// // <-- this means that there are now 3 services registered! -VIE.prototype.use = function(service, name) { - if (!name && !service.name) { - throw new Error("Please provide a name for the service!"); - } - service.vie = this; - service.name = (name)? name : service.name; - if (service.init) { - service.init(); - } - this.services[service.name] = service; - - return this; -}; - -// ### service(name) -// This method returns the service object that is -// registered under the given name. -// **Parameters**: -// *{string}* **name** ... -// **Throws**: -// *{Error}* if no service could be found. -// **Returns**: -// *{object}* : The service to be queried. -// **Example usage**: -// -// var vie = new VIE(); -// vie.use(new vie.StanbolService(), "stanbol"); -// var service = vie.service("stanbol"); -VIE.prototype.service = function(name) { - if (!this.hasService(name)) { - throw "Undefined service " + name; - } - return this.services[name]; -}; - -// ### hasService(name) -// This method returns a boolean telling whether VIE has a particular -// service loaded. -// **Parameters**: -// *{string}* **name** -// **Returns**: -// *{boolean}* whether service is available -VIE.prototype.hasService = function(name) { - if (!this.services[name]) { - return false; - } - return true; -}; - -// ### getServicesArray() -// This method returns an array of all registered services. -// **Parameters**: -// *nothing* -// **Throws**: -// *nothing* -// **Returns**: -// *{array}* : An array of service instances. -// **Example usage**: -// -// var vie = new VIE(); -// vie.use(new vie.StanbolService(), "stanbol"); -// var services = vie.getServicesArray(); -// services.length; // <-- 1 -VIE.prototype.getServicesArray = function() { - return _.map(this.services, function (v) {return v;}); -}; - -// ### load(options) -// This method instantiates a new VIE.Loadable in order to -// perform queries on the services. -// **Parameters**: -// *{object}* **options** Options to be set. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Loadable}* : A new instance of VIE.Loadable. -// **Example usage**: -// -// var vie = new VIE(); -// vie.use(new vie.StanbolService(), "stanbol"); -// var loader = vie.load({...}); -VIE.prototype.load = function(options) { - if (!options) { options = {}; } - options.vie = this; - return new this.Loadable(options); -}; - -// ### save(options) -// This method instantiates a new VIE.Savable in order to -// perform queries on the services. -// **Parameters**: -// *{object}* **options** Options to be set. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Savable}* : A new instance of VIE.Savable. -// **Example usage**: -// -// var vie = new VIE(); -// vie.use(new vie.StanbolService(), "stanbol"); -// var saver = vie.save({...}); -VIE.prototype.save = function(options) { - if (!options) { options = {}; } - options.vie = this; - return new this.Savable(options); -}; - -// ### remove(options) -// This method instantiates a new VIE.Removable in order to -// perform queries on the services. -// **Parameters**: -// *{object}* **options** Options to be set. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Removable}* : A new instance of VIE.Removable. -// **Example usage**: -// -// var vie = new VIE(); -// vie.use(new vie.StanbolService(), "stanbol"); -// var remover = vie.remove({...}); -VIE.prototype.remove = function(options) { - if (!options) { options = {}; } - options.vie = this; - return new this.Removable(options); -}; - -// ### analyze(options) -// This method instantiates a new VIE.Analyzable in order to -// perform queries on the services. -// **Parameters**: -// *{object}* **options** Options to be set. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Analyzable}* : A new instance of VIE.Analyzable. -// **Example usage**: -// -// var vie = new VIE(); -// vie.use(new vie.StanbolService(), "stanbol"); -// var analyzer = vie.analyze({...}); -VIE.prototype.analyze = function(options) { - if (!options) { options = {}; } - options.vie = this; - return new this.Analyzable(options); -}; - -// ### find(options) -// This method instantiates a new VIE.Findable in order to -// perform queries on the services. -// **Parameters**: -// *{object}* **options** Options to be set. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Findable}* : A new instance of VIE.Findable. -// **Example usage**: -// -// var vie = new VIE(); -// vie.use(new vie.StanbolService(), "stanbol"); -// var finder = vie.find({...}); -VIE.prototype.find = function(options) { - if (!options) { options = {}; } - options.vie = this; - return new this.Findable(options); -}; - -// ### loadSchema(url, options) -// VIE only knows the *owl:Thing* type by default. -// You can use this method to import another -// schema (ontology) from an external resource. -// (Currently, this supports only the JSON format!!) -// As this method works asynchronously, you might want -// to register `success` and `error` callbacks via the -// options. -// **Parameters**: -// *{string}* **url** The url, pointing to the schema to import. -// *{object}* **options** Options to be set. -// (Set ```success``` and ```error``` as callbacks.). -// **Throws**: -// *{Error}* if the url is not set. -// **Returns**: -// *{VIE}* : The VIE instance itself. -// **Example usage**: -// -// var vie = new VIE(); -// vie.loadSchema("http://schema.rdfs.org/all.json", -// { -// baseNS : "http://schema.org/", -// success : function () {console.log("success");}, -// error : function (msg) {console.warn(msg);} -// }); -VIE.prototype.loadSchema = function(url, options) { - options = (!options)? {} : options; - - if (!url) { - throw new Error("Please provide a proper URL"); - } - else { - var vie = this; - jQuery.getJSON(url) - .success(function(data) { - try { - VIE.Util.loadSchemaOrg(vie, data, options.baseNS); - if (options.success) { - options.success.call(vie); - } - } catch (e) { - options.error.call(vie, e); - return; - } - }) - .error(function(data, textStatus, jqXHR) { - if (options.error) { - console.warn(data, textStatus, jqXHR); - options.error.call(vie, "Could not load schema from URL (" + url + ")"); - } - }); - } - - return this; -}; - -// ### getTypedEntityClass(type) -// This method generates a special type of `Entity` based on the given type. -// **Parameters**: -// *{string}* **type** The type. -// **Throws**: -// *{Error}* if the type is unknown to VIE. -// **Returns**: -// *{VIE.Entity}* : A subclass of `VIE.Entity`. -// **Example usage**: -// -// var vie = new VIE(); -// vie.types.add("Person"); -// var PersonClass = vie.getTypedEntityClass("Person"); -// var Person = new PersonClass({"name", "Sebastian"}); -VIE.prototype.getTypedEntityClass = function (type) { - var typeType = this.types.get(type); - if (!typeType) { - throw new Error("Unknown type " + type); - } - var TypedEntityClass = function (attrs, opts) { - if (!attrs) { - attrs = {}; - } - attrs["@type"] = type; - this.set(attrs, opts); - }; - TypedEntityClass.prototype = new this.Entity(); - TypedEntityClass.prototype.schema = function () { - return VIE.Util.getFormSchemaForType(typeType); - }; - return TypedEntityClass; -}; - -// ## Running VIE on Node.js -// -// When VIE is running under Node.js we can use the CommonJS -// require interface to load our dependencies automatically. -// -// This means Node.js users don't need to care about dependencies -// and can just run VIE with: -// -// var VIE = require('vie'); -// -// In browser environments the dependencies have to be included -// before including VIE itself. -if (typeof exports === 'object') { - exports.VIE = VIE; - - if (!jQuery) { - jQuery = require('jquery'); - } - if (!Backbone) { - Backbone = require('backbone'); - Backbone.setDomLibrary(jQuery); - } - if (!_) { - _ = require('underscore')._; - } -} -// VIE - Vienna IKS Editables -// (c) 2011 Henri Bergius, IKS Consortium -// (c) 2011 Sebastian Germesin, IKS Consortium -// (c) 2011 Szaby Grünwald, IKS Consortium -// VIE may be freely distributed under the MIT license. -// For all details and documentation: -// http://viejs.org/ - -// ## VIE.Able -// VIE implements asynchronius service methods through -// [jQuery.Deferred](http://api.jquery.com/category/deferred-object/) objects. -// Loadable, Analysable, Savable, etc. are part of the VIE service API and -// are implemented with the generic VIE.Able class. -// Example: -// -// VIE.prototype.Loadable = function (options) { -// this.init(options,"load"); -// }; -// VIE.prototype.Loadable.prototype = new VIE.prototype.Able(); -// -// This defines -// -// someVIEService.load(options) -// .using(...) -// .execute() -// .success(...) -// .fail(...) -// which will run the asynchronius `load` function of the service with the created Loadable -// object. - -// ### VIE.Able() -// This is the constructor of a VIE.Able. This should not be called -// globally but using the inherited classes below. -// **Parameters**: -// *nothing* -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Able}* : A **new** VIE.Able object. -// Example: -// -// VIE.prototype.Loadable = function (options) { -// this.init(options,"load"); -// }; -// VIE.prototype.Loadable.prototype = new VIE.prototype.Able(); -VIE.prototype.Able = function(){ - -// ### init(options, methodName) -// Internal method, called during initialization. -// **Parameters**: -// *{object}* **options** the *able* options coming from the API call -// *{string}* **methodName** the service method called on `.execute`. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Able}* : The current instance. -// **Example usage**: -// -// VIE.prototype.Loadable = function (options) { -// this.init(options,"load"); -// }; -// VIE.prototype.Loadable.prototype = new VIE.prototype.Able(); - this.init = function(options, methodName) { - this.options = options; - this.services = options.from || options.using || options.to || []; - this.vie = options.vie; - - this.methodName = methodName; - - // Instantiate the deferred object - this.deferred = jQuery.Deferred(); - -// In order to get more information and documentation about the passed-through -// deferred methods and their synonyms, please see the documentation of -// the [jQuery.Deferred object](http://api.jquery.com/category/deferred-object/) - /* Public deferred-methods */ - this.resolve = this.deferred.resolve; - this.resolveWith = this.deferred.resolveWith; - this.reject = this.deferred.reject; - this.rejectWith = this.deferred.rejectWith; - this.success = this.done = this.deferred.done; - this.fail = this.deferred.fail; - this.then = this.deferred.then; - this.always = this.deferred.always; - this.from = this.using; - this.to = this.using; - - return this; - }; - - -// ### using(services) -// This method registers services with the current able instance. -// **Parameters**: -// *{string|array}* **services** An id of a service or an array of strings. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Able}* : The current instance. -// **Example usage**: -// -// var loadable = vie.load({id: "http://example.com/entity/1234"}); -// able.using("myService"); - this.using = function(services) { - var self = this; - services = (_.isArray(services))? services : [ services ]; - _.each (services, function (s) { - var obj = (typeof s === "string")? self.vie.service(s) : s; - self.services.push(obj); - }); - return this; - }; - -// ### execute() -// This method runs the actual method on all registered services. -// **Parameters**: -// *nothing* -// **Throws**: -// *nothing* ... -// **Returns**: -// *{VIE.Able}* : The current instance. -// **Example usage**: -// -// var able = new vie.Able().init(); -// able.using("stanbol") -// .done(function () {alert("finished");}) -// .execute(); - this.execute = function() { - /* call service[methodName] */ - var able = this; - _(this.services).each(function(service){ - service[able.methodName](able); - }); - return this; - }; -}; - -// ## VIE.Loadable -// A ```VIE.Loadable``` is a wrapper around the deferred object -// to **load** semantic data from a semantic web service. -VIE.prototype.Loadable = function (options) { - this.init(options,"load"); -}; -VIE.prototype.Loadable.prototype = new VIE.prototype.Able(); - -// ## VIE.Savable -// A ```VIE.Savable``` is a wrapper around the deferred object -// to **save** entities by a VIE service. The RDFaService would write the data -// in the HTML as RDFa, the StanbolService stores the data in its Entityhub, etc. -VIE.prototype.Savable = function(options){ - this.init(options, "save"); -}; -VIE.prototype.Savable.prototype = new VIE.prototype.Able(); - -// ## VIE.Removable -// A ```VIE.Removable``` is a wrapper around the deferred object -// to **remove** semantic data from a semantic web service. -VIE.prototype.Removable = function(options){ - this.init(options, "remove"); -}; -VIE.prototype.Removable.prototype = new VIE.prototype.Able(); - -// ## VIE.Analyzable -// A ```VIE.Analyzable``` is a wrapper around the deferred object -// to **analyze** data and extract semantic information with the -// help of a semantic web service. -VIE.prototype.Analyzable = function (options) { - this.init(options, "analyze"); -}; -VIE.prototype.Analyzable.prototype = new VIE.prototype.Able(); - -// ## VIE.Findable -// A ```VIE.Findable``` is a wrapper around the deferred object -// to **find** semantic data on a semantic storage. -VIE.prototype.Findable = function (options) { - this.init(options, "find"); -}; -VIE.prototype.Findable.prototype = new VIE.prototype.Able(); - -// VIE - Vienna IKS Editables -// (c) 2011 Henri Bergius, IKS Consortium -// (c) 2011 Sebastian Germesin, IKS Consortium -// (c) 2011 Szaby Grünwald, IKS Consortium -// VIE may be freely distributed under the MIT license. -// For all details and documentation: -// http://viejs.org/ - -// ## VIE Utils -// -// The here-listed methods are utility methods for the day-to-day -// VIE.js usage. All methods are within the static namespace ```VIE.Util```. -VIE.Util = { - -// ### VIE.Util.toCurie(uri, safe, namespaces) -// This method converts a given -// URI into a CURIE (or SCURIE), based on the given ```VIE.Namespaces``` object. -// If the given uri is already a URI, it is left untouched and directly returned. -// If no prefix could be found, an ```Error``` is thrown. -// **Parameters**: -// *{string}* **uri** The URI to be transformed. -// *{boolean}* **safe** A flag whether to generate CURIEs or SCURIEs. -// *{VIE.Namespaces}* **namespaces** The namespaces to be used for the prefixes. -// **Throws**: -// *{Error}* If no prefix could be found in the passed namespaces. -// **Returns**: -// *{string}* The CURIE or SCURIE. -// **Example usage**: -// -// var ns = new myVIE.Namespaces( -// "http://viejs.org/ns/", -// { "dbp": "http://dbpedia.org/ontology/" } -// ); -// var uri = ""; -// VIE.Util.toCurie(uri, false, ns); // --> dbp:Person -// VIE.Util.toCurie(uri, true, ns); // --> [dbp:Person] - toCurie : function (uri, safe, namespaces) { - if (VIE.Util.isCurie(uri, namespaces)) { - return uri; - } - var delim = ":"; - for (var k in namespaces.toObj()) { - if (uri.indexOf(namespaces.get(k)) === 1) { - var pattern = new RegExp("^" + "$/, '') + - ((safe)? "]" : ""); - } - } - throw new Error("No prefix found for URI '" + uri + "'!"); - }, - -// ### VIE.Util.isCurie(curie, namespaces) -// This method checks, whether -// the given string is a CURIE and returns ```true``` if so and ```false```otherwise. -// **Parameters**: -// *{string}* **curie** The CURIE (or SCURIE) to be checked. -// *{VIE.Namespaces}* **namespaces** The namespaces to be used for the prefixes. -// **Throws**: -// *nothing* -// **Returns**: -// *{boolean}* ```true``` if the given curie is a CURIE or SCURIE and ```false``` otherwise. -// **Example usage**: -// -// var ns = new myVIE.Namespaces( -// "http://viejs.org/ns/", -// { "dbp": "http://dbpedia.org/ontology/" } -// ); -// var uri = ""; -// var curie = "dbp:Person"; -// var scurie = "[dbp:Person]"; -// var text = "This is some text."; -// VIE.Util.isCurie(uri, ns); // --> false -// VIE.Util.isCurie(curie, ns); // --> true -// VIE.Util.isCurie(scurie, ns); // --> true -// VIE.Util.isCurie(text, ns); // --> false - isCurie : function (curie, namespaces) { - if (VIE.Util.isUri(curie)) { - return false; - } else { - try { - VIE.Util.toUri(curie, namespaces); - return true; - } catch (e) { - return false; - } - } - }, - -// ### VIE.Util.toUri(curie, namespaces) -// This method converts a -// given CURIE (or save CURIE) into a URI, based on the given ```VIE.Namespaces``` object. -// **Parameters**: -// *{string}* **curie** The CURIE to be transformed. -// *{VIE.Namespaces}* **namespaces** The namespaces object -// **Throws**: -// *{Error}* If no URI could be assembled. -// **Returns**: -// *{string}* : A string, representing the URI. -// **Example usage**: -// -// var ns = new myVIE.Namespaces( -// "http://viejs.org/ns/", -// { "dbp": "http://dbpedia.org/ontology/" } -// ); -// var curie = "dbp:Person"; -// var scurie = "[dbp:Person]"; -// VIE.Util.toUri(curie, ns); -// --> -// VIE.Util.toUri(scurie, ns); -// --> - toUri : function (curie, namespaces) { - if (VIE.Util.isUri(curie)) { - return curie; - } - var delim = ":"; - for (var prefix in namespaces.toObj()) { - if (prefix !== "" && (curie.indexOf(prefix + ":") === 0 || curie.indexOf("[" + prefix + ":") === 0)) { - var pattern = new RegExp("^" + "\\[{0,1}" + prefix + delim); - return "<" + curie.replace(pattern, namespaces.get(prefix)).replace(/\]{0,1}$/, '') + ">"; - } - } - /* check for the default namespace */ - if (curie.indexOf(delim) === -1) { - return "<" + namespaces.base() + curie + ">"; - } - throw new Error("No prefix found for CURIE '" + curie + "'!"); - }, - -// ### VIE.Util.isUri(something) -// This method checks, whether the given string is a URI. -// **Parameters**: -// *{string}* **something** : The string to be checked. -// **Throws**: -// *nothing* -// **Returns**: -// *{boolean}* : ```true``` if the string is a URI, ```false``` otherwise. -// **Example usage**: -// -// var uri = ""; -// var curie = "dbp:Person"; -// VIE.Util.isUri(uri); // --> true -// VIE.Util.isUri(curie); // --> false - isUri : function (something) { - return (typeof something === "string" && something.search(/^<.+>$/) === 0); - }, - -// ### VIE.Util.mapAttributeNS(attr, ns) -// This method maps an attribute of an entity into namespaces if they have CURIEs. -// **Parameters**: -// *{string}* **attr** : The attribute to be transformed. -// *{VIE.Namespaces}* **ns** : The namespaces. -// **Throws**: -// *nothing* -// **Returns**: -// *{string}* : The transformed attribute's name. -// **Example usage**: -// -// var attr = "name"; -// var ns = myVIE.namespaces; -// VIE.Util.mapAttributeNS(attr, ns); // '<' + ns.base() + attr + '>'; - mapAttributeNS : function (attr, ns) { - var a = attr; - if (ns.isUri (attr) || attr.indexOf('@') === 0) { - //ignore - } else if (ns.isCurie(attr)) { - a = ns.uri(attr); - } else if (!ns.isUri(attr)) { - if (attr.indexOf(":") === -1) { - a = '<' + ns.base() + attr + '>'; - } else { - a = '<' + attr + '>'; - } - } - return a; - }, - -// ### VIE.Util.rdf2Entities(service, results) -// This method converts *rdf/json* data from an external service -// into VIE.Entities. -// **Parameters**: -// *{object}* **service** The service that retrieved the data. -// *{object}* **results** The data to be transformed. -// **Throws**: -// *nothing* -// **Returns**: -// *{[VIE.Entity]}* : An array, containing VIE.Entity instances which have been transformed from the given data. - rdf2Entities: function (service, results) { - if (typeof jQuery.rdf !== 'function') { - /* fallback if no rdfQuery has been loaded */ - return VIE.Util._rdf2EntitiesNoRdfQuery(service, results); - } - try { - var rdf = (results instanceof jQuery.rdf)? - results.base(service.vie.namespaces.base()) : - jQuery.rdf().base(service.vie.namespaces.base()).load(results, {}); - - /* if the service contains rules to apply special transformation, they are executed here.*/ - if (service.rules) { - var rules = jQuery.rdf.ruleset(); - for (var prefix in service.vie.namespaces.toObj()) { - if (prefix !== "") { - rules.prefix(prefix, service.vie.namespaces.get(prefix)); - } - } - for (var i = 0; i < service.rules.length; i++)if(service.rules.hasOwnProperty(i)) { - var rule = service.rules[i]; - rules.add(rule.left, rule.right); - } - rdf = rdf.reason(rules, 10); /* execute the rules only 10 times to avoid looping */ - } - var entities = {}; - rdf.where('?subject ?property ?object').each(function() { - var subject = this.subject.toString(); - if (!entities[subject]) { - entities[subject] = { - '@subject': subject, - '@context': service.vie.namespaces.toObj(true), - '@type': [] - }; - } - var propertyUri = this.property.toString(); - var propertyCurie; - - try { - propertyCurie = service.vie.namespaces.curie(propertyUri); - //jQuery.createCurie(propertyUri, {namespaces: service.vie.namespaces.toObj(true)}); - } catch (e) { - propertyCurie = propertyUri; - // console.warn(propertyUri + " doesn't have a namespace definition in '", service.vie.namespaces.toObj()); - } - entities[subject][propertyCurie] = entities[subject][propertyCurie] || []; - - function getValue(rdfQueryLiteral){ - if(typeof rdfQueryLiteral.value === "string"){ - if (rdfQueryLiteral.lang){ - var literal = { - toString: function(){ - return this["@value"]; - }, - "@value": rdfQueryLiteral.value.replace(/^"|"$/g, ''), - "@language": rdfQueryLiteral.lang - }; - return literal; - } - else - return rdfQueryLiteral.value; - return rdfQueryLiteral.value.toString(); - } else if (rdfQueryLiteral.type === "uri"){ - return rdfQueryLiteral.toString(); - } else { - return rdfQueryLiteral.value; - } - } - entities[subject][propertyCurie].push(getValue(this.object)); - }); - - _(entities).each(function(ent){ - ent["@type"] = ent["@type"].concat(ent["rdf:type"]); - delete ent["rdf:type"]; - _(ent).each(function(value, property){ - if(value.length === 1){ - ent[property] = value[0]; - } - }); - }); - - var vieEntities = []; - jQuery.each(entities, function() { - var entityInstance = new service.vie.Entity(this); - entityInstance = service.vie.entities.addOrUpdate(entityInstance); - vieEntities.push(entityInstance); - }); - return vieEntities; - } catch (e) { - console.warn("Something went wrong while parsing the returned results!", e); - return []; - } - }, - - /* - VIE.Util.getPreferredLangForPreferredProperty(entity, preferredFields, preferredLanguages) - looks for specific ranking fields and languages. It calculates all possibilities and gives them - a score. It returns the value with the best score. - */ - getPreferredLangForPreferredProperty: function(entity, preferredFields, preferredLanguages) { - var l, labelArr, lang, p, property, resArr, valueArr, _len, _len2, - _this = this; - resArr = []; - /* Try to find a label in the preferred language - */ - _.each(preferredLanguages, function (lang) { - _.each(preferredFields, function (property) { - labelArr = null; - /* property can be a string e.g. "skos:prefLabel" - */ - if (typeof property === "string" && entity.get(property)) { - labelArr = _.flatten([entity.get(property)]); - _(labelArr).each(function(label) { - /* - The score is a natural number with 0 for the - best candidate with the first preferred language - and first preferred property - */ - var labelLang, score, value; - score = p; - labelLang = label["@language"]; - /* - legacy code for compatibility with uotdated stanbol, - to be removed after may 2012 - */ - if (typeof label === "string" && (label.indexOf("@") === label.length - 3 || label.indexOf("@") === label.length - 5)) { - labelLang = label.replace(/(^\"*|\"*@)..(..)?$/g, ""); - } - /* end of legacy code - */ - if (labelLang) { - if (labelLang === lang) { - score += l; - } else { - score += 20; - } - } else { - score += 10; - } - value = label.toString(); - /* legacy code for compatibility with uotdated stanbol, to be removed after may 2012 - */ - value = value.replace(/(^\"*|\"*@..$)/g, ""); - /* end of legacy code - */ - return resArr.push({ - score: score, - value: value - }); - }); - /* - property can be an object like - { - property: "skos:broader", - makeLabel: function(propertyValueArr) { return "..."; } - } - */ - } else if (typeof property === "object" && entity.get(property.property)) { - valueArr = _.flatten([entity.get(property.property)]); - valueArr = _(valueArr).map(function(termUri) { - if (termUri.isEntity) { - return termUri.getSubject(); - } else { - return termUri; - } - }); - resArr.push({ - score: p, - value: property.makeLabel(valueArr) - }); - } - }); - }); - /* - take the result with the best score - */ - resArr = _(resArr).sortBy(function(a) { - return a.score; - }); - if(resArr.length) { - return resArr[0].value; - } else { - return "n/a"; - } - }, - - -// ### VIE.Util._rdf2EntitiesNoRdfQuery(service, results) -// This is a **private** method which should -// only be accessed through ```VIE.Util._rdf2Entities()``` and is a helper method in case there is no -// rdfQuery loaded (*not recommended*). -// **Parameters**: -// *{object}* **service** The service that retrieved the data. -// *{object}* **results** The data to be transformed. -// **Throws**: -// *nothing* -// **Returns**: -// *{[VIE.Entity]}* : An array, containing VIE.Entity instances which have been transformed from the given data. - _rdf2EntitiesNoRdfQuery: function (service, results) { - var jsonLD = []; - _.forEach(results, function(value, key) { - var entity = {}; - entity['@subject'] = '<' + key + '>'; - _.forEach(value, function(triples, predicate) { - predicate = '<' + predicate + '>'; - _.forEach(triples, function(triple) { - if (triple.type === 'uri') { - triple.value = '<' + triple.value + '>'; - } - - if (entity[predicate] && !_.isArray(entity[predicate])) { - entity[predicate] = [entity[predicate]]; - } - - if (_.isArray(entity[predicate])) { - entity[predicate].push(triple.value); - return; - } - entity[predicate] = triple.value; - }); - }); - jsonLD.push(entity); - }); - return jsonLD; - }, - -// ### VIE.Util.loadSchemaOrg(vie, SchemaOrg, baseNS) -// This method is a wrapper around -// the schema.org ontology. It adds all the -// given types and properties as ```VIE.Type``` instances to the given VIE instance. -// If the paramenter **baseNS** is set, the method automatically sets the namespace -// to the provided one. If it is not set, it will keep the base namespace of VIE untouched. -// **Parameters**: -// *{VIE}* **vie** The instance of ```VIE```. -// *{object}* **SchemaOrg** The data imported from schema.org. -// *{string|undefined}* **baseNS** If set, this will become the new baseNamespace within the given ```VIE``` instance. -// **Throws**: -// *{Error}* If the parameter was not given. -// **Returns**: -// *nothing* - loadSchemaOrg : function (vie, SchemaOrg, baseNS) { - - if (!SchemaOrg) { - throw new Error("Please load the schema.json file."); - } - vie.types.remove(""); - - var baseNSBefore = (baseNS)? baseNS : vie.namespaces.base(); - vie.namespaces.base(baseNS); - - var datatypeMapping = { - 'DataType': 'xsd:anyType', - 'Boolean' : 'xsd:boolean', - 'Date' : 'xsd:date', - 'DateTime': 'xsd:dateTime', - 'Time' : 'xsd:time', - 'Float' : 'xsd:float', - 'Integer' : 'xsd:integer', - 'Number' : 'xsd:anySimpleType', - 'Text' : 'xsd:string', - 'URL' : 'xsd:anyURI' - }; - - var dataTypeHelper = function (ancestors, id) { - var type = vie.types.add(id, [{'id' : 'value', 'range' : datatypeMapping[id]}]); - - for (var i = 0; i < ancestors.length; i++) { - var supertype = (vie.types.get(ancestors[i]))? vie.types.get(ancestors[i]) : - dataTypeHelper.call(vie, SchemaOrg.datatypes[ancestors[i]].supertypes, ancestors[i]); - type.inherit(supertype); - } - return type; - }; - - for (var dt in SchemaOrg.datatypes) { - if (!vie.types.get(dt)) { - var ancestors = SchemaOrg.datatypes[dt].supertypes; - dataTypeHelper.call(vie, ancestors, dt); - } - } - - var metadataHelper = function (definition) { - var metadata = {}; - - if (definition.label) { - metadata.label = definition.label; - } - - if (definition.url) { - metadata.url = definition.url; - } - - if (definition.comment) { - metadata.comment = definition.comment; - } - - if (definition.metadata) { - metadata = _.extend(metadata, definition.metadata); - } - return metadata; - }; - - var typeProps = function (id) { - var props = []; - _.each(SchemaOrg.types[id].specific_properties, function (pId) { - var property = SchemaOrg.properties[pId]; - props.push({ - 'id' : property.id, - 'range' : property.ranges, - 'min' : property.min, - 'max' : property.max, - 'metadata': metadataHelper(property) - }); - }); - return props; - }; - - var typeHelper = function (ancestors, id, props, metadata) { - var type = vie.types.add(id, props, metadata); - - for (var i = 0; i < ancestors.length; i++) { - var supertype = (vie.types.get(ancestors[i]))? vie.types.get(ancestors[i]) : - typeHelper.call(vie, SchemaOrg.types[ancestors[i]].supertypes, ancestors[i], typeProps.call(vie, ancestors[i]), metadataHelper(SchemaOrg.types[ancestors[i]])); - type.inherit(supertype); - } - if (id === "Thing" && !type.isof("owl:Thing")) { - type.inherit("owl:Thing"); - } - return type; - }; - - _.each(SchemaOrg.types, function (typeDef) { - if (vie.types.get(typeDef.id)) { - return; - } - var ancestors = typeDef.supertypes; - var metadata = metadataHelper(typeDef); - typeHelper.call(vie, ancestors, typeDef.id, typeProps.call(vie, typeDef.id), metadata); - }); - - /* set the namespace to either the old value or the provided baseNS value */ - vie.namespaces.base(baseNSBefore); - }, - -// ### VIE.Util.getEntityTypeUnion(entity) -// This generates a entity-specific VIE type that is a subtype of all the -// types of the entity. This makes it easier to deal with attribute definitions -// specific to an entity because they're merged to a single list. This custom -// type is transient, meaning that it won't be automatilly added to the entity -// or the VIE type registry. - getEntityTypeUnion : function(entity) { - var vie = entity.vie; - return new vie.Type('Union').inherit(entity.get('@type')); - }, - -// ### VIE.Util.getFormSchemaForType(type) -// This creates a [Backbone Forms](https://github.com/powmedia/backbone-forms) -// -compatible form schema for any VIE Type. - getFormSchemaForType : function(type, allowNested) { - var schema = {}; - - // Generate a schema - _.each(type.attributes.toArray(), function (attribute) { - var key = VIE.Util.toCurie(attribute.id, false, attribute.vie.namespaces); - schema[key] = VIE.Util.getFormSchemaForAttribute(attribute); - }); - - // Clean up unknown attribute types - _.each(schema, function (field, id) { - if (!field.type) { - delete schema[id]; - } - - if (field.type === 'URL') { - field.type = 'Text'; - field.dataType = 'url'; - } - - if (field.type === 'List' && !field.listType) { - delete schema[id]; - } - - if (!allowNested) { - if (field.type === 'NestedModel' || field.listType === 'NestedModel') { - delete schema[id]; - } - } - }); - - return schema; - }, - -/// ### VIE.Util.getFormSchemaForAttribute(attribute) - getFormSchemaForAttribute : function(attribute) { - var primaryType = attribute.range[0]; - var schema = {}; - - var getWidgetForType = function (type) { - switch (type) { - case 'xsd:anySimpleType': - case 'xsd:float': - case 'xsd:integer': - return 'Number'; - case 'xsd:string': - return 'Text'; - case 'xsd:date': - return 'Date'; - case 'xsd:dateTime': - return 'DateTime'; - case 'xsd:boolean': - return 'Checkbox'; - case 'xsd:anyURI': - return 'URL'; - default: - var typeType = attribute.vie.types.get(type); - if (!typeType) { - return null; - } - if (typeType.attributes.get('value')) { - // Convert to proper xsd type - return getWidgetForType(typeType.attributes.get('value').range[0]); - } - return 'NestedModel'; - } - }; - - // TODO: Generate a nicer label - schema.title = VIE.Util.toCurie(attribute.id, false, attribute.vie.namespaces); - - // TODO: Handle attributes linking to other VIE entities - - if (attribute.min > 0) { - schema.validators = ['required']; - } - - if (attribute.max > 1) { - schema.type = 'List'; - schema.listType = getWidgetForType(primaryType); - if (schema.listType === 'NestedModel') { - schema.nestedModelType = primaryType; - } - return schema; - } - - schema.type = getWidgetForType(primaryType); - if (schema.type === 'NestedModel') { - schema.nestedModelType = primaryType; - } - return schema; - }, - -// ### VIE.Util.getFormSchema(entity) -// This creates a [Backbone Forms](https://github.com/powmedia/backbone-forms) -// -compatible form schema for any VIE Entity. The form schema creation -// utilizes type information attached to the entity. -// **Parameters**: -// *{```Entity```}* **entity** An instance of VIE ```Entity```. -// **Throws**: -// *nothing*.. -// **Returns**: -// *{object}* a JavaScript object representation of the form schema - getFormSchema : function(entity) { - if (!entity || !entity.isEntity) { - return {}; - } - - var unionType = VIE.Util.getEntityTypeUnion(entity); - var schema = VIE.Util.getFormSchemaForType(unionType, true); - - // Handle nested models - _.each(schema, function (property, id) { - if (property.type !== 'NestedModel' && property.listType !== 'NestedModel') { - return; - } - schema[id].model = entity.vie.getTypedEntityClass(property.nestedModelType); - }); - - return schema; - }, - -// ### VIE.Util.xsdDateTime(date) -// This transforms a ```Date``` instance into an xsd:DateTime format. -// **Parameters**: -// *{```Date```}* **date** An instance of a javascript ```Date```. -// **Throws**: -// *nothing*.. -// **Returns**: -// *{string}* A string representation of the dateTime in the xsd:dateTime format. - xsdDateTime : function(date) { - function pad(n) { - var s = n.toString(); - return s.length < 2 ? '0'+s : s; - } - - var yyyy = date.getFullYear(); - var mm1 = pad(date.getMonth()+1); - var dd = pad(date.getDate()); - var hh = pad(date.getHours()); - var mm2 = pad(date.getMinutes()); - var ss = pad(date.getSeconds()); - - return yyyy +'-' +mm1 +'-' +dd +'T' +hh +':' +mm2 +':' +ss; - }, - -// ### VIE.Util.extractLanguageString(entity, attrs, langs) -// This method extracts a literal string from an entity, searching through the given attributes and languages. -// **Parameters**: -// *{```VIE.Entity```}* **entity** An instance of a VIE.Entity. -// *{```array|string```}* **attrs** Either a string or an array of possible attributes. -// *{```array|string```}* **langs** Either a string or an array of possible languages. -// **Throws**: -// *nothing*.. -// **Returns**: -// *{string|undefined}* The string that was found at the attribute with the wanted language, undefined if nothing could be found. -// **Example usage**: -// -// var attrs = ["name", "rdfs:label"]; -// var langs = ["en", "de"]; -// VIE.Util.extractLanguageString(someEntity, attrs, langs); // "Barack Obama"; - extractLanguageString : function(entity, attrs, langs) { - var p, attr, name, i, n; - if (entity && typeof entity !== "string") { - attrs = (_.isArray(attrs))? attrs : [ attrs ]; - langs = (_.isArray(langs))? langs : [ langs ]; - for (p = 0; p < attrs.length; p++) { - for (var l = 0; l < langs.length; l++) { - var lang = langs[l]; - attr = attrs[p]; - if (entity.has(attr)) { - name = entity.get(attr); - name = (_.isArray(name))? name : [ name ]; - for (i = 0; i < name.length; i++) { - n = name[i]; - if (n.isEntity) { - n = VIE.Util.extractLanguageString(n, attrs, lang); - } else if (typeof n === "string") { - n = n; - } else { - n = ""; - } - if (n && n.indexOf('@' + lang) > -1) { - return n.replace(/"/g, "").replace(/@[a-z]+/, '').trim(); - } - } - } - } - } - /* let's do this again in case we haven't found a name but are dealing with - broken data where no language is given */ - for (p = 0; p < attrs.length; p++) { - attr = attrs[p]; - if (entity.has(attr)) { - name = entity.get(attr); - name = (_.isArray(name))? name : [ name ]; - for (i = 0; i < name.length; i++) { - n = name[i]; - if (n.isEntity) { - n = VIE.Util.extractLanguageString(n, attrs, []); - } - if (n && (typeof n === "string") && n.indexOf('@') === -1) { - return n.replace(/"/g, "").replace(/@[a-z]+/, '').trim(); - } - } - } - } - } - return undefined; - }, - -// ### VIE.Util.transformationRules(service) -// This returns a default set of rdfQuery rules that transform semantic data into the -// VIE entity types. -// **Parameters**: -// *{object}* **service** An instance of a vie.service. -// **Throws**: -// *nothing*.. -// **Returns**: -// *{array}* An array of rules with 'left' and 'right' side. - transformationRules : function (service) { - var res = [ - // rule(s) to transform a dbpedia:Person into a VIE:Person - { - 'left' : [ - '?subject a dbpedia:Person', - '?subject rdfs:label ?label' - ], - 'right': function(ns){ - return function(){ - return [ - jQuery.rdf.triple(this.subject.toString(), - 'a', - '<' + ns.base() + 'Person>', { - namespaces: ns.toObj() - }), - jQuery.rdf.triple(this.subject.toString(), - '<' + ns.base() + 'name>', - this.label, { - namespaces: ns.toObj() - }) - ]; - }; - }(service.vie.namespaces) - }, - // rule(s) to transform a foaf:Person into a VIE:Person - { - 'left' : [ - '?subject a foaf:Person', - '?subject rdfs:label ?label' - ], - 'right': function(ns){ - return function(){ - return [ - jQuery.rdf.triple(this.subject.toString(), - 'a', - '<' + ns.base() + 'Person>', { - namespaces: ns.toObj() - }), - jQuery.rdf.triple(this.subject.toString(), - '<' + ns.base() + 'name>', - this.label, { - namespaces: ns.toObj() - }) - ]; - }; - }(service.vie.namespaces) - }, - // rule(s) to transform a dbpedia:Place into a VIE:Place - { - 'left' : [ - '?subject a dbpedia:Place', - '?subject rdfs:label ?label' - ], - 'right': function(ns) { - return function() { - return [ - jQuery.rdf.triple(this.subject.toString(), - 'a', - '<' + ns.base() + 'Place>', { - namespaces: ns.toObj() - }), - jQuery.rdf.triple(this.subject.toString(), - '<' + ns.base() + 'name>', - this.label.toString(), { - namespaces: ns.toObj() - }) - ]; - }; - }(service.vie.namespaces) - }, - // rule(s) to transform a dbpedia:City into a VIE:City - { - 'left' : [ - '?subject a dbpedia:City', - '?subject rdfs:label ?label', - '?subject dbpedia:abstract ?abs', - '?subject dbpedia:country ?country' - ], - 'right': function(ns) { - return function() { - return [ - jQuery.rdf.triple(this.subject.toString(), - 'a', - '<' + ns.base() + 'City>', { - namespaces: ns.toObj() - }), - jQuery.rdf.triple(this.subject.toString(), - '<' + ns.base() + 'name>', - this.label.toString(), { - namespaces: ns.toObj() - }), - jQuery.rdf.triple(this.subject.toString(), - '<' + ns.base() + 'description>', - this.abs.toString(), { - namespaces: ns.toObj() - }), - jQuery.rdf.triple(this.subject.toString(), - '<' + ns.base() + 'containedIn>', - this.country.toString(), { - namespaces: ns.toObj() - }) - ]; - }; - }(service.vie.namespaces) - } - ]; - return res; - }, - - getAdditionalRules : function (service) { - - var mapping = { - Work : "CreativeWork", - Film : "Movie", - TelevisionEpisode : "TVEpisode", - TelevisionShow : "TVSeries", // not listed as equivalent class on dbpedia.org - Website : "WebPage", - Painting : "Painting", - Sculpture : "Sculpture", - - Event : "Event", - SportsEvent : "SportsEvent", - MusicFestival : "Festival", - FilmFestival : "Festival", - - Place : "Place", - Continent : "Continent", - Country : "Country", - City : "City", - Airport : "Airport", - Station : "TrainStation", // not listed as equivalent class on dbpedia.org - Hospital : "GovernmentBuilding", - Mountain : "Mountain", - BodyOfWater : "BodyOfWater", - - Company : "Organization", - Person : "Person" - }; - - var additionalRules = []; - _.each(mapping, function (map, key) { - var tripple = { - 'left' : [ '?subject a dbpedia:' + key, '?subject rdfs:label ?label' ], - 'right' : function(ns) { - return function() { - return [ jQuery.rdf.triple(this.subject.toString(), 'a', '<' + ns.base() + map + '>', { - namespaces : ns.toObj() - }), jQuery.rdf.triple(this.subject.toString(), '<' + ns.base() + 'name>', this.label.toString(), { - namespaces : ns.toObj() - }) ]; - }; - }(service.vie.namespaces) - }; - additionalRules.push(tripple); - }); - return additionalRules; - } -}; -// VIE - Vienna IKS Editables -// (c) 2011 Henri Bergius, IKS Consortium -// (c) 2011 Sebastian Germesin, IKS Consortium -// (c) 2011 Szaby Grünwald, IKS Consortium -// VIE may be freely distributed under the MIT license. -// For all details and documentation: -// http://viejs.org/ - -// ## VIE Entities -// -// In VIE there are two low-level model types for storing data. -// **Collections** and **Entities**. Considering `var v = new VIE();` a VIE instance, -// `v.entities` is a Collection with `VIE Entity` objects in it. -// VIE internally uses JSON-LD to store entities. -// -// Each Entity has a few special attributes starting with an `@`. VIE has an API -// for correctly using these attributes, so in order to stay compatible with later -// versions of the library, possibly using a later version of JSON-LD, use the API -// to interact with your entities. -// -// * `@subject` stands for the identifier of the entity. Use `e.getSubject()` -// * `@type` stores the explicit entity types. VIE internally handles Type hierarchy, -// which basically enables to define subtypes and supertypes. Every entity has -// the type 'owl:Thing'. Read more about Types in VIE.Type. -// * `@context` stores namespace definitions used in the entity. Read more about -// Namespaces in VIE Namespaces. -VIE.prototype.Entity = function(attrs, opts) { - - attrs = (attrs)? attrs : {}; - opts = (opts)? opts : {}; - - var self = this; - - if (attrs['@type'] !== undefined) { - attrs['@type'] = (_.isArray(attrs['@type']))? attrs['@type'] : [ attrs['@type'] ]; - attrs['@type'] = _.map(attrs['@type'], function(val){ - if (!self.vie.types.get(val)) { - //if there is no such type -> add it and let it inherit from "owl:Thing" - self.vie.types.add(val).inherit("owl:Thing"); - } - return self.vie.types.get(val).id; - }); - attrs['@type'] = (attrs['@type'].length === 1)? attrs['@type'][0] : attrs['@type']; - } else { - // provide "owl:Thing" as the default type if none was given - attrs['@type'] = self.vie.types.get("owl:Thing").id; - } - - //the following provides full seamless namespace support - //for attributes. It should not matter, if you - //query for `model.get('name')` or `model.get('foaf:name')` - //or even `model.get('http://xmlns.com/foaf/0.1/name');` - //However, if we just overwrite `set()` and `get()`, this - //raises a lot of side effects, so we need to expand - //the attributes before we create the model. - _.each (attrs, function (value, key) { - var newKey = VIE.Util.mapAttributeNS(key, this.namespaces); - if (key !== newKey) { - delete attrs[key]; - attrs[newKey] = value; - } - }, self.vie); - - var Model = Backbone.Model.extend({ - idAttribute: '@subject', - - initialize: function(attributes, options) { - if (attributes['@subject']) { - this.id = this['@subject'] = this.toReference(attributes['@subject']); - } else { - this.id = this['@subject'] = attributes['@subject'] = this.cid.replace('c', '_:bnode'); - } - return this; - }, - - schema: function() { - return VIE.Util.getFormSchema(this); - }, - - // ### Getter, Has, Setter - // #### `.get(attr)` - // To be able to communicate to a VIE Entity you can use a simple get(property) - // command as in `entity.get('rdfs:label')` which will give you one or more literals. - // If the property points to a collection, its entities can be browsed further. - get: function (attr) { - attr = VIE.Util.mapAttributeNS(attr, self.vie.namespaces); - var value = Backbone.Model.prototype.get.call(this, attr); - value = (_.isArray(value))? value : [ value ]; - - value = _.map(value, function(v) { - if (v !== undefined && attr === '@type' && self.vie.types.get(v)) { - return self.vie.types.get(v); - } else if (v !== undefined && self.vie.entities.get(v)) { - return self.vie.entities.get(v); - } else { - return v; - } - }, this); - if(value.length === 0) { - return undefined; - } - // if there is only one element, just return that one - value = (value.length === 1)? value[0] : value; - return value; - }, - - // #### `.has(attr)` - // Sometimes you'd like to determine if a specific attribute is set - // in an entity. For this reason you can call for example `person.has('friend')` - // to determine if a person entity has friends. - has: function(attr) { - attr = VIE.Util.mapAttributeNS(attr, self.vie.namespaces); - return Backbone.Model.prototype.has.call(this, attr); - }, - - // #### `.set(attrName, value, opts)`, - // The `options` parameter always refers to a `Backbone.Model.set` `options` object. - // - // **`.set(attributes, options)`** is the most universal way of calling the - // `.set` method. In this case the `attributes` object is a map of all - // attributes to be changed. - set : function(attrs, options, opts) { - if (!attrs) { - return this; - } - - if (attrs['@subject']) { - attrs['@subject'] = this.toReference(attrs['@subject']); - } - - // Use **`.set(attrName, value, options)`** for setting or changing exactly one - // entity attribute. - if (typeof attrs === "string") { - var obj = {}; - obj[attrs] = options; - return this.set(obj, opts); - } - // **`.set(entity)`**: In case you'd pass a VIE entity, - // the passed entities attributes are being set for the entity. - if (attrs.attributes) { - attrs = attrs.attributes; - } - var self = this; - var coll; - // resolve shortened URIs like rdfs:label.. - _.each (attrs, function (value, key) { - var newKey = VIE.Util.mapAttributeNS(key, self.vie.namespaces); - if (key !== newKey) { - delete attrs[key]; - attrs[newKey] = value; - } - }, this); - // Finally iterate through the *attributes* to be set and prepare - // them for the Backbone.Model.set method. - _.each (attrs, function (value, key) { - if (!value) { return; } - if (key.indexOf('@') === -1) { - if (value.isCollection) { - // ignore - value.each(function (child) { - self.vie.entities.addOrUpdate(child); - }); - } else if (value.isEntity) { - self.vie.entities.addOrUpdate(value); - coll = new self.vie.Collection(value, { - vie: self.vie, - predicate: key - }); - attrs[key] = coll; - } else if (_.isArray(value)) { - if (this.attributes[key] && this.attributes[key].isCollection) { - var newEntities = this.attributes[key].addOrUpdate(value); - attrs[key] = this.attributes[key]; - attrs[key].reset(newEntities); - } - } else if (value["@value"]) { - // The value is a literal object, ignore - } else if (_.isObject(value) && !_.isDate(value)) { - // The value is another VIE Entity - var child = new self.vie.Entity(value, options); - // which is being stored in `v.entities` - self.vie.entities.addOrUpdate(child); - // and set as VIE Collection attribute on the original entity - coll = new self.vie.Collection(value, { - vie: self.vie, - predicate: key - }); - attrs[key] = coll; - } else { - // ignore - } - } - }, this); - var ret = Backbone.Model.prototype.set.call(this, attrs, options); - if (options && options.ignoreChanges) { - this.changed = {}; - this._previousAttributes = _.clone(this.attributes); - } - return ret; - }, - - // **`.unset(attr, opts)` ** removes an attribute from the entity. - unset: function (attr, opts) { - attr = VIE.Util.mapAttributeNS(attr, self.vie.namespaces); - return Backbone.Model.prototype.unset.call(this, attr, opts); - }, - - // Validation based on type rules. - // - // There are two ways to skip validation for entity operations: - // - // * `options.silent = true` - // * `options.validate = false` - validate: function (attrs, opts) { - if (opts && opts.validate === false) { - return; - } - var types = this.get('@type'); - if (_.isArray(types)) { - var results = []; - _.each(types, function (type) { - var res = this.validateByType(type, attrs, opts); - if (res) { - results.push(res); - } - }, this); - if (_.isEmpty(results)) { - return; - } - return _.flatten(results); - } - - return this.validateByType(types, attrs, opts); - }, - - validateByType: function (type, attrs, opts) { - var messages = { - max: '<%= property %> cannot contain more than <%= num %> items', - min: '<%= property %> must contain at least <%= num %> items', - required: '<%= property %> is required' - }; - - if (!type.attributes) { - return; - } - - var toError = function (definition, constraint, messageValues) { - return { - property: definition.id, - constraint: constraint, - message: _.template(messages[constraint], _.extend({ - property: definition.id - }, messageValues)) - }; - }; - - var checkMin = function (definition, attrs) { - if (!attrs[definition.id] || _.isEmpty(attrs[definition.id])) { - return toError(definition, 'required', {}); - } - }; - - // Check the number of items in attr against max - var checkMax = function (definition, attrs) { - if (!attrs[definition.id]) { - return; - } - - if (!attrs[definition.id].isCollection && !_.isArray(attrs[definition.id])) { - return; - } - - if (attrs[definition.id].length > definition.max) { - return toError(definition, 'max', { - num: definition.max - }); - } - }; - - var results = []; - _.each(type.attributes.list(), function (definition) { - var res; - if (definition.max && definition.max != -1) { - res = checkMax(definition, attrs); - if (res) { - results.push(res); - } - } - - if (definition.min && definition.min > 0) { - res = checkMin(definition, attrs); - if (res) { - results.push(res); - } - } - }); - - if (_.isEmpty(results)) { - return; - } - return results; - }, - - isNew: function() { - if (this.getSubjectUri().substr(0, 7) === '_:bnode') { - return true; - } - return false; - }, - - hasChanged: function(attr) { - if (this.markedChanged) { - return true; - } - - return Backbone.Model.prototype.hasChanged.call(this, attr); - }, - - // Force hasChanged to return true - forceChanged: function(changed) { - this.markedChanged = changed ? true : false; - }, - - // **`getSubject()`** is the getter for the entity identifier. - getSubject: function(){ - if (typeof this.id === "undefined") { - this.id = this.attributes[this.idAttribute]; - } - if (typeof this.id === 'string') { - if (this.id.substr(0, 7) === 'http://' || this.id.substr(0, 4) === 'urn:') { - return this.toReference(this.id); - } - return this.id; - } - return this.cid.replace('c', '_:bnode'); - }, - - // TODO describe - getSubjectUri: function(){ - return this.fromReference(this.getSubject()); - }, - - isReference: function(uri){ - var matcher = new RegExp("^\\<([^\\>]*)\\>$"); - if (matcher.exec(uri)) { - return true; - } - return false; - }, - - toReference: function(uri){ - if (_.isArray(uri)) { - var self = this; - return _.map(uri, function(part) { - return self.toReference(part); - }); - } - var ns = this.vie.namespaces; - var ret = uri; - if (uri.substring(0, 2) === "_:") { - ret = uri; - } - else if (ns.isCurie(uri)) { - ret = ns.uri(uri); - if (ret === "<" + ns.base() + uri + ">") { - /* no base namespace extension with IDs */ - ret = '<' + uri + '>'; - } - } else if (!ns.isUri(uri)) { - ret = '<' + uri + '>'; - } - return ret; - }, - - fromReference: function(uri){ - var ns = this.vie.namespaces; - if (!ns.isUri(uri)) { - return uri; - } - return uri.substring(1, uri.length - 1); - }, - - as: function(encoding){ - if (encoding === "JSON") { - return this.toJSON(); - } - if (encoding === "JSONLD") { - return this.toJSONLD(); - } - throw new Error("Unknown encoding " + encoding); - }, - - toJSONLD: function(){ - var instanceLD = {}; - var instance = this; - _.each(instance.attributes, function(value, name){ - var entityValue = value; //instance.get(name); - - if (value instanceof instance.vie.Collection) { - entityValue = value.map(function(instance) { - return instance.getSubject(); - }); - } - - // TODO: Handle collections separately - instanceLD[name] = entityValue; - }); - - instanceLD['@subject'] = instance.getSubject(); - - return instanceLD; - }, - - // **`.setOrAdd(arg1, arg2)`** similar to `.set(..)`, `.setOrAdd(..)` can - // be used for setting one or more attributes of an entity, but in - // this case it's a collection of values, not just one. That means, if the - // entity already has the attribute set, make the value to a VIE Collection - // and use the collection as value. The collection can contain entities - // or literals, but not both at the same time. - setOrAdd: function (arg1, arg2, option) { - var entity = this; - if (typeof arg1 === "string" && arg2) { - // calling entity.setOrAdd("rdfs:type", "example:Musician") - entity._setOrAddOne(arg1, arg2, option); - } - else - if (typeof arg1 === "object") { - // calling entity.setOrAdd({"rdfs:type": "example:Musician", ...}) - _(arg1).each(function(val, key){ - entity._setOrAddOne(key, val, arg2); - }); - } - return this; - }, - - - /* attr is always of type string */ - /* value can be of type: string,int,double,object,VIE.Entity,VIE.Collection */ - /* val can be of type: undefined,string,int,double,array,VIE.Collection */ - - /* depending on the type of value and the type of val, different actions need to be made */ - _setOrAddOne: function (attr, value, options) { - if (!attr || !value) - return; - options = (options)? options : {}; - var v; - - attr = VIE.Util.mapAttributeNS(attr, self.vie.namespaces); - - if (_.isArray(value)) { - for (v = 0; v < value.length; v++) { - this._setOrAddOne(attr, value[v], options); - } - return; - } - - if (attr === "@type" && value instanceof self.vie.Type) { - value = value.id; - } - - var obj = {}; - var existing = Backbone.Model.prototype.get.call(this, attr); - - if (!existing) { - obj[attr] = value; - this.set(obj, options); - } else if (existing.isCollection) { - if (value.isCollection) { - value.each(function (model) { - existing.add(model); - }); - } else if (value.isEntity) { - existing.add(value); - } else if (typeof value === "object") { - value = new this.vie.Entity(value); - existing.add(value); - } else { - throw new Error("you cannot add a literal to a collection of entities!"); - } - this.trigger('change:' + attr, this, value, {}); - this.change({}); - } else if (_.isArray(existing)) { - if (value.isCollection) { - for (v = 0; v < value.size(); v++) { - this._setOrAddOne(attr, value.at(v).getSubject(), options); - } - } else if (value.isEntity) { - this._setOrAddOne(attr, value.getSubject(), options); - } else if (typeof value === "object") { - value = new this.vie.Entity(value); - this._setOrAddOne(attr, value, options); - } else { - /* yes, we (have to) allow multiple equal values */ - existing.push(value); - obj[attr] = existing; - this.set(obj); - } - } else { - var arr = [ existing ]; - arr.push(value); - obj[attr] = arr; - return this.set(obj, options); - } - }, - - // **`.hasType(type)`** determines if the entity has the explicit `type` set. - hasType: function(type){ - type = self.vie.types.get(type); - return this.hasPropertyValue("@type", type); - }, - - // TODO describe - hasPropertyValue: function(property, value) { - var t = this.get(property); - if (!(value instanceof Object)) { - value = self.vie.entities.get(value); - } - if (t instanceof Array) { - return t.indexOf(value) !== -1; - } - else { - return t === value; - } - }, - - // **`.isof(type)`** determines if the entity is of `type` by explicit or implicit - // declaration. E.g. if Employee is a subtype of Person and e Entity has - // explicitly set type Employee, e.isof(Person) will evaluate to true. - isof: function (type) { - var types = this.get('@type'); - - if (types === undefined) { - return false; - } - types = (_.isArray(types))? types : [ types ]; - - type = (self.vie.types.get(type))? self.vie.types.get(type) : new self.vie.Type(type); - for (var t = 0; t < types.length; t++) { - if (self.vie.types.get(types[t])) { - if (self.vie.types.get(types[t]).isof(type)) { - return true; - } - } else { - var typeTmp = new self.vie.Type(types[t]); - if (typeTmp.id === type.id) { - return true; - } - } - } - return false; - }, - // TODO describe - addTo : function (collection, update) { - var self = this; - if (collection instanceof self.vie.Collection) { - if (update) { - collection.addOrUpdate(self); - } else { - collection.add(self); - } - return this; - } - throw new Error("Please provide a proper collection of type VIE.Collection as argument!"); - }, - - isEntity: true, - - vie: self.vie - }); - - return new Model(attrs, opts); -}; -// VIE - Vienna IKS Editables -// (c) 2011 Henri Bergius, IKS Consortium -// (c) 2011 Sebastian Germesin, IKS Consortium -// (c) 2011 Szaby Grünwald, IKS Consortium -// VIE may be freely distributed under the MIT license. -// For all details and documentation: -// http://viejs.org/ -VIE.prototype.Collection = Backbone.Collection.extend({ - model: VIE.prototype.Entity, - - initialize: function (models, options) { - if (!options || !options.vie) { - throw new Error('Each collection needs a VIE reference'); - } - this.vie = options.vie; - this.predicate = options.predicate; - }, - - canAdd: function (type) { - return true; - }, - - get: function(id) { - if (id === null) { - return null; - } - - id = (id.getSubject)? id.getSubject() : id; - if (typeof id === "string" && id.indexOf("_:") === 0) { - if (id.indexOf("bnode") === 2) { - //bnode! - id = id.replace("_:bnode", 'c'); - return this._byCid[id]; - } else { - return this._byId["<" + id + ">"]; - } - } else { - id = this.toReference(id); - return this._byId[id]; - } - }, - - addOrUpdate: function(model, options) { - options = options || {}; - - var collection = this; - var existing; - if (_.isArray(model)) { - var entities = []; - _.each(model, function(item) { - entities.push(collection.addOrUpdate(item, options)); - }); - return entities; - } - - if (model === undefined) { - throw new Error("No model given"); - } - - if (_.isString(model)) { - model = { - '@subject': model, - id: model - }; - } - - if (!model.isEntity) { - model = new this.model(model); - } - - if (model.id && this.get(model.id)) { - existing = this.get(model.id); - } - if (this.getByCid(model.cid)) { - existing = this.getByCid(model.cid); - } - if (existing) { - var newAttribs = {}; - _.each(model.attributes, function(value, attribute) { - if (!existing.has(attribute)) { - newAttribs[attribute] = value; - return true; - } - - if (attribute === '@subject') { - if (model.isNew() && !existing.isNew()) { - // Save order issue, skip - return true; - } - } - - if (existing.get(attribute) === value) { - return true; - } - //merge existing attribute values with new ones! - //not just overwrite 'em!! - var oldVals = existing.attributes[attribute]; - var newVals = value; - if (oldVals instanceof collection.vie.Collection) { - // TODO: Merge collections - return true; - } - if (options.overrideAttributes) { - newAttribs[attribute] = value; - return true; - } - if (attribute === '@context') { - newAttribs[attribute] = jQuery.extend(true, {}, oldVals, newVals); - } else { - oldVals = (jQuery.isArray(oldVals))? oldVals : [ oldVals ]; - newVals = (jQuery.isArray(newVals))? newVals : [ newVals ]; - newAttribs[attribute] = _.uniq(oldVals.concat(newVals)); - newAttribs[attribute] = (newAttribs[attribute].length === 1)? newAttribs[attribute][0] : newAttribs[attribute]; - } - }); - - if (!_.isEmpty(newAttribs)) { - existing.set(newAttribs, options.updateOptions); - } - return existing; - } - this.add(model, options.addOptions); - return model; - }, - - isReference: function(uri){ - var matcher = new RegExp("^\\<([^\\>]*)\\>$"); - if (matcher.exec(uri)) { - return true; - } - return false; - }, - - toReference: function(uri){ - if (this.isReference(uri)) { - return uri; - } - return '<' + uri + '>'; - }, - - fromReference: function(uri){ - if (!this.isReference(uri)) { - return uri; - } - return uri.substring(1, uri.length - 1); - }, - - isCollection: true -}); -// VIE - Vienna IKS Editables -// (c) 2011 Henri Bergius, IKS Consortium -// (c) 2011 Sebastian Germesin, IKS Consortium -// (c) 2011 Szaby Grünwald, IKS Consortium -// VIE may be freely distributed under the MIT license. -// For all details and documentation: -// http://viejs.org/ -// - -// ## VIE.Types -// Within VIE, we provide special capabilities of handling types of entites. This helps -// for example to query easily for certain entities (e.g., you only need to query for *Person*s -// and not for all subtypes). -if (VIE.prototype.Type) { - throw new Error("ERROR: VIE.Type is already defined. Please check your installation!"); -} -if (VIE.prototype.Types) { - throw new Error("ERROR: VIE.Types is already defined. Please check your installation!"); -} - -// ### VIE.Type(id, attrs, metadata) -// This is the constructor of a VIE.Type. -// **Parameters**: -// *{string}* **id** The id of the type. -// *{string|array|VIE.Attribute}* **attrs** A string, proper ```VIE.Attribute``` or an array of these which -// *{object}* **metadata** Possible metadata about the type -// are the possible attributes of the type -// **Throws**: -// *{Error}* if one of the given paramenters is missing. -// **Returns**: -// *{VIE.Type}* : A **new** VIE.Type object. -// **Example usage**: -// -// var person = new vie.Type("Person", ["name", "knows"]); -VIE.prototype.Type = function (id, attrs, metadata) { - if (id === undefined || typeof id !== 'string') { - throw "The type constructor needs an 'id' of type string! E.g., 'Person'"; - } - -// ### id -// This field stores the id of the type's instance. -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{string}* : The id of the type as a URI. -// **Example usage**: -// -// console.log(person.id); -// // --> "" - this.id = this.vie.namespaces.isUri(id) ? id : this.vie.namespaces.uri(id); - - /* checks whether such a type is already defined. */ - if (this.vie.types.get(this.id)) { - throw new Error("The type " + this.id + " is already defined!"); - } - -// ### supertypes -// This field stores all parent types of the type's instance. This -// is set if the current type inherits from another type. -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{VIE.Types}* : The supertypes (parents) of the type. -// **Example usage**: -// -// console.log(person.supertypes); - this.supertypes = new this.vie.Types(); - -// ### subtypes -// This field stores all children types of the type's instance. This -// will be set if another type inherits from the current type. -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{VIE.Types}* : The subtypes (parents) of the type. -// **Example usage**: -// -// console.log(person.subtypes); - this.subtypes = new this.vie.Types(); - -// ### attributes -// This field stores all attributes of the type's instance as -// a proper ```VIE.Attributes``` class. (see also VIE.Attributes) -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{VIE.Attributes}* : The attributes of the type. -// **Example usage**: -// -// console.log(person.attributes); - this.attributes = new this.vie.Attributes(this, (attrs)? attrs : []); - -// ### metadata -// This field stores possible additional information about the type, like -// a human-readable label. - this.metadata = metadata ? metadata : {}; - -// ### isof(type) -// This method checks whether the current type is a child of the given type. -// **Parameters**: -// *{string|VIE.Type}* **type** The type (or the id of that type) to be checked. -// **Throws**: -// *{Error}* If the type is not valid. -// **Returns**: -// *{boolean}* : ```true``` if the current type inherits from the type, ```false``` otherwise. -// **Example usage**: -// -// console.log(person.isof("owl:Thing")); -// // <-- true - this.isof = function (type) { - type = this.vie.types.get(type); - if (type) { - return type.subsumes(this.id); - } else { - throw new Error("No valid type given"); - } - }; - -// ### subsumes(type) -// This method checks whether the current type is a parent of the given type. -// **Parameters**: -// *{string|VIE.Type}* **type** The type (or the id of that type) to be checked. -// **Throws**: -// *{Error}* If the type is not valid. -// **Returns**: -// *{boolean}* : ```true``` if the current type is a parent of the type, ```false``` otherwise. -// **Example usage**: -// -// var x = new vie.Type(...); -// var y = new vie.Type(...).inherit(x); -// y.isof(x) === x.subsumes(y); - this.subsumes = function (type) { - type = this.vie.types.get(type); - if (type) { - if (this.id === type.id) { - return true; - } - var subtypes = this.subtypes.list(); - for (var c = 0; c < subtypes.length; c++) { - var childObj = subtypes[c]; - if (childObj) { - if (childObj.id === type.id || childObj.subsumes(type)) { - return true; - } - } - } - return false; - } else { - throw new Error("No valid type given"); - } - }; - -// ### inherit(supertype) -// This method invokes inheritance throught the types. This adds the current type to the -// subtypes of the supertype and vice versa. -// **Parameters**: -// *{string|VIE.Type|array}* **supertype** The type to be inherited from. If this is an array -// the inherit method is called sequentially on all types. -// **Throws**: -// *{Error}* If the type is not valid. -// **Returns**: -// *{VIE.Type}* : The instance itself. -// **Example usage**: -// -// var x = new vie.Type(...); -// var y = new vie.Type(...).inherit(x); -// y.isof(x) // <-- true - this.inherit = function (supertype) { - if (typeof supertype === "string") { - this.inherit(this.vie.types.get(supertype)); - } - else if (supertype instanceof this.vie.Type) { - supertype.subtypes.addOrOverwrite(this); - this.supertypes.addOrOverwrite(supertype); - try { - /* only for validation of attribute-inheritance! - if this throws an error (inheriting two attributes - that cannot be combined) we reverse all changes. */ - this.attributes.list(); - } catch (e) { - supertype.subtypes.remove(this); - this.supertypes.remove(supertype); - throw e; - } - } else if (jQuery.isArray(supertype)) { - for (var i = 0, slen = supertype.length; i < slen; i++) { - this.inherit(supertype[i]); - } - } else { - throw new Error("Wrong argument in VIE.Type.inherit()"); - } - return this; - }; - -// ### hierarchy() -// This method serializes the hierarchy of child types into an object. -// **Parameters**: -// *nothing* -// **Throws**: -// *nothing* -// **Returns**: -// *{object}* : The hierachy of child types as an object. -// **Example usage**: -// -// var x = new vie.Type(...); -// var y = new vie.Type(...).inherit(x); -// x.hierarchy(); - this.hierarchy = function () { - var obj = {id : this.id, subtypes: []}; - var list = this.subtypes.list(); - for (var c = 0, llen = list.length; c < llen; c++) { - var childObj = this.vie.types.get(list[c]); - obj.subtypes.push(childObj.hierarchy()); - } - return obj; - }; - -// ### instance() -// This method creates a ```VIE.Entity``` instance from this type. -// **Parameters**: -// *{object}* **attrs** see constructor of VIE.Entity -// *{object}* **opts** see constructor of VIE.Entity -// **Throws**: -// *{Error}* if the instance could not be built -// **Returns**: -// *{VIE.Entity}* : A **new** instance of a ```VIE.Entity``` with the current type. -// **Example usage**: -// -// var person = new vie.Type("person"); -// var sebastian = person.instance( -// {"@subject" : "#me", -// "name" : "Sebastian"}); -// console.log(sebastian.get("name")); // <-- "Sebastian" - this.instance = function (attrs, opts) { - attrs = (attrs)? attrs : {}; - opts = (opts)? opts : {}; - - /* turn type/attribute checking on by default! */ - if (opts.typeChecking !== false) { - for (var a in attrs) { - if (a.indexOf('@') !== 0 && !this.attributes.get(a)) { - throw new Error("Cannot create an instance of " + this.id + " as the type does not allow an attribute '" + a + "'!"); - } - } - } - - if (attrs['@type']) { - attrs['@type'].push(this.id); - } else { - attrs['@type'] = this.id; - } - - return new this.vie.Entity(attrs, opts); - }; - -// ### toString() -// This method returns the id of the type. -// **Parameters**: -// *nothing* -// **Throws**: -// *nothing* -// **Returns**: -// *{string}* : The id of the type. -// **Example usage**: -// -// var x = new vie.Type(...); -// x.toString() === x.id; - this.toString = function () { - return this.id; - }; -}; - -// ### VIE.Types() -// This is the constructor of a VIE.Types. This is a convenience class -// to store ```VIE.Type``` instances properly. -// **Parameters**: -// *nothing* -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Types}* : A **new** VIE.Types object. -// **Example usage**: -// -// var types = new vie.Types(); -VIE.prototype.Types = function () { - - this._types = {}; - -// ### add(id, attrs, metadata) -// This method adds a `VIE.Type` to the types. -// **Parameters**: -// *{string|VIE.Type}* **id** If this is a string, the type is created and directly added. -// *{string|object}* **attrs** Only used if ```id``` is a string. -// *{object}* **metadata** potential additional metadata about the type. -// **Throws**: -// *{Error}* if a type with the given id already exists a ```VIE.Entity``` instance from this type. -// **Returns**: -// *{VIE.Types}* : The instance itself. -// **Example usage**: -// -// var types = new vie.Types(); -// types.add("Person", ["name", "knows"]); - this.add = function (id, attrs, metadata) { - if (_.isArray(id)) { - _.each(id, function (type) { - this.add(type); - }, this); - return this; - } - - if (this.get(id)) { - throw new Error("Type '" + id + "' already registered."); - } else { - if (typeof id === "string") { - var t = new this.vie.Type(id, attrs, metadata); - this._types[t.id] = t; - return t; - } else if (id instanceof this.vie.Type) { - this._types[id.id] = id; - return id; - } else { - throw new Error("Wrong argument to VIE.Types.add()!"); - } - } - return this; - }; - -// ### addOrOverwrite(id, attrs) -// This method adds or overwrites a `VIE.Type` to the types. This is the same as -// ``this.remove(id); this.add(id, attrs);`` -// **Parameters**: -// *{string|VIE.Type}* **id** If this is a string, the type is created and directly added. -// *{string|object}* **attrs** Only used if ```id``` is a string. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Types}* : The instance itself. -// **Example usage**: -// -// var types = new vie.Types(); -// types.addOrOverwrite("Person", ["name", "knows"]); - this.addOrOverwrite = function(id, attrs){ - if (this.get(id)) { - this.remove(id); - } - return this.add(id, attrs); - }; - -// ### get(id) -// This method retrieves a `VIE.Type` from the types by it's id. -// **Parameters**: -// *{string|VIE.Type}* **id** The id or the type itself. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Type}* : The instance of the type or ```undefined```. -// **Example usage**: -// -// var types = new vie.Types(); -// types.addOrOverwrite("Person", ["name", "knows"]); -// types.get("Person"); - this.get = function (id) { - if (!id) { - return undefined; - } - if (typeof id === 'string') { - var lid = this.vie.namespaces.isUri(id) ? id : this.vie.namespaces.uri(id); - return this._types[lid]; - } else if (id instanceof this.vie.Type) { - return this.get(id.id); - } - return undefined; - }; - -// ### remove(id) -// This method removes a type of given id from the type. This also -// removes all children if their only parent were this -// type. Furthermore, this removes the link from the -// super- and subtypes. -// **Parameters**: -// *{string|VIE.Type}* **id** The id or the type itself. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Type}* : The removed type. -// **Example usage**: -// -// var types = new vie.Types(); -// types.addOrOverwrite("Person", ["name", "knows"]); -// types.remove("Person"); - this.remove = function (id) { - var t = this.get(id); - /* test whether the type actually exists in VIE - * and prevents removing *owl:Thing*. - */ - if (!t) { - return this; - } - if (!t || t.subsumes("owl:Thing")) { - console.warn("You are not allowed to remove 'owl:Thing'."); - return this; - } - delete this._types[t.id]; - - var subtypes = t.subtypes.list(); - for (var c = 0; c < subtypes.length; c++) { - var childObj = subtypes[c]; - if (childObj.supertypes.list().length === 1) { - /* recursively remove all children - that inherit only from this type */ - this.remove(childObj); - } else { - childObj.supertypes.remove(t.id); - } - } - return t; - }; - -// ### toArray() === list() -// This method returns an array of all types. -// **Parameters**: -// *nothing* -// **Throws**: -// *nothing* -// **Returns**: -// *{array}* : An array of ```VIE.Type``` instances. -// **Example usage**: -// -// var types = new vie.Types(); -// types.addOrOverwrite("Person", ["name", "knows"]); -// types.list(); - this.toArray = this.list = function () { - var ret = []; - for (var i in this._types) { - ret.push(this._types[i]); - } - return ret; - }; - -// ### sort(types, desc) -// This method sorts an array of types in their order, given by the -// inheritance. This returns a copy and leaves the original array untouched. -// **Parameters**: -// *{array|VIE.Type}* **types** The array of ```VIE.Type``` instances or ids of types to be sorted. -// *{boolean}* **desc** If 'desc' is given and 'true', the array will be sorted -// in descendant order. -// *nothing* -// **Throws**: -// *nothing* -// **Returns**: -// *{array}* : A sorted copy of the array. -// **Example usage**: -// -// var types = new vie.Types(); -// types.addOrOverwrite("Person", ["name", "knows"]); -// types.sort(types.list(), true); - this.sort = function (types, desc) { - var self = this; - types = (jQuery.isArray(types))? types : [ types ]; - desc = (desc)? true : false; - - if (types.length === 0) return []; - var copy = [ types[0] ]; - var x, tlen; - for (x = 1, tlen = types.length; x < tlen; x++) { - var insert = types[x]; - var insType = self.get(insert); - if (insType) { - for (var y = 0; y < copy.length; y++) { - if (insType.subsumes(copy[y])) { - copy.splice(y,0,insert); - break; - } else if (y === copy.length - 1) { - copy.push(insert); - } - } - } - } - - //unduplicate - for (x = 0; x < copy.length; x++) { - if (copy.lastIndexOf(copy[x]) !== x) { - copy.splice(x, 1); - x--; - } - } - - if (!desc) { - copy.reverse(); - } - return copy; - }; -}; -// VIE - Vienna IKS Editables -// (c) 2011 Henri Bergius, IKS Consortium -// (c) 2011 Sebastian Germesin, IKS Consortium -// (c) 2011 Szaby Grünwald, IKS Consortium -// VIE may be freely distributed under the MIT license. -// For all details and documentation: -// http://viejs.org/ -// - -// ## VIE.Attributes -// Within VIE, we provide special capabilities of handling attributes of types of entites. This -// helps first of all to list all attributes of an entity type, but furthermore fully supports -// inheritance of attributes from the type-class to inherit from. -if (VIE.prototype.Attribute) { - throw new Error("ERROR: VIE.Attribute is already defined. Please check your VIE installation!"); -} -if (VIE.prototype.Attributes) { - throw new Error("ERROR: VIE.Attributes is already defined. Please check your VIE installation!"); -} - -// ### VIE.Attribute(id, range, domain, minCount, maxCount, metadata) -// This is the constructor of a VIE.Attribute. -// **Parameters**: -// *{string}* **id** The id of the attribute. -// *{string|array}* **range** A string or an array of strings of the target range of -// the attribute. -// *{string}* **domain** The domain of the attribute. -// *{number}* **minCount** The minimal number this attribute can occur. (needs to be >= 0) -// *{number}* **maxCount** The maximal number this attribute can occur. (needs to be >= minCount, use `-1` for unlimited) -// *{object}* **metadata** Possible metadata about the attribute -// **Throws**: -// *{Error}* if one of the given paramenters is missing. -// **Returns**: -// *{VIE.Attribute}* : A **new** VIE.Attribute object. -// **Example usage**: -// -// var knowsAttr = new vie.Attribute("knows", ["Person"], "Person", 0, 10); -// // Creates an attribute to describe a *knows*-relationship -// // between persons. Each person can only have -VIE.prototype.Attribute = function (id, range, domain, minCount, maxCount, metadata) { - if (id === undefined || typeof id !== 'string') { - throw new Error("The attribute constructor needs an 'id' of type string! E.g., 'Person'"); - } - if (range === undefined) { - throw new Error("The attribute constructor of " + id + " needs 'range'."); - } - if (domain === undefined) { - throw new Error("The attribute constructor of " + id + " needs a 'domain'."); - } - - this._domain = domain; - -// ### id -// This field stores the id of the attribute's instance. -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{string}* : A URI, representing the id of the attribute. -// **Example usage**: -// -// var knowsAttr = new vie.Attribute("knows", ["Person"], "Person"); -// console.log(knowsAttr.id); -// // --> - this.id = this.vie.namespaces.isUri(id) ? id : this.vie.namespaces.uri(id); - -// ### range -// This field stores the ranges of the attribute's instance. -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{array}* : An array of strings which represent the types. -// **Example usage**: -// -// var knowsAttr = new vie.Attribute("knows", ["Person"], "Person"); -// console.log(knowsAttr.range); -// // --> ["Person"] - this.range = (_.isArray(range))? range : [ range ]; - -// ### min -// This field stores the minimal amount this attribute can occur in the type's instance. The number -// needs to be greater or equal to zero. -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{int}* : The minimal amount this attribute can occur. -// **Example usage**: -// -// console.log(person.min); -// // --> 0 - minCount = minCount ? minCount : 0; - this.min = (minCount > 0) ? minCount : 0; - -// ### max -// This field stores the maximal amount this attribute can occur in the type's instance. -// This number cannot be smaller than min -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{int}* : The maximal amount this attribute can occur. -// **Example usage**: -// -// console.log(person.max); -// // --> 1.7976931348623157e+308 - maxCount = maxCount ? maxCount : 1; - if (maxCount === -1) { - maxCount = Number.MAX_VALUE; - } - this.max = (maxCount >= this.min)? maxCount : this.min; - -// ### metadata -// This field holds potential metadata about the attribute. - this.metadata = metadata ? metadata : {}; - -// ### applies(range) -// This method checks, whether the current attribute applies in the given range. -// If ```range``` is a string and cannot be transformed into a ```VIE.Type```, -// this performs only string comparison, if it is a VIE.Type -// or an ID of a VIE.Type, then inheritance is checked as well. -// **Parameters**: -// *{string|VIE.Type}* **range** The ```VIE.Type``` (or it's string representation) to be checked. -// **Throws**: -// nothing -// **Returns**: -// *{boolean}* : ```true``` if the given type applies to this attribute and ```false``` otherwise. -// **Example usage**: -// -// var knowsAttr = new vie.Attribute("knows", ["Person"], "Person"); -// console.log(knowsAttr.applies("Person")); // --> true -// console.log(knowsAttr.applies("Place")); // --> false - this.applies = function (range) { - if (this.vie.types.get(range)) { - range = this.vie.types.get(range); - } - for (var r = 0, len = this.range.length; r < len; r++) { - var x = this.vie.types.get(this.range[r]); - if (x === undefined && typeof range === "string") { - if (range === this.range[r]) { - return true; - } - } - else { - if (range.isof(this.range[r])) { - return true; - } - } - } - return false; - }; - -}; - -// ## VIE.Attributes(domain, attrs) -// This is the constructor of a VIE.Attributes. Basically a convenience class -// that represents a list of ```VIE.Attribute```. As attributes are part of a -// certain ```VIE.Type```, it needs to be passed for inheritance checks. -// **Parameters**: -// *{string}* **domain** The domain of the attributes (the type they will be part of). -// *{string|VIE.Attribute|array}* **attrs** Either a string representation of an attribute, -// a proper instance of ```VIE.Attribute``` or an array of both. -// *{string}* **domain** The domain of the attribute. -// **Throws**: -// *{Error}* if one of the given paramenters is missing. -// **Returns**: -// *{VIE.Attribute}* : A **new** VIE.Attribute instance. -// **Example usage**: -// -// var knowsAttr = new vie.Attribute("knows", ["Person"], "Person"); -// var personAttrs = new vie.Attributes("Person", knowsAttr); -VIE.prototype.Attributes = function (domain, attrs) { - - this._local = {}; - this._attributes = {}; - -// ### domain -// This field stores the domain of the attributes' instance. -// **Parameters**: -// nothing -// **Throws**: -// nothing -// **Returns**: -// *{string}* : The string representation of the domain. -// **Example usage**: -// -// console.log(personAttrs.domain); -// // --> ["Person"] - this.domain = domain; - -// ### add(id, range, min, max, metadata) -// This method adds a ```VIE.Attribute``` to the attributes instance. -// **Parameters**: -// *{string|VIE.Attribute}* **id** The string representation of an attribute, or a proper -// instance of a ```VIE.Attribute```. -// *{string|array}* **range** An array representing the target range of the attribute. -// *{number}* **min** The minimal amount this attribute can appear. -// instance of a ```VIE.Attribute```. -// *{number}* **max** The maximal amount this attribute can appear. -// *{object}* **metadata** Additional metadata for the attribute. -// **Throws**: -// *{Error}* If an atribute with the given id is already registered. -// *{Error}* If the ```id``` parameter is not a string, nor a ```VIE.Type``` instance. -// **Returns**: -// *{VIE.Attribute}* : The generated or passed attribute. -// **Example usage**: -// -// personAttrs.add("name", "Text", 0, 1); - this.add = function (id, range, min, max, metadata) { - if (_.isArray(id)) { - _.each(id, function (attribute) { - this.add(attribute); - }, this); - return this; - } - - if (this.get(id)) { - throw new Error("Attribute '" + id + "' already registered for domain " + this.domain.id + "!"); - } else { - if (typeof id === "string") { - var a = new this.vie.Attribute(id, range, this.domain, min, max, metadata); - this._local[a.id] = a; - return a; - } else if (id instanceof this.vie.Attribute) { - id.domain = this.domain; - id.vie = this.vie; - this._local[id.id] = id; - return id; - } else { - throw new Error("Wrong argument to VIE.Types.add()!"); - } - } - }; - -// ### remove(id) -// This method removes a ```VIE.Attribute``` from the attributes instance. -// **Parameters**: -// *{string|VIE.Attribute}* **id** The string representation of an attribute, or a proper -// instance of a ```VIE.Attribute```. -// **Throws**: -// *{Error}* When the attribute is inherited from a parent ```VIE.Type``` and thus cannot be removed. -// **Returns**: -// *{VIE.Attribute}* : The removed attribute. -// **Example usage**: -// -// personAttrs.remove("knows"); - this.remove = function (id) { - var a = this.get(id); - if (a.id in this._local) { - delete this._local[a.id]; - return a; - } - throw new Error("The attribute " + id + " is inherited and cannot be removed from the domain " + this.domain.id + "!"); - }; - -// ### get(id) -// This method returns a ```VIE.Attribute``` from the attributes instance by it's id. -// **Parameters**: -// *{string|VIE.Attribute}* **id** The string representation of an attribute, or a proper -// instance of a ```VIE.Attribute```. -// **Throws**: -// *{Error}* When the method is called with an unknown datatype. -// **Returns**: -// *{VIE.Attribute}* : The attribute. -// **Example usage**: -// -// personAttrs.get("knows"); - this.get = function (id) { - if (typeof id === 'string') { - var lid = this.vie.namespaces.isUri(id) ? id : this.vie.namespaces.uri(id); - return this._inherit()._attributes[lid]; - } else if (id instanceof this.vie.Attribute) { - return this.get(id.id); - } else { - throw new Error("Wrong argument in VIE.Attributes.get()"); - } - }; - -// ### _inherit() -// The private method ```_inherit``` creates a full list of all attributes. This includes -// local attributes as well as inherited attributes from the parents. The ranges of attributes -// with the same id will be merged. This method is called everytime an attribute is requested or -// the list of all attributes. Usually this method should not be invoked outside of the class. -// **Parameters**: -// *nothing* -// instance of a ```VIE.Attribute```. -// **Throws**: -// *nothing* -// **Returns**: -// *nothing* -// **Example usage**: -// -// personAttrs._inherit(); - this._inherit = function () { - var a, x, id; - var attributes = jQuery.extend(true, {}, this._local); - - var inherited = _.map(this.domain.supertypes.list(), - function (x) { - return x.attributes; - } - ); - - var add = {}; - var merge = {}; - var ilen, alen; - for (a = 0, ilen = inherited.length; a < ilen; a++) { - var attrs = inherited[a].list(); - for (x = 0, alen = attrs.length; x < alen; x++) { - id = attrs[x].id; - if (!(id in attributes)) { - if (!(id in add) && !(id in merge)) { - add[id] = attrs[x]; - } - else { - if (!merge[id]) { - merge[id] = {range : [], mins : [], maxs: [], metadatas: []}; - } - if (id in add) { - merge[id].range = jQuery.merge(merge[id].range, add[id].range); - merge[id].mins = jQuery.merge(merge[id].mins, [ add[id].min ]); - merge[id].maxs = jQuery.merge(merge[id].maxs, [ add[id].max ]); - merge[id].metadatas = jQuery.merge(merge[id].metadatas, [ add[id].metadata ]); - delete add[id]; - } - merge[id].range = jQuery.merge(merge[id].range, attrs[x].range); - merge[id].mins = jQuery.merge(merge[id].mins, [ attrs[x].min ]); - merge[id].maxs = jQuery.merge(merge[id].maxs, [ attrs[x].max ]); - merge[id].metadatas = jQuery.merge(merge[id].metadatas, [ attrs[x].metadata ]); - merge[id].range = _.uniq(merge[id].range); - merge[id].mins = _.uniq(merge[id].mins); - merge[id].maxs = _.uniq(merge[id].maxs); - merge[id].metadatas = _.uniq(merge[id].metadatas); - } - } - } - } - - /* adds inherited attributes that do not need to be merged */ - jQuery.extend(attributes, add); - - /* merges inherited attributes */ - for (id in merge) { - var mranges = merge[id].range; - var mins = merge[id].mins; - var maxs = merge[id].maxs; - var metadatas = merge[id].metadatas; - var ranges = []; - //merging ranges - for (var r = 0, mlen = mranges.length; r < mlen; r++) { - var p = this.vie.types.get(mranges[r]); - var isAncestorOf = false; - if (p) { - for (x = 0; x < mlen; x++) { - if (x === r) { - continue; - } - var c = this.vie.types.get(mranges[x]); - if (c && c.isof(p)) { - isAncestorOf = true; - break; - } - } - } - if (!isAncestorOf) { - ranges.push(mranges[r]); - } - } - - var maxMin = _.max(mins); - var minMax = _.min(maxs); - if (maxMin <= minMax && minMax >= 0 && maxMin >= 0) { - attributes[id] = new this.vie.Attribute(id, ranges, this, maxMin, minMax, metadatas[0]); - } else { - throw new Error("This inheritance is not allowed because of an invalid minCount/maxCount pair!"); - } - } - - this._attributes = attributes; - return this; - }; - -// ### toArray() === list() -// This method return an array of ```VIE.Attribute```s from the attributes instance. -// **Parameters**: -// *nothing. -// **Throws**: -// *nothing* -// **Returns**: -// *{array}* : An array of ```VIE.Attribute```. -// **Example usage**: -// -// personAttrs.list(); - this.toArray = this.list = function (range) { - var ret = []; - var attributes = this._inherit()._attributes; - for (var a in attributes) { - if (!range || attributes[a].applies(range)) { - ret.push(attributes[a]); - } - } - return ret; - }; - - attrs = _.isArray(attrs) ? attrs : [ attrs ]; - _.each(attrs, function (attr) { - this.add(attr.id, attr.range, attr.min, attr.max, attr.metadata); - }, this); -}; -// VIE - Vienna IKS Editables -// (c) 2011 Henri Bergius, IKS Consortium -// (c) 2011 Sebastian Germesin, IKS Consortium -// (c) 2011 Szaby Grünwald, IKS Consortium -// VIE may be freely distributed under the MIT license. -// For all details and documentation: -// http://viejs.org/ -if (VIE.prototype.Namespaces) { - throw new Error("ERROR: VIE.Namespaces is already defined. " + - "Please check your VIE installation!"); -} - -// ## VIE Namespaces -// -// In general, a namespace is a container that provides context for the identifiers. -// Within VIE, namespaces are used to distinguish different ontolgies or vocabularies -// of identifiers, types and attributes. However, because of their verbosity, namespaces -// tend to make their usage pretty circuitous. The ``VIE.Namespaces(...)`` class provides VIE -// with methods to maintain abbreviations (akak **prefixes**) for namespaces in order to -// alleviate their usage. By default, every VIE instance is equipped with a main instance -// of the namespaces in ``myVIE.namespaces``. Furthermore, VIE uses a **base namespace**, -// which is used if no prefix is given (has an empty prefix). -// In the upcoming sections, we will explain the -// methods to add, access and remove prefixes. - - - -// ## VIE.Namespaces(base, namespaces) -// This is the constructor of a VIE.Namespaces. The constructor initially -// needs a *base namespace* and can optionally be initialised with an -// associative array of prefixes and namespaces. The base namespace is used in a way -// that every non-prefixed, non-expanded attribute or type is assumed to be of that -// namespace. This helps, e.g., in an environment where only one namespace is given. -// **Parameters**: -// *{string}* **base** The base namespace. -// *{object}* **namespaces** Initial namespaces to bootstrap the namespaces. (optional) -// **Throws**: -// *{Error}* if the base namespace is missing. -// **Returns**: -// *{VIE.Attribute}* : A **new** VIE.Attribute object. -// **Example usage**: -// -// var ns = new myVIE.Namespaces("http://viejs.org/ns/", -// { -// "foaf": "http://xmlns.com/foaf/0.1/" -// }); -VIE.prototype.Namespaces = function (base, namespaces) { - - if (!base) { - throw new Error("Please provide a base namespace!"); - } - this._base = base; - - this._namespaces = (namespaces)? namespaces : {}; - if (typeof this._namespaces !== "object" || _.isArray(this._namespaces)) { - throw new Error("If you want to initialise VIE namespace prefixes, " + - "please provide a proper object!"); - } -}; - - -// ### base(ns) -// This is a **getter** and **setter** for the base -// namespace. If called like ``base();`` it -// returns the actual base namespace as a string. If provided -// with a string, e.g., ``base("http://viejs.org/ns/");`` -// it sets the current base namespace and retuns the namespace object -// for the purpose of chaining. If provided with anything except a string, -// it throws an Error. -// **Parameters**: -// *{string}* **ns** The namespace to be set. (optional) -// **Throws**: -// *{Error}* if the namespace is not of type string. -// **Returns**: -// *{string}* : The current base namespace. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// console.log(namespaces.base()); // <-- "http://base.ns/" -// namespaces.base("http://viejs.org/ns/"); -// console.log(namespaces.base()); // <-- "http://viejs.org/ns/" -VIE.prototype.Namespaces.prototype.base = function (ns) { - if (!ns) { - return this._base; - } - else if (typeof ns === "string") { - /* remove another mapping */ - this.removeNamespace(ns); - this._base = ns; - return this._base; - } else { - throw new Error("Please provide a valid namespace!"); - } -}; - -// ### add(prefix, namespace) -// This method adds new prefix mappings to the -// current instance. If a prefix or a namespace is already -// present (in order to avoid ambiguities), an Error is thrown. -// ``prefix`` can also be an object in which case, the method -// is called sequentially on all elements. -// **Parameters**: -// *{string|object}* **prefix** The prefix to be set. If it is an object, the -// method will be applied to all key,value pairs sequentially. -// *{string}* **namespace** The namespace to be set. -// **Throws**: -// *{Error}* If a prefix or a namespace is already -// present (in order to avoid ambiguities). -// **Returns**: -// *{VIE.Namespaces}* : The current namespaces instance. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.add("", "http://..."); -// // is always equal to -// namespaces.base("http://..."); // <-- setter of base namespace -VIE.prototype.Namespaces.prototype.add = function (prefix, namespace) { - if (typeof prefix === "object") { - for (var k1 in prefix) { - this.add(k1, prefix[k1]); - } - return this; - } - if (prefix === "") { - this.base(namespace); - return this; - } - /* checking if we overwrite existing mappings */ - else if (this.contains(prefix) && namespace !== this._namespaces[prefix]) { - throw new Error("ERROR: Trying to register namespace prefix mapping (" + prefix + "," + namespace + ")!" + - "There is already a mapping existing: '(" + prefix + "," + this.get(prefix) + ")'!"); - } else { - jQuery.each(this._namespaces, function (k1,v1) { - if (v1 === namespace && k1 !== prefix) { - throw new Error("ERROR: Trying to register namespace prefix mapping (" + prefix + "," + namespace + ")!" + - "There is already a mapping existing: '(" + k1 + "," + namespace + ")'!"); - } - }); - } - /* if not, just add them */ - this._namespaces[prefix] = namespace; - return this; -}; - -// ### addOrReplace(prefix, namespace) -// This method adds new prefix mappings to the -// current instance. This will overwrite existing mappings. -// **Parameters**: -// *{string|object}* **prefix** The prefix to be set. If it is an object, the -// method will be applied to all key,value pairs sequentially. -// *{string}* **namespace** The namespace to be set. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Namespaces}* : The current namespaces instance. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.addOrReplace("", "http://..."); -// // is always equal to -// namespaces.base("http://..."); // <-- setter of base namespace -VIE.prototype.Namespaces.prototype.addOrReplace = function (prefix, namespace) { - if (typeof prefix === "object") { - for (var k1 in prefix) { - this.addOrReplace(k1, prefix[k1]); - } - return this; - } - this.remove(prefix); - this.removeNamespace(namespace); - return this.add(prefix, namespace); -}; - -// ### get(prefix) -// This method retrieves a namespaces, given a prefix. If the -// prefix is the empty string, the base namespace is returned. -// **Parameters**: -// *{string}* **prefix** The prefix to be retrieved. -// **Throws**: -// *nothing* -// **Returns**: -// *{string|undefined}* : The namespace or ```undefined``` if no namespace could be found. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.addOrReplace("test", "http://test.ns"); -// console.log(namespaces.get("test")); // <-- "http://test.ns" -VIE.prototype.Namespaces.prototype.get = function (prefix) { - if (prefix === "") { - return this.base(); - } - return this._namespaces[prefix]; -}; - -// ### getPrefix(namespace) -// This method retrieves a prefix, given a namespace. -// **Parameters**: -// *{string}* **namespace** The namespace to be retrieved. -// **Throws**: -// *nothing* -// **Returns**: -// *{string|undefined}* : The prefix or ```undefined``` if no prefix could be found. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.addOrReplace("test", "http://test.ns"); -// console.log(namespaces.getPrefix("http://test.ns")); // <-- "test" -VIE.prototype.Namespaces.prototype.getPrefix = function (namespace) { - var prefix; - if (namespace.indexOf('<') === 0) { - namespace = namespace.substring(1, namespace.length - 1); - } - jQuery.each(this._namespaces, function (k1,v1) { - if (namespace.indexOf(v1) === 0) { - prefix = k1; - } - - if (namespace.indexOf(k1 + ':') === 0) { - prefix = k1; - } - }); - return prefix; -}; - -// ### contains(prefix) -// This method checks, whether a prefix is stored in the instance. -// **Parameters**: -// *{string}* **prefix** The prefix to be checked. -// **Throws**: -// *nothing* -// **Returns**: -// *{boolean}* : ```true``` if the prefix could be found, ```false``` otherwise. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.addOrReplace("test", "http://test.ns"); -// console.log(namespaces.contains("test")); // <-- true -VIE.prototype.Namespaces.prototype.contains = function (prefix) { - return (prefix in this._namespaces); -}; - -// ### containsNamespace(namespace) -// This method checks, whether a namespace is stored in the instance. -// **Parameters**: -// *{string}* **namespace** The namespace to be checked. -// **Throws**: -// *nothing* -// **Returns**: -// *{boolean}* : ```true``` if the namespace could be found, ```false``` otherwise. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.addOrReplace("test", "http://test.ns"); -// console.log(namespaces.containsNamespace("http://test.ns")); // <-- true -VIE.prototype.Namespaces.prototype.containsNamespace = function (namespace) { - return this.getPrefix(namespace) !== undefined; -}; - -// ### update(prefix, namespace) -// This method overwrites the namespace that is stored under the -// prefix ``prefix`` with the new namespace ``namespace``. -// If a namespace is already bound to another prefix, an Error is thrown. -// **Parameters**: -// *{string}* **prefix** The prefix. -// *{string}* **namespace** The namespace. -// **Throws**: -// *{Error}* If a namespace is already bound to another prefix. -// **Returns**: -// *{VIE.Namespaces}* : The namespace instance. -// **Example usage**: -// -// ... -VIE.prototype.Namespaces.prototype.update = function (prefix, namespace) { - this.remove(prefix); - return this.add(prefix, namespace); -}; - -// ### updateNamespace(prefix, namespace) -// This method overwrites the prefix that is bound to the -// namespace ``namespace`` with the new prefix ``prefix``. If another namespace is -// already registered with the given ``prefix``, an Error is thrown. -// **Parameters**: -// *{string}* **prefix** The prefix. -// *{string}* **namespace** The namespace. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Namespaces}* : The namespace instance. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.add("test", "http://test.ns"); -// namespaces.updateNamespace("test2", "http://test.ns"); -// namespaces.get("test2"); // <-- "http://test.ns" -VIE.prototype.Namespaces.prototype.updateNamespace = function (prefix, namespace) { - this.removeNamespace(prefix); - return this.add(prefix, namespace); -}; - -// ### remove(prefix) -// This method removes the namespace that is stored under the prefix ``prefix``. -// **Parameters**: -// *{string}* **prefix** The prefix to be removed. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Namespaces}* : The namespace instance. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.add("test", "http://test.ns"); -// namespaces.get("test"); // <-- "http://test.ns" -// namespaces.remove("test"); -// namespaces.get("test"); // <-- undefined -VIE.prototype.Namespaces.prototype.remove = function (prefix) { - if (prefix) { - delete this._namespaces[prefix]; - } - return this; -}; - -// ### removeNamespace(namespace) -// This method removes removes the namespace ``namespace`` from the instance. -// **Parameters**: -// *{string}* **namespace** The namespace to be removed. -// **Throws**: -// *nothing* -// **Returns**: -// *{VIE.Namespaces}* : The namespace instance. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.add("test", "http://test.ns"); -// namespaces.get("test"); // <-- "http://test.ns" -// namespaces.removeNamespace("http://test.ns"); -// namespaces.get("test"); // <-- undefined -VIE.prototype.Namespaces.prototype.removeNamespace = function (namespace) { - var prefix = this.getPrefix(namespace); - if (prefix) { - delete this._namespaces[prefix]; - } - return this; -}; - -// ### toObj() -// This method serializes the namespace instance into an associative -// array representation. The base namespace is given an empty -// string as key. -// **Parameters**: -// *{boolean}* **omitBase** If set to ```true``` this omits the baseNamespace. -// **Throws**: -// *nothing* -// **Returns**: -// *{object}* : A serialization of the namespaces as an object. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.add("test", "http://test.ns"); -// console.log(namespaces.toObj()); -// // <-- {"" : "http://base.ns/", -// "test": "http://test.ns"} -// console.log(namespaces.toObj(true)); -// // <-- {"test": "http://test.ns"} -VIE.prototype.Namespaces.prototype.toObj = function (omitBase) { - if (omitBase) { - return jQuery.extend({}, this._namespaces); - } - return jQuery.extend({'' : this._base}, this._namespaces); -}; - -// ### curie(uri, safe) -// This method converts a given -// URI into a CURIE (or SCURIE), based on the given ```VIE.Namespaces``` object. -// If the given uri is already a URI, it is left untouched and directly returned. -// If no prefix could be found, an ```Error``` is thrown. -// **Parameters**: -// *{string}* **uri** The URI to be transformed. -// *{boolean}* **safe** A flag whether to generate CURIEs or SCURIEs. -// **Throws**: -// *{Error}* If no prefix could be found in the passed namespaces. -// **Returns**: -// *{string}* The CURIE or SCURIE. -// **Example usage**: -// -// var ns = new myVIE.Namespaces( -// "http://viejs.org/ns/", -// { "dbp": "http://dbpedia.org/ontology/" } -// ); -// var uri = ""; -// ns.curie(uri, false); // --> dbp:Person -// ns.curie(uri, true); // --> [dbp:Person] -VIE.prototype.Namespaces.prototype.curie = function(uri, safe){ - return VIE.Util.toCurie(uri, safe, this); -}; - -// ### isCurie(curie) -// This method checks, whether -// the given string is a CURIE and returns ```true``` if so and ```false```otherwise. -// **Parameters**: -// *{string}* **curie** The CURIE (or SCURIE) to be checked. -// **Throws**: -// *nothing* -// **Returns**: -// *{boolean}* ```true``` if the given curie is a CURIE or SCURIE and ```false``` otherwise. -// **Example usage**: -// -// var ns = new myVIE.Namespaces( -// "http://viejs.org/ns/", -// { "dbp": "http://dbpedia.org/ontology/" } -// ); -// var uri = ""; -// var curie = "dbp:Person"; -// var scurie = "[dbp:Person]"; -// var text = "This is some text."; -// ns.isCurie(uri); // --> false -// ns.isCurie(curie); // --> true -// ns.isCurie(scurie); // --> true -// ns.isCurie(text); // --> false -VIE.prototype.Namespaces.prototype.isCurie = function (something) { - return VIE.Util.isCurie(something, this); -}; - -// ### uri(curie) -// This method converts a -// given CURIE (or save CURIE) into a URI, based on the given ```VIE.Namespaces``` object. -// **Parameters**: -// *{string}* **curie** The CURIE to be transformed. -// **Throws**: -// *{Error}* If no URI could be assembled. -// **Returns**: -// *{string}* : A string, representing the URI. -// **Example usage**: -// -// var ns = new myVIE.Namespaces( -// "http://viejs.org/ns/", -// { "dbp": "http://dbpedia.org/ontology/" } -// ); -// var curie = "dbp:Person"; -// var scurie = "[dbp:Person]"; -// ns.uri(curie); -// --> -// ns.uri(scurie); -// --> -VIE.prototype.Namespaces.prototype.uri = function (curie) { - return VIE.Util.toUri(curie, this); -}; - -// ### isUri(something) -// This method checks, whether the given string is a URI. -// **Parameters**: -// *{string}* **something** : The string to be checked. -// **Throws**: -// *nothing* -// **Returns**: -// *{boolean}* : ```true``` if the string is a URI, ```false``` otherwise. -// **Example usage**: -// -// var namespaces = new vie.Namespaces("http://base.ns/"); -// namespaces.addOrReplace("test", "http://test.ns"); -// var uri = ""; -// var curie = "test:Person"; -// namespaces.isUri(uri); // --> true -// namespaces.isUri(curie); // --> false -VIE.prototype.Namespaces.prototype.isUri = VIE.Util.isUri; -/*global VIE:false Backbone:false _:false */ -if (!VIE.prototype.view) { - VIE.prototype.view = {}; -} - -VIE.prototype.view.Entity = Backbone.View.extend({ - initialize: function(options) { - this.service = options.service ? options.service : 'rdfa'; - this.vie = options.vie; - - // Ensure view gets updated when properties of the Entity change. - _.bindAll(this, 'render', 'renderAbout'); - this.model.on('change', this.render); - this.model.on('change:@subject', this.renderAbout); - }, - - // Rendering a view means writing the properties of the Entity back to - // the element containing our RDFa annotations. - render: function() { - this.vie.save({ - element: this.el, - entity: this.model - }). - to(this.service). - execute(); - return this; - }, - - renderAbout: function () { - this.vie.service(this.service).setElementSubject(this.model.getSubjectUri(), this.el); - } -}); -})(); \ No newline at end of file diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 0c8eb0f..3f13edd 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -53,6 +53,14 @@ function edit_contextual_links_view_alter(&$element, $items) { return; } + $element['#attached']['js'][] = array( + 'type' => 'setting', + 'data' => array('edit' => array( + 'metadataURL' => url('edit/metadata'), + 'fieldFormURL' => url('edit/form/!entity_type/!id/!field_name/!langcode/!view_mode'), + 'context' => 'body', + )), + ); $element['#attached']['library'][] = array('edit', 'edit'); } @@ -66,38 +74,24 @@ function edit_library_info() { ); $libraries['edit'] = array( 'title' => 'Edit: in-place editing', - 'website' => 'http://drupal.org/project/edit', 'version' => VERSION, 'js' => array( // Core. $path . '/js/edit.js' => $options, - $path . '/js/app.js' => $options, // Models. - $path . '/js/models/edit-app-model.js' => $options, + $path . '/js/models/AppModel.js' => $options, + $path . '/js/models/EntityModel.js' => $options, + $path . '/js/models/FieldModel.js' => $options, // Views. - $path . '/js/views/propertyeditordecoration-view.js' => $options, - $path . '/js/views/contextuallink-view.js' => $options, - $path . '/js/views/modal-view.js' => $options, - $path . '/js/views/toolbar-view.js' => $options, - // 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, + $path . '/js/views/AppView.js' => $options, + $path . '/js/views/EditorDecorationView.js' => $options, + $path . '/js/views/ContextualLinkView.js' => $options, + $path . '/js/views/ModalView.js' => $options, + $path . '/js/views/FieldToolbarView.js' => $options, + $path . '/js/views/EditorView.js' => $options, // Other. $path . '/js/util.js' => $options, $path . '/js/theme.js' => $options, - // Basic settings. - array( - 'data' => array('edit' => array( - 'metadataURL' => url('edit/metadata'), - 'fieldFormURL' => url('edit/form/!entity_type/!id/!field_name/!langcode/!view_mode'), - 'context' => 'body', - )), - 'type' => 'setting', - ), ), 'css' => array( $path . '/css/edit.css' => array(), @@ -106,8 +100,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'), @@ -115,20 +107,20 @@ function edit_library_info() { ), ); $libraries['edit.editorWidget.form'] = array( - 'title' => '"Form" Create.js PropertyEditor widget', + 'title' => 'Form in-place editor', 'version' => VERSION, 'js' => array( - $path . '/js/createjs/editingWidgets/formwidget.js' => $options, + $path . '/js/editors/formEditor.js' => $options, ), 'dependencies' => array( array('edit', 'edit'), ), ); $libraries['edit.editorWidget.direct'] = array( - 'title' => '"Direct" Create.js PropertyEditor widget', + 'title' => 'Direct in-place editor', '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 deleted file mode 100644 index 750d16f..0000000 --- a/core/modules/edit/js/backbone.drupalform.js +++ /dev/null @@ -1,184 +0,0 @@ -/** - * @file - * Backbone.sync implementation for Edit. This is the beating heart. - */ -(function (jQuery, Backbone, Drupal) { - -"use strict"; - -Backbone.defaultSync = Backbone.sync; -Backbone.sync = function(method, model, options) { - if (options.editor.options.editorName === 'form') { - return Backbone.syncDrupalFormWidget(method, model, options); - } - else { - return Backbone.syncDirect(method, model, options); - } -}; - -/** - * Performs syncing for "form" PredicateEditor widgets. - * - * Implemented on top of Form API and the AJAX commands framework. Sets up - * scoped AJAX command closures specifically for a given PredicateEditor widget - * (which contains a pre-existing form). By submitting the form through - * Drupal.ajax and leveraging Drupal.ajax' ability to have scoped (per-instance) - * command implementations, we are able to update the VIE model, re-render the - * form when there are validation errors and ensure no Drupal.ajax memory leaks. - * - * @see Drupal.edit.util.form - */ -Backbone.syncDrupalFormWidget = function(method, model, options) { - if (method === 'update') { - var predicate = options.editor.options.property; - - var $formContainer = options.editor.$formContainer; - var $submit = $formContainer.find('.edit-form-submit'); - var base = $submit.attr('id'); - - // Successfully saved. - Drupal.ajax[base].commands.editFieldFormSaved = function(ajax, response, status) { - Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); - - // Call Backbone.sync's success callback with the rerendered field. - var changedAttributes = {}; - // @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216) - // Once full JSON-LD support in Drupal core lands, we can ensure that the - // models that VIE maintains are properly updated. - changedAttributes[predicate] = undefined; - changedAttributes[predicate + '/rendered'] = response.data; - options.success(changedAttributes); - }; - - // Unsuccessfully saved; validation errors. - Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) { - // Call Backbone.sync's error callback with the validation error messages. - options.error(response.data); - }; - - // The edit_field_form AJAX command is only called upon loading the form for - // the first time, and when there are validation errors in the form; Form - // API then marks which form items have errors. Therefor, we have to replace - // the existing form, unbind the existing Drupal.ajax instance and create a - // new Drupal.ajax instance. - Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) { - Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); - - Drupal.ajax.prototype.commands.insert(ajax, { - data: response.data, - selector: '#' + $formContainer.attr('id') + ' form' - }); - - // Create a Drupa.ajax instance for the re-rendered ("new") form. - var $newSubmit = $formContainer.find('.edit-form-submit'); - Drupal.edit.util.form.ajaxifySaving({ nocssjs: false }, $newSubmit); - }; - - // Click the form's submit button; the scoped AJAX commands above will - // handle the server's response. - $submit.trigger('click.edit'); - } -}; - -/** -* Performs syncing for "direct" PredicateEditor widgets. - * - * @see Backbone.syncDrupalFormWidget() - * @see Drupal.edit.util.form - */ -Backbone.syncDirect = function(method, model, options) { - if (method === 'update') { - var fillAndSubmitForm = function(value) { - var $form = jQuery('#edit_backstage form'); - // Fill in the value in any that isn't hidden or a submit button. - $form.find(':input[type!="hidden"][type!="submit"]:not(select)') - // Don't mess with the node summary. - .not('[name$="\[summary\]"]').val(value); - // Submit the form. - $form.find('.edit-form-submit').trigger('click.edit'); - }; - var entity = options.editor.options.entity; - var predicate = options.editor.options.property; - var value = model.get(predicate); - - // If form doesn't already exist, load it and then submit. - if (jQuery('#edit_backstage form').length === 0) { - var formOptions = { - propertyID: Drupal.edit.util.calcPropertyID(entity, predicate), - $editorElement: options.editor.element, - nocssjs: true - }; - Drupal.edit.util.form.load(formOptions, function(form, ajax) { - // Create a backstage area for storing forms that are hidden from view - // (hence "backstage" — since the editing doesn't happen in the form, it - // happens "directly" in the content, the form is only used for saving). - jQuery(Drupal.theme('editBackstage', { id: 'edit_backstage' })).appendTo('body'); - // Direct forms are stuffed into #edit_backstage, apparently. - jQuery('#edit_backstage').append(form); - // Disable the browser's HTML5 validation; we only care about server- - // side validation. (Not disabling this will actually cause problems - // because browsers don't like to set HTML5 validation errors on hidden - // forms.) - jQuery('#edit_backstage form').prop('novalidate', true); - var $submit = jQuery('#edit_backstage form .edit-form-submit'); - var base = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); - - // Successfully saved. - Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) { - Backbone.syncDirectCleanUp(); - - // Call Backbone.sync's success callback with the rerendered field. - var changedAttributes = {}; - // @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216) - // Once full JSON-LD support in Drupal core lands, we can ensure that the - // models that VIE maintains are properly updated. - changedAttributes[predicate] = jQuery(response.data).find('.field-item').html(); - changedAttributes[predicate + '/rendered'] = response.data; - options.success(changedAttributes); - }; - - // Unsuccessfully saved; validation errors. - Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) { - // Call Backbone.sync's error callback with the validation error messages. - options.error(response.data); - }; - - // The editFieldForm AJAX command is only called upon loading the form - // for the first time, and when there are validation errors in the form; - // Form API then marks which form items have errors. This is useful for - // "form" editors, but pointless for "direct" editors: the form itself - // won't be visible at all anyway! Therefor, we ignore the new form and - // we continue to use the existing form. - Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) { - // no-op - }; - - fillAndSubmitForm(value); - }); - } - else { - fillAndSubmitForm(value); - } - } -}; - -/** - * Cleans up the hidden form that Backbone.syncDirect uses for syncing. - * - * This is called automatically by Backbone.syncDirect when saving is successful - * (i.e. when there are no validation errors). Only when editing is canceled - * while a PropertyEditor widget is in the invalid state, this must be called - * "manually" (in practice, ToolbarView does this). This is necessary because - * Backbone.syncDirect is not aware of the application state, it only does the - * syncing. - * An alternative could be to also remove the hidden form when validation errors - * occur, but then the form must be retrieved again, thus resulting in another - * roundtrip, which is bad for front-end performance. - */ -Backbone.syncDirectCleanUp = function() { - var $submit = jQuery('#edit_backstage form .edit-form-submit'); - Drupal.edit.util.form.unajaxifySaving($submit); - jQuery('#edit_backstage form').remove(); -}; - -})(jQuery, Backbone, Drupal); 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 61157c4..ec3473c 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -1,103 +1,105 @@ /** * @file * Attaches behavior for the Edit module. + * + * Everything happens asynchronously, to allow for: + * - dynamically rendered contextual links + * - asynchronously retrieved (and cached) per-field in-place editing metadata + * - asynchronous setup of in-place editable field and "Quick edit" link + * + * To achieve this, there are several queues: + * - fieldsMetadataQueue: fields whose metadata still needs to be fetched. + * - fieldsAvailableQueue: queue of fields whose metadata is known, and for + * which it has been confirmed that the user has permission to edit them. + * However, FieldModels will only be created for them once there's a + * contextual link for their entity: when it's possible to initiate editing. + * - contextualLinksQueue: queue of contextual links on entities for which it + * is not yet known whether the user has permission to edit at >=1 of them. */ (function ($, _, Backbone, Drupal, drupalSettings) { "use strict"; -Drupal.edit = { metadataCache: {}, contextualLinksQueue: [] }; +var options = $.extend({ + strings: { + quickEdit: Drupal.t('Quick edit'), + stopQuickEdit: Drupal.t('Stop quick edit') + } +}, drupalSettings.edit); + +// Tracks fields without metadata. Contains objects with the following keys: +// - DOM el +// - String editID +var fieldsMetadataQueue = []; + +// Tracks fields ready for use. Contains objects with the following keys: +// - DOM el +// - String editID +// - String entityID +var fieldsAvailableQueue = []; + +// Tracks contextual links on entities. Contains objects with the following keys: +// - String entityID +// - DOM el +// - DOM region +var contextualLinksQueue = []; -/** - * Attach toggling behavior and in-place editing. - */ Drupal.behaviors.edit = { attach: function (context) { - var $context = $(context); - var $fields = $context.find('[data-edit-id]'); - - // Initialize the Edit app. - $('body').once('edit-init', Drupal.edit.init); - - function annotateField (field) { - var hasField = _.has(Drupal.edit.metadataCache, field.editID); - if (hasField) { - 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 hasField; - } + // Initialize the Edit app once per page load. + $('body').once('edit-init', initEdit); - // Find all fields in the context without metadata. - var fieldsToAnnotate = _.map($fields.not('.edit-allowed, .edit-disallowed'), function (el) { - var $el = $(el); - return { $el: $el, editID: $el.attr('data-edit-id') }; + // Process each field element: queue to be used or to fetch metadata. + $(context).find('[data-edit-id]').once('edit').each(function (index, fieldElement) { + processField(fieldElement); }); - // 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)) { - result.push(field); - } - return result; - }, []); - - // Make fields that could be annotated immediately available for editing. - Drupal.edit.app.findEditableProperties($context); - - if (remainingFieldsToAnnotate.length) { - $(window).ready(function () { - var id = 'edit-load-metadata'; - // Create a temporary element to be able to use Drupal.ajax. - var $el = jQuery('
').appendTo('body'); - // Create a Drupal.ajax instance to load the form. - Drupal.ajax[id] = new Drupal.ajax(id, $el, { - url: drupalSettings.edit.metadataURL, - event: 'edit-internal.edit', - submit: { 'fields[]': _.pluck(remainingFieldsToAnnotate, 'editID') }, - // No progress indicator. - progress: { type: null } - }); - // Implement a scoped editMetaData AJAX command: calls the callback. - Drupal.ajax[id].commands.editMetadata = function (ajax, response, status) { - // Update the metadata cache. - _.each(response.data, function (metadata, editID) { - Drupal.edit.metadataCache[editID] = metadata; - }); - - // Annotate the remaining fields based on the updated access cache. - _.each(remainingFieldsToAnnotate, annotateField); - - // Find editable fields, make them editable. - Drupal.edit.app.findEditableProperties($context); - - // Metadata cache has been updated, try to set up more contextual - // links now. - Drupal.edit.contextualLinksQueue = _.filter(Drupal.edit.contextualLinksQueue, function (data) { - return !Drupal.edit.setUpContextualLink(data); - }); - - // Delete the Drupal.ajax instance that called this very function. - delete Drupal.ajax[id]; - - // Also delete the temporary element. - // $el.remove(); - }; - // This will ensure our scoped editMetadata AJAX command gets called. - $el.trigger('edit-internal.edit'); + // Fetch metadata for any fields that are queued to retrieve it. + fetchMissingMetadata(function (fieldElementsWithFreshMetadata) { + // Metadata has been fetched, reprocess fields whose metadata was missing. + _.each(fieldElementsWithFreshMetadata, processField); + + // Metadata has been fetched, try to set up more contextual links now. + contextualLinksQueue = _.filter(contextualLinksQueue, function (contextualLink) { + return !initializeEntityContextualLink(contextualLink); }); + }); + }, + detach: function (context, settings, trigger) { + if (trigger === 'unload') { + deleteContainedModelsAndQueues($(context)); } } }; +Drupal.edit = { + // A Drupal.edit.AppView instance. + app: null, + + collections: { + // All in-place editable entities (Drupal.edit.EntityModel) on the page. + entities: null, + // All in-place editable fields (Drupal.edit.FieldModel) on the page. + fields: null + }, + + // In-place editors will register themselves in this object. + editors: {}, + + // Per-field metadata that indicates whether in-place editing is allowed, + // which in-place editor should be used, etc. + Metadata: { + has: function (editID) { return _.has(this.data, editID); }, + add: function (editID, metadata) { this.data[editID] = metadata; }, + get: function (editID, key) { + return (key === undefined) ? this.data[editID] : this.data[editID][key]; + }, + intersection: function (editIDs) { return _.intersection(editIDs, _.keys(this.data)); }, + // Contains the actual metadata, keyed by edit ID. + data: {} + } +}; + /** * Detect contextual links on entities annotated by Edit; queue these to be * processed. @@ -105,19 +107,161 @@ Drupal.behaviors.edit = { $(document).on('drupalContextualLinkAdded', function (event, data) { if (data.$region.is('[data-edit-entity]')) { var contextualLink = { - entity: data.$region.attr('data-edit-entity'), - $el: data.$el, - $region: data.$region + entityID: data.$region.attr('data-edit-entity'), + el: data.$el[0], + region: data.$region[0] }; // Set up contextual links for this, otherwise queue it to be set up later. - if (!Drupal.edit.setUpContextualLink(contextualLink)) { - Drupal.edit.contextualLinksQueue.push(contextualLink); + if (!initializeEntityContextualLink(contextualLink)) { + contextualLinksQueue.push(contextualLink); } } }); /** - * Attempts to set up a "Quick edit" contextual link. + * Extracts the entity ID from an edit ID. + * + * @param String editID + * An edit ID: a string of the format + * `////`. + * @return String + * An entity ID: a string of the format `/`. + */ +function extractEntityID (editID) { + return editID.split('/').slice(0, 2).join('/'); +} + +/** + * Initialize the Edit app. + * + * @param DOM bodyElement + * This document's body element. + */ +function initEdit (bodyElement) { + Drupal.edit.collections.entities = new Drupal.edit.EntityCollection(); + Drupal.edit.collections.fields = new Drupal.edit.FieldCollection(); + + // Instantiate AppModel (application state) and AppView, which is the + // controller of the whole in-place editing experience. + Drupal.edit.app = new Drupal.edit.AppView({ + el: bodyElement, + model: new Drupal.edit.AppModel(), + entitiesCollection: Drupal.edit.collections.entities, + fieldsCollection: Drupal.edit.collections.fields + }); +} + +/** + * Fetch the field's metadata; queue or initialize it (if EntityModel exists). + * + * @param DOM fieldElement + * A Drupal Field API field's DOM element with a data-edit-id attribute. + */ +function processField (fieldElement) { + var metadata = Drupal.edit.Metadata; + var editID = fieldElement.getAttribute('data-edit-id'); + + // Early-return if metadata for this field is mising. + if (!metadata.has(editID)) { + fieldsMetadataQueue.push({ el: fieldElement, editID: editID }); + return; + } + // Early-return if the user is not allowed to in-place edit this field. + if (metadata.get(editID, 'access') !== true) { + return; + } + + // If an EntityModel for this field already exists (and hence also a "Quick + // edit" contextual link), then initialize it immediately. + var entityID = extractEntityID(editID); + if (Drupal.edit.collections.entities.where({ id: entityID }).length > 0) { + initializeField(fieldElement, editID); + } + // Otherwise: queue the field. It is now available to be set up when its + // corresponding entity becomes in-place editable. + else { + fieldsAvailableQueue.push({ el: fieldElement, editID: editID, entityID: entityID }); + } +} + +/** + * Initialize a field: create FieldModel. + * + * @param DOM fieldElement + * The field's DOM element. + * @param String editID + * The field's edit ID. + */ +function initializeField (fieldElement, editID) { + var entityId = extractEntityID(editID); + var entity = Drupal.edit.collections.entities.where({ id: entityId })[0]; + + // @todo Refactor CSS to get rid of this. + $(fieldElement).addClass('edit-field'); + + // The FieldModel stores the state of an in-place editable entity field. + var field = new Drupal.edit.FieldModel({ + entity: entity, + el: fieldElement, + editID: editID, + // @todo just pass the entire bit of metadata? + label: Drupal.edit.Metadata.get(editID, 'label'), + editor: Drupal.edit.Metadata.get(editID, 'editor'), + html: fieldElement.outerHTML, + acceptStateChange: _.bind(Drupal.edit.app.acceptEditorStateChange, Drupal.edit.app) + }); + + // Track all fields on the page. + Drupal.edit.collections.fields.add(field); +} + +/** + * Fetches metadata for fields whose metadata is missing. + * + * Fields whose metadata is missing are tracked at fieldsMetadataQueue. + * + * @param Function callback + * A callback function that receives field elements whose metadata will just + * have been fetched. + */ +function fetchMissingMetadata (callback) { + if (fieldsMetadataQueue.length) { + var editIDs = _.pluck(fieldsMetadataQueue, 'editID'); + var fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el'); + fieldsMetadataQueue = []; + + $(window).ready(function () { + var id = 'edit-load-metadata'; + // Create a temporary element to be able to use Drupal.ajax. + var $el = jQuery('
').appendTo('body'); + // Create a Drupal.ajax instance to load the form. + Drupal.ajax[id] = new Drupal.ajax(id, $el, { + url: drupalSettings.edit.metadataURL, + event: 'edit-internal.edit', + submit: { 'fields[]': editIDs }, + // No progress indicator. + progress: { type: null } + }); + // Implement a scoped editMetaData AJAX command: calls the callback. + Drupal.ajax[id].commands.editMetadata = function (ajax, response, status) { + // Store the metadata. + _.each(response.data, function (fieldMetadata, editID) { + Drupal.edit.Metadata.add(editID, fieldMetadata); + }); + // Clean-up. + delete Drupal.ajax[id]; + $el.remove(); + + callback(fieldElementsWithoutMetadata); + }; + // This will ensure our scoped editMetadata AJAX command gets called. + $el.trigger('edit-internal.edit'); + }); + } +} + +/** + * Attempts to set up a "Quick edit" link and corresponding EntityModel. * * @param Object contextualLink * An object with the following properties: @@ -133,41 +277,56 @@ $(document).on('drupalContextualLinkAdded', function (event, data) { * its fields). * Returns false otherwise. */ -Drupal.edit.setUpContextualLink = function (contextualLink) { +function initializeEntityContextualLink (contextualLink) { + var metadata = Drupal.edit.Metadata; // Check if the user has permission to edit at least one of them. function hasFieldWithPermission (editIDs) { - var i, meta = Drupal.edit.metadataCache; - for (i = 0; i < editIDs.length; i++) { + for (var i = 0; i < editIDs.length; i++) { var editID = editIDs[i]; - if (_.has(meta, editID) && meta[editID].access === true) { + if (metadata.get(editID, 'access') === true) { return true; } } return false; } - // Checks if the metadata for all given editIDs exists. + // Checks if the metadata for all given edit IDs exists. function allMetadataExists (editIDs) { - var editIDsWithMetadata = _.intersection(editIDs, _.keys(Drupal.edit.metadataCache)); - return editIDs.length === editIDsWithMetadata.length; + return editIDs.length === metadata.intersection(editIDs).length; } - // Find the Edit IDs of all fields within this entity. - var editIDs = []; - contextualLink.$region - .find('[data-edit-id^="' + contextualLink.entity + '/"]') - .each(function () { - editIDs.push($(this).attr('data-edit-id')); - }); + // Find all fields for this entity and collect their edit IDs. + var fields = _.where(fieldsAvailableQueue, { entityID: contextualLink.entityID }); + var editIDs = _.pluck(fields, 'editID'); + // No fields found yet. + if (editIDs.length === 0) { + return false; + } // The entity for the given contextual link contains at least one field that - // the current user may edit in-place; instantiate ContextualLinkView. - if (hasFieldWithPermission(editIDs)) { - new Drupal.edit.views.ContextualLinkView({ - el: $('
  • ').prependTo(contextualLink.$el), - model: Drupal.edit.app.model, - entity: contextualLink.entity + // the current user may edit in-place; instantiate EntityModel and + // ContextualLinkView. + else if (hasFieldWithPermission(editIDs)) { + var entityModel = new Drupal.edit.EntityModel({ + id: contextualLink.entityID, + el: contextualLink.region }); + Drupal.edit.collections.entities.add(entityModel); + + // Initialize all queued fields within this entity (creates FieldModels). + _.each(fields, function (field) { + initializeField(field.el, field.editID); + }); + fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields); + + // Set up contextual link view. + var contextualLinkView = new Drupal.edit.ContextualLinkView($.extend({ + el: $('
  • ').prependTo(contextualLink.el), + model: entityModel, + appModel: Drupal.edit.app.model + }, options)); + entityModel.set('contextualLinkView', contextualLinkView); + return true; } // There was not at least one field that the current user may edit in-place, @@ -177,19 +336,58 @@ Drupal.edit.setUpContextualLink = function (contextualLink) { } return false; -}; +} + +/** + * Delete models and queue items that are contained within a given context. + * + * Deletes any contained EntityModels (plus their associated FieldModels and + * ContextualLinkView) and FieldModels, as well as the corresponding queues. + * + * After EntityModels, FieldModels must also be deleted, because it is possible + * in Drupal for a field DOM element to exist outside of the entity DOM element, + * e.g. when viewing the full node, the title of the node is not rendered within + * the node (the entity) but as the page title. + * + * Note: this will not delete an entity that is actively being in-place edited. + * + * @param jQuery $context + * The context within which to delete. + */ +function deleteContainedModelsAndQueues($context) { + $context.find('[data-edit-entity]').addBack('[data-edit-entity]').each(function (index, entityElement) { + // Delete entity model. + // @todo change to findWhere() as soon as we have Backbone 1.0 + var entityModels = Drupal.edit.collections.entities.where({el: entityElement}); + if (entityModels.length) { + // @todo Make this cleaner; let EntityModel.destroy() do this? + var contextualLinkView = entityModels[0].get('contextualLinkView'); + contextualLinkView.undelegateEvents(); + contextualLinkView.remove(); + + entityModels[0].destroy(); + } + + // Filter queue. + function hasOtherRegion (contextualLink) { + return contextualLink.region !== entityElement; + } + contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion); + }); + + $context.find('[data-edit-id]').addBack('[data-edit-id]').each(function (index, fieldElement) { + // Delete field models. + Drupal.edit.collections.fields.chain() + .filter(function (fieldModel) { return fieldModel.get('el') === fieldElement; }) + .invoke('destroy'); -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(); - // 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 = new Drupal.edit.EditAppView({ - el: $('body'), - model: appModel + // Filter queues. + function hasOtherFieldElement (field) { + return field.el !== fieldElement; + } + fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement); + fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement); }); -}; +} })(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..621860a --- /dev/null +++ b/core/modules/edit/js/editors/directEditor.js @@ -0,0 +1,107 @@ +/** + * @file + * contentEditable-based in-place editor for plain text content. + */ +(function ($, Drupal) { + +"use strict"; + +Drupal.edit.editors.direct = Drupal.edit.EditorView.extend({ + + $textElement: null, + + /** + * {@inheritdoc} + */ + initialize: function (options) { + Drupal.edit.EditorView.prototype.initialize.call(this, options); + + var model = this.model; + + // Store the actual value of this field. We'll need this to restore the + // original value when the user discards his modifications. + var $textElement = this.$textElement = this.$el.find('.field-item:first'); + model.set('originalValue', jQuery.trim(this.$textElement.text())); + + // Sets the state to 'changed' whenever the content has changed. + var previousText = model.get('originalValue'); + $textElement.on('keyup paste', function (event) { + var currentText = jQuery.trim($textElement.text()); + if (previousText !== currentText) { + previousText = currentText; + model.set('currentValue', currentText); + model.set('state', 'changed'); + } + }); + }, + + /** + * {@inheritdoc} + */ + getEditedElement: function () { + return this.$textElement; + }, + + /** + * {@inheritdoc} + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + case 'candidate': + if (from !== 'inactive') { + this.$textElement.removeAttr('contentEditable'); + } + if (from === 'invalid') { + this.removeValidationErrors(); + } + break; + case 'highlighted': + break; + case 'activating': + // As soon as the current state change has propagated, apply this one. + // @see http://jsfiddle.net/5MVzp/2/ vs. http://jsfiddle.net/5MVzp/3/ + var that = this; + _.defer(function() { + that.model.set('state', 'active'); + }); + break; + case 'active': + this.$textElement.attr('contentEditable', 'true'); + break; + case 'changed': + break; + case 'saving': + if (from === 'invalid') { + this.removeValidationErrors(); + } + this.save(); + break; + case 'saved': + break; + case 'invalid': + this.showValidationErrors(); + break; + } + }, + + /** + * {@inheritdoc} + */ + getEditUISettings: function () { + return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** + * {@inheritdoc} + */ + revert: function () { + this.$textElement.html(this.model.get('originalValue')); + } + +}); + +})(jQuery, Drupal); diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js new file mode 100644 index 0000000..2225ced --- /dev/null +++ b/core/modules/edit/js/editors/formEditor.js @@ -0,0 +1,187 @@ +/** + * @file + * Form-based in-place editor. Works for any field type. + */ +(function ($, Drupal) { + +"use strict"; + +Drupal.edit.editors.form = Drupal.edit.EditorView.extend({ + + $formContainer: null, + + /** + * {@inheritdoc} + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + case 'candidate': + if (from !== 'inactive') { + this.disable(); + } + if (from === 'invalid') { + // No need to call removeValidationErrors() for this in-place editor! + } + 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': + this.showValidationErrors(); + break; + } + }, + + /** + * Enables the widget. + */ + enable: function () { + // Generate a DOM-compatible ID for the form container DOM element. + var id = 'edit-form-for-' + this.model.get('editID').replace(/\//g, '_'); + + // Render form container. + this.$formContainer = $(Drupal.theme('editFormContainer', { + id: 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 (this.$el.css('display') === 'inline') { + this.$formContainer.prependTo(this.$el.offsetParent()); + // Position the form container to render on top of the field's element. + var pos = this.$el.position(); + this.$formContainer.css('left', pos.left).css('top', pos.top); + } + else { + this.$formContainer.insertBefore(this.$el); + } + + // Load form, insert it into the form container and attach event handlers. + var that = this; + var formOptions = { + propertyID: this.model.get('editID'), + $editorElement: this.$el, + nocssjs: false + }; + Drupal.edit.util.form.load(formOptions, function(form, ajax) { + Drupal.ajax.prototype.commands.insert(ajax, { + data: form, + selector: '#' + id + ' .placeholder' + }); + + var $submit = that.$formContainer.find('.edit-form-submit'); + Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); + that.$formContainer + .on('formUpdated.edit', ':input', function () { + that.model.set('state', 'changed'); + }) + .on('keypress.edit', 'input', function (event) { + if (event.keyCode === 13) { + return false; + } + }); + + // The in-place editor has loaded; change state to 'active'. + that.model.set('state', 'active'); + }); + }, + + /** + * Disables the widget. + */ + disable: function () { + if (this.$formContainer === null) { + return; + } + + Drupal.edit.util.form.unajaxifySaving(this.$formContainer.find('.edit-form-submit')); + // Allow form widgets to detach properly. + Drupal.detachBehaviors(this.$formContainer, null, 'unload'); + this.$formContainer + .off('change.edit', ':input') + .off('keypress.edit', 'input') + .remove(); + this.$formContainer = null; + }, + + /** + * {@inheritdoc} + */ + save: function () { + var $formContainer = this.model.attributes.editorView.$formContainer; + var $submit = $formContainer.find('.edit-form-submit'); + var base = $submit.attr('id'); + var that = this; + + // Successfully saved. + Drupal.ajax[base].commands.editFieldFormSaved = function(ajax, response, status) { + Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); + + // First, transition the state to 'saved'. + that.model.set('state', 'saved'); + // Then, set the 'html' attribute on the field model. This will cause the + // field to be rerendered. + that.model.set('html', response.data); + }; + + // Unsuccessfully saved; validation errors. + Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) { + that.model.set('validationErrors', response.data); + that.model.set('state', 'invalid'); + }; + + // The edit_field_form AJAX command is only called upon loading the form for + // the first time, and when there are validation errors in the form; Form + // API then marks which form items have errors. Therefor, we have to replace + // the existing form, unbind the existing Drupal.ajax instance and create a + // new Drupal.ajax instance. + Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) { + Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); + + Drupal.ajax.prototype.commands.insert(ajax, { + data: response.data, + selector: '#' + $formContainer.attr('id') + ' form' + }); + + // Create a Drupal.ajax instance for the re-rendered ("new") form. + var $newSubmit = $formContainer.find('.edit-form-submit'); + Drupal.edit.util.form.ajaxifySaving({ nocssjs: false }, $newSubmit); + }; + + // Click the form's submit button; the scoped AJAX commands above will + // handle the server's response. + $submit.trigger('click.edit'); + }, + + /** + * {@inheritdoc} + */ + showValidationErrors: function () { + this.$formContainer + .find('.edit-form') + .addClass('edit-validation-error') + .find('form') + .prepend(this.model.get('validationErrors')); + } +}); + +})(jQuery, Drupal); diff --git a/core/modules/edit/js/models/AppModel.js b/core/modules/edit/js/models/AppModel.js new file mode 100644 index 0000000..75da33b --- /dev/null +++ b/core/modules/edit/js/models/AppModel.js @@ -0,0 +1,22 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + AppModel: Backbone.Model.extend({ + defaults: { + highlightedEditor: null, + activeEditor: null, + // Reference to a ModalView-instance if a state change requires confirmation. + activeModal: null + } + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js new file mode 100644 index 0000000..9edfea3 --- /dev/null +++ b/core/modules/edit/js/models/EntityModel.js @@ -0,0 +1,49 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + EntityModel: Backbone.Model.extend({ + defaults: { + // The DOM element that represents this entity. It may seem bizarre to + // have a DOM element in a Backbone Model, but we need to be able to map + // entities in the DOM to EntityModels in memory. + el: null, + + // An entity ID, of the form "/", e.g. "node/1". + id: null, + // Indicates whether this instance of this entity is currently being + // edited. + isActive: false, + // A Drupal.edit.FieldCollection for all fields of this entity. + fields: null + }, + initialize: function () { + this.set('fields', new Drupal.edit.FieldCollection()); + }, + destroy: function(options) { + if (this.get('isActive')) { + throw new Error("EntityModel cannot be destroyed while it is being edited."); + } + Backbone.Model.prototype.destroy.apply(this, options); + + // Destroy all fields of this entity. + this.get('fields').each(function (fieldModel) { + fieldModel.destroy(); + }); + } + }), + + // A collection of EntityModels. + EntityCollection: Backbone.Collection.extend({ + model: Drupal.edit.EntityModel + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js new file mode 100644 index 0000000..a1a6e84 --- /dev/null +++ b/core/modules/edit/js/models/FieldModel.js @@ -0,0 +1,132 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + FieldModel: Backbone.Model.extend({ + defaults: { + // The DOM element that represents this field. It may seem bizarre to have + // a DOM element in a Backbone Model, but we need to be able to map fields + // in the DOM to FieldModels in memory. + el: null, + + // Possible states: @see Drupal.edit.FieldModel.states. + state: 'inactive', + // A Drupal.edit.EntityModel. Its "fields" attribute, which is a + // FieldCollection, is automatically updated to include this FieldModel. + entity: null, + + + // + // 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: `::::`. + // @todo rename to "id"? + 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, + + // Not the full HTML representation of this field, but the "actual" + // original value of the field, stored by the used in-place editor, and + // in a representation that can be chosen by the in-place editor. + originalValue: null, + // Analogous to originalValue, but the current value. + currentValue: null, + + validationErrors: null, + + // + // Callbacks. + // + + // Callback function for validating changes between states. Receives the + // previous state, new state, context, and a callback + acceptStateChange: null, + + + // + // Attached views. + // + decorated: false, + editorView: null, + toolbarView: null, + decorationView: null + + }, + initialize: function () { + this.get('entity').get('fields').add(this); + this.on('change:state', function(model, state) { + console.log('%c [MOD] ' + this.previous('state') + ' → ' + state + ' ' + model.get('editID'), 'background-color: blue; color: white'); + }); + this.on('error', function(model, error) { + console.log('%c' + model.get("editID") + " " + error, 'color: orange'); + }); + }, + destroy: function(options) { + if (this.get('state') !== 'inactive') { + throw new Error("FieldModel cannot be destroyed if it is not inactive state."); + } + Backbone.Model.prototype.destroy.apply(this, options); + }, + validate: function (attrs, options) { + // We only care about validating the 'state' attribute. + if (!_.has(attrs, 'state')) { + return; + } + + var current = this.get('state'); + var next = attrs.state; + if (current !== next) { + // Ensure it's a valid state. + if (_.indexOf(this.constructor.states, next) === -1) { + return '"' + next + '" is an invalid state'; + } + // Check if the acceptStateChange callback accepts it. + if (!this.get('acceptStateChange')(current, next, options)) { + return 'state change not accepted'; + } + } + }, + // Parses the `/` part from the edit ID. + getEntityID: function() { + return this.get('editID').split('/').slice(0, 2).join('/'); + }, + // Parses the `//` part from the edit ID. + getFieldID: function() { + return this.get('editID').split('/').slice(2, 5).join('/'); + } + }, { + states: [ + 'inactive', 'candidate', 'highlighted', + 'activating', 'active', 'changed', 'saving', 'saved', 'invalid' + ], + followsStateSequence: function (from, to) { + return _.indexOf(this.states, from) < _.indexOf(this.states, to); + } + }), + + // A collection of FieldModels. + // @todo link back to the entity at the collection level (not the collection element level)? + FieldCollection: Backbone.Collection.extend({ + model: Drupal.edit.FieldModel + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/models/edit-app-model.js b/core/modules/edit/js/models/edit-app-model.js deleted file mode 100644 index 0c90fd0..0000000 --- a/core/modules/edit/js/models/edit-app-model.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @file - * A Backbone Model that models the current Edit application state. - */ -(function(Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.models = Drupal.edit.models || {}; -Drupal.edit.models.EditAppModel = Backbone.Model.extend({ - defaults: { - activeEntity: null, - highlightedEditor: null, - activeEditor: null, - // Reference to a ModalView-instance if a transition requires confirmation. - activeModal: null - } -}); - -})(Backbone, Drupal); diff --git a/core/modules/edit/js/util.js b/core/modules/edit/js/util.js index e0aa490..d287a1d 100644 --- a/core/modules/edit/js/util.js +++ b/core/modules/edit/js/util.js @@ -12,10 +12,6 @@ Drupal.edit.util = Drupal.edit.util || {}; Drupal.edit.util.constants = {}; Drupal.edit.util.constants.transitionEnd = "transitionEnd.edit webkitTransitionEnd.edit transitionend.edit msTransitionEnd.edit oTransitionEnd.edit"; -Drupal.edit.util.calcPropertyID = function(entity, predicate) { - return entity.getSubjectUri() + '/' + predicate; -}; - /** * Retrieves a setting of the editor-specific Edit UI integration. * @@ -24,7 +20,7 @@ Drupal.edit.util.calcPropertyID = function(entity, predicate) { * be used. * * @param editor - * A Create.js PropertyEditor widget instance. + * A Drupal.edit.Editor instance. * @param setting * Name of the Edit UI integration setting. * diff --git a/core/modules/edit/js/viejs/EditService.js b/core/modules/edit/js/viejs/EditService.js deleted file mode 100644 index 00cb04b..0000000 --- a/core/modules/edit/js/viejs/EditService.js +++ /dev/null @@ -1,289 +0,0 @@ -/** - * @file - * VIE DOM parsing service for Edit. - */ -(function(jQuery, _, VIE, Drupal, drupalSettings) { - -"use strict"; - - VIE.prototype.EditService = function (options) { - var defaults = { - name: 'edit', - subjectSelector: '.edit-field.edit-allowed' - }; - this.options = _.extend({}, defaults, options); - - this.views = []; - this.vie = null; - this.name = this.options.name; - }; - - VIE.prototype.EditService.prototype = { - load: function (loadable) { - var correct = loadable instanceof this.vie.Loadable; - if (!correct) { - throw new Error('Invalid Loadable passed'); - } - - var element; - if (!loadable.options.element) { - if (typeof document === 'undefined') { - return loadable.resolve([]); - } else { - element = drupalSettings.edit.context; - } - } else { - element = loadable.options.element; - } - - var entities = this.readEntities(element); - loadable.resolve(entities); - }, - - _getViewForElement:function (element, collectionView) { - var viewInstance; - - jQuery.each(this.views, function () { - if (jQuery(this.el).get(0) === element.get(0)) { - if (collectionView && !this.template) { - return true; - } - viewInstance = this; - return false; - } - }); - return viewInstance; - }, - - _registerEntityView:function (entity, element, isNew) { - if (!element.length) { - return; - } - - // Let's only have this overhead for direct types. Form-based editors are - // handled in backbone.drupalform.js and the PropertyEditor instance. - if (jQuery(element).hasClass('edit-type-form')) { - return; - } - - var service = this; - var viewInstance = this._getViewForElement(element); - if (viewInstance) { - return viewInstance; - } - - viewInstance = new this.vie.view.Entity({ - model:entity, - el:element, - tagName:element.get(0).nodeName, - vie:this.vie, - service:this.name - }); - - this.views.push(viewInstance); - - return viewInstance; - }, - - save: function(saveable) { - var correct = saveable instanceof this.vie.Savable; - if (!correct) { - throw "Invalid Savable passed"; - } - - if (!saveable.options.element) { - // FIXME: we could find element based on subject - throw "Unable to write entity to edit.module-markup, no element given"; - } - - if (!saveable.options.entity) { - throw "Unable to write to edit.module-markup, no entity given"; - } - - var $element = jQuery(saveable.options.element); - this._writeEntity(saveable.options.entity, saveable.options.element); - saveable.resolve(); - }, - - _writeEntity:function (entity, element) { - var service = this; - this.findPredicateElements(this.getElementSubject(element), element, true).each(function () { - var predicateElement = jQuery(this); - var predicate = service.getElementPredicate(predicateElement); - if (!entity.has(predicate)) { - return true; - } - - var value = entity.get(predicate); - if (value && value.isCollection) { - // Handled by CollectionViews separately - return true; - } - if (value === service.readElementValue(predicate, predicateElement)) { - return true; - } - // Unlike in the VIE's RdfaService no (re-)mapping needed here. - predicateElement.html(value); - }); - return true; - }, - - // The edit-id data attribute contains the full identifier of - // each entity element in the format - // `::::`. - _getID: function (element) { - var id = jQuery(element).attr('data-edit-id'); - if (!id) { - id = jQuery(element).closest('[data-edit-id]').attr('data-edit-id'); - } - return id; - }, - - // Returns the "URI" of an entity of an element in format - // `/`. - getElementSubject: function (element) { - return this._getID(element).split('/').slice(0, 2).join('/'); - }, - - // Returns the field name for an element in format - // `//`. - // (Slashes instead of colons because the field name is no namespace.) - getElementPredicate: function (element) { - if (!this._getID(element)) { - throw new Error('Could not find predicate for element'); - } - return this._getID(element).split('/').slice(2, 5).join('/'); - }, - - getElementType: function (element) { - return this._getID(element).split('/').slice(0, 1)[0]; - }, - - // Reads all editable entities (currently each Drupal field is considered an - // entity, in the future Drupal entities should be mapped to VIE entities) - // from DOM and returns the VIE enties it found. - readEntities: function (element) { - var service = this; - var entities = []; - var entityElements = jQuery(this.options.subjectSelector, element); - entityElements = entityElements.add(jQuery(element).filter(this.options.subjectSelector)); - entityElements.each(function () { - var entity = service._readEntity(jQuery(this)); - if (entity) { - entities.push(entity); - } - }); - return entities; - }, - - // Returns a filled VIE Entity instance for a DOM element. The Entity - // is also registered in the VIE entities collection. - _readEntity: function (element) { - var subject = this.getElementSubject(element); - var type = this.getElementType(element); - var entity = this._readEntityPredicates(subject, element, false); - if (jQuery.isEmptyObject(entity)) { - return null; - } - entity['@subject'] = subject; - if (type) { - entity['@type'] = this._registerType(type, element); - } - - var entityInstance = new this.vie.Entity(entity); - entityInstance = this.vie.entities.addOrUpdate(entityInstance, { - updateOptions: { - silent: true, - ignoreChanges: true - } - }); - - this._registerEntityView(entityInstance, element); - return entityInstance; - }, - - _registerType: function (typeId, element) { - typeId = ''; - var type = this.vie.types.get(typeId); - if (!type) { - this.vie.types.add(typeId, []); - type = this.vie.types.get(typeId); - } - - var predicate = this.getElementPredicate(element); - if (type.attributes.get(predicate)) { - return type; - } - var range = predicate.split('/')[0]; - type.attributes.add(predicate, [range], 0, 1, { - label: element.data('edit-field-label') - }); - - return type; - }, - - _readEntityPredicates: function (subject, element, emptyValues) { - var entityPredicates = {}; - var service = this; - this.findPredicateElements(subject, element, true).each(function () { - var predicateElement = jQuery(this); - var predicate = service.getElementPredicate(predicateElement); - if (!predicate) { - return; - } - var value = service.readElementValue(predicate, predicateElement); - if (value === null && !emptyValues) { - return; - } - - entityPredicates[predicate] = value; - entityPredicates[predicate + '/rendered'] = predicateElement[0].outerHTML; - }); - return entityPredicates; - }, - - readElementValue : function(predicate, element) { - // Unlike in RdfaService there is parsing needed here. - if (element.hasClass('edit-type-form')) { - return undefined; - } - else { - return jQuery.trim(element.html()); - } - }, - - // Subject elements are the DOM elements containing a single or multiple - // editable fields. - findSubjectElements: function (element) { - if (!element) { - element = drupalSettings.edit.context; - } - return jQuery(this.options.subjectSelector, element); - }, - - // Predicate Elements are the actual DOM elements that users will be able - // to edit. - findPredicateElements: function (subject, element, allowNestedPredicates, stop) { - var predicates = jQuery(); - // Make sure that element is wrapped by jQuery. - var $element = jQuery(element); - - // Form-type predicates - predicates = predicates.add($element.filter('.edit-type-form')); - - // Direct-type predicates - var direct = $element.filter('.edit-type-direct'); - predicates = predicates.add(direct.find('.field-item')); - - if (!predicates.length && !stop) { - var parentElement = $element.parent(this.options.subjectSelector); - if (parentElement.length) { - return this.findPredicateElements(subject, parentElement, allowNestedPredicates, true); - } - } - - return predicates; - } - }; - -})(jQuery, _, VIE, Drupal, drupalSettings); diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js new file mode 100644 index 0000000..a124c77 --- /dev/null +++ b/core/modules/edit/js/views/AppView.js @@ -0,0 +1,384 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + AppView: Backbone.View.extend({ + + // Configuration for state handling. + activeEditorStates: [], + singleEditorStates: [], + + /** + * {@inheritdoc} + */ + initialize: function (options) { + _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange'); + + // Instantiate configuration for state handling. + // @see Drupal.edit.FieldModel.states + this.activeEditorStates = ['activating', 'active']; + this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates); + + options.entitiesCollection + // Track app state. + .on('change:isActive', this.appStateChange, this) + .on('change:isActive', this.enforceSingleActiveEntity, this); + + options.fieldsCollection + // Track app state. + .on('change:state', this.editorStateChange, this) + // Respond to field model HTML representation change events. + .on('change:html', this.propagateUpdatedField, this) + .on('change:html', this.renderUpdatedField, this) + // Respond to addition. + .on('add', this.rerenderedFieldToCandidate, this) + // Respond to destruction. + .on('destroy', this.undecorate, this); + }, + + /** + * Handles setup/teardown and state changes when the active entity changes. + */ + appStateChange: function (entityModel, isActive) { + var app = this; + if (isActive) { + // Move all fields of this entity from the 'inactive' state to the + // 'candidate' state. + entityModel.get('fields').each(function (fieldModel) { + // First, set up decoration views. + app.decorate(fieldModel); + // Second, change the field's state. + fieldModel.set('state', 'candidate'); + }); + } + else { + // Move all fields of this entity from whatever state they are in to + // the 'inactive' state. + entityModel.get('fields').each(function (fieldModel) { + // First, change the field's state. + fieldModel.set('state', 'inactive', { reason: 'stop' }); + // Second, tear down decoration views. + app.undecorate(fieldModel); + }); + } + }, + + /** + * Accepts or reject editor (Editor) 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' || context.reason === 'rerender')) { + if (from === 'candidate' && to === 'inactive') { + accept = true; + } + } + // Handling of edit mode state changes is more granular. + else { + // In general, enforce the states sequence. Disallow going back from a + // "later" state to an "earlier" state, except in explicitly allowed + // cases. + if (!Drupal.edit.FieldModel.followsStateSequence(from, to)) { + accept = false; + // Allow: activating/active -> candidate. + // Necessary to stop editing a property. + if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { + accept = true; + } + // Allow: changed/invalid -> candidate. + // Necessary to stop editing a property when it is changed or invalid. + else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { + accept = true; + } + // Allow: highlighted -> candidate. + // Necessary to stop highlighting a property. + else if (from === 'highlighted' && to === 'candidate') { + accept = true; + } + // Allow: saved -> candidate. + // Necessary when successfully saved a property. + else if (from === 'saved' && to === 'candidate') { + accept = true; + } + // Allow: invalid -> saving. + // Necessary to be able to save a corrected, invalid property. + else if (from === 'invalid' && to === 'saving') { + accept = true; + } + } + + // If it's not against the general principle, then here are more + // disallowed cases to check. + if (accept) { + // Ensure only one editor (field) at a time may be higlighted or active. + if (from === 'candidate' && _.indexOf(this.singleEditorStates, to) !== -1) { + if (this.model.get('highlightedEditor') || this.model.get('activeEditor')) { + accept = false; + } + } + // Reject going from activating/active to candidate because of a + // mouseleave. + else if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { + if (context && context.reason === 'mouseleave') { + accept = false; + } + } + // When attempting to stop editing a changed/invalid property, ask for + // confirmation. + else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { + if (context && context.reason === 'mouseleave') { + accept = false; + } + else { + // Check whether the transition has been confirmed? + if (context && context.confirmed) { + accept = true; + } + // Confirm this transition. + else { + // Do not accept this change right now, instead open a modal + // that will ask the user to confirm his choice. + accept = false; + // The callback will be called from the helper function. + this._confirmStopEditing(callback); + } + } + } + } + } + + return accept; + }, + + // @todo rename to decorateField + decorate: function (fieldModel) { + // Create a new Editor. + var editorName = fieldModel.get('editor'); + fieldModel.set('editorName', editorName); + var editorView = new Drupal.edit.editors[editorName]({ + el: $(fieldModel.get('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.FieldToolbarView({ + model: fieldModel, + $field: $(editorView.getEditedElement()), + editorView: editorView + }); + + // Decorate the editor's DOM element depending on its state. + var decorationView = new Drupal.edit.EditorDecorationView({ + el: $(editorView.getEditedElement()), + model: fieldModel, + editorView: editorView, + toolbarId: toolbarView.getId() + }); + + // Create references in the field model; necessary for undecorate() and + // necessary for some EditorView implementations. + fieldModel.set('editorView', editorView); + fieldModel.set('toolbarView', toolbarView); + fieldModel.set('decorationView', decorationView); + fieldModel.set('decorated', true); + }, + + // @todo rename to undecorateField + undecorate: function (fieldModel) { + // Early-return if this field was not yet decorated. + if (!fieldModel.get('decorated')) { + return; + } + + // Unbind event handlers; remove toolbar element; delete toolbar view. + fieldModel.get('toolbarView').remove(); + fieldModel.unset('toolbarView'); + + // Unbind event handlers; delete decoration view. Don't remove the element + // because that would remove the field itself. + fieldModel.get('decorationView').remove(); + fieldModel.unset('decorationView'); + + // Unbind event handlers; delete editor view. Don't remove the element + // because that would remove the field itself. + fieldModel.get('editorView').remove(); + fieldModel.unset('editorView'); + }, + + /** + * Asks the user to confirm whether he wants to stop editing via a modal. + * + * @see acceptEditorStateChange() + */ + _confirmStopEditing: function () { + // Only instantiate if there isn't a modal instance visible yet. + if (!this.model.get('activeModal')) { + var that = this; + var modal = new Drupal.edit.ModalView({ + model: this.model, + message: Drupal.t('You have unsaved changes'), + buttons: [ + { action: 'discard', classes: 'gray-button', label: Drupal.t('Discard changes') }, + { action: 'save', type: 'submit', classes: 'blue-button', label: Drupal.t('Save') } + ], + callback: function(action) { + // The active modal has been removed. + that.model.set('activeModal', null); + // Set the state that matches the user's action. + var targetState = (action === 'discard') ? 'candidate' : 'saving'; + that.model.get('activeEditor').set('state', 'candidate', { confirmed: true }); + } + }); + this.model.set('activeModal', modal); + // The modal will set the activeModal property on the model when rendering + // to prevent multiple modals from being instantiated. + modal.render(); + } + }, + + /** + * Reacts to field state changes; tracks global state. + * + * @param Drupal.edit.FieldModel fieldModel + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + editorStateChange: function(fieldModel, state) { + var from = fieldModel.previous('state'); + var to = state; + + console.log("%c [APP] %s → %s %s", "background-color: black; color: white", from, to, fieldModel.get('editID')); + + // Keep track of the highlighted editor in the global state. + if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== fieldModel) { + this.model.set('highlightedEditor', fieldModel); + } + else if (this.model.get('highlightedEditor') === fieldModel && to === 'candidate') { + this.model.set('highlightedEditor', null); + } + + // Keep track of the active editor in the global state. + if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== fieldModel) { + this.model.set('activeEditor', fieldModel); + } + else if (this.model.get('activeEditor') === fieldModel && to === 'candidate') { + // Discarded if it transitions from a changed state to 'candidate'. + if (from === 'changed' || from === 'invalid') { + fieldModel.get('editorView').revert(); + } + this.model.set('activeEditor', null); + } + }, + + // 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. + propagateUpdatedField: function (changedModel) { + // changedModel.collection.where({ editID: changedModel.get('editID') }) + // .set('html', changedModel.get('html')); + }, + + renderUpdatedField: function (field) { + // Get data necessary to rerender property before it is unavailable. + var html = field.get('html'); + var $fieldWrapper = $(field.get('el')).closest('[data-edit-id]'); + var $context = $fieldWrapper.parent(); + + // First set the state to 'candidate', to allow all attached views to + // clean up all their "active state"-related changes. + // @todo: get rid of this, ensure all views do the proper clean-up when + // going directly to the 'inactive' state. + field.set('state', 'candidate'); + + // Set the field's state to 'inactive', to enable the updating of its DOM + // value. + field.set('state', 'inactive', { reason: 'rerender' }); + + // Destroy the field model; this will cause all attached views to be + // destroyed too, and removal from all collections in which it exists. + field.destroy(); + + // Replace the old content with the new content. + $fieldWrapper.replaceWith(html); + + // Attach behaviors again to the modified piece of HTML; this will create + // a new field model and call rerenderedFieldToCandidate() with it. + Drupal.attachBehaviors($context); + }, + + /** + * If the new in-place editable field is for the entity that's currently + * being edited, then transition it to the 'candidate' state. + * + * This happens when a field was modified, saved and hence rerendered. + * + * @param Drupal.edit.FieldModel fieldModel + * A field that was just added to the collection of fields. + */ + rerenderedFieldToCandidate: function (fieldModel) { + var activeEntity = Drupal.edit.collections.entities.where({ isActive: true })[0]; + + // Early-return if there is no active entity. + if (activeEntity === null) { + return; + } + + // If the field's entity is the active entity, make it a candidate. + if (fieldModel.get('entity') === activeEntity) { + this.decorate(fieldModel); + fieldModel.set('state', 'candidate'); + } + }, + + /** + * EntityModel Collection change handler, called on change:isActive, enforces + * a single active entity. + */ + enforceSingleActiveEntity: function (changedEntityModel) { + // When an entity is deactivated, we don't need to enforce anything. + if (changedEntityModel.get('isActive') === false) { + return; + } + + // This entity was activated; deactivate all other entities. + changedEntityModel.collection.chain() + .filter(function (entityModel) { + return entityModel.get('isActive') === true && entityModel !== changedEntityModel; + }) + .each(function (entityModel) { + entityModel.set('isActive', false); + }); + } + + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/views/ContextualLinkView.js b/core/modules/edit/js/views/ContextualLinkView.js new file mode 100644 index 0000000..1dac235 --- /dev/null +++ b/core/modules/edit/js/views/ContextualLinkView.js @@ -0,0 +1,74 @@ +/** + * @file + * A Backbone View that a dynamic contextual link. + */ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +// Main model: Drupal.edit.EntityModel +Drupal.edit.ContextualLinkView = Backbone.View.extend({ + + events: function () { + // Prevents delay and simulated mouse events. + function touchEndToClick (event) { + event.preventDefault(); + event.target.click(); + } + return { + 'click a': function (event) { + event.preventDefault(); + this.model.set('isActive', !this.model.get('isActive')); + }, + 'touchEnd a': touchEndToClick + }; + }, + + /** + * {@inheritdoc} + * + * @param Object options + * An object with the following keys: + * - appModel: the application state model + * - strings: the strings for the "Quick edit" link + */ + initialize: function (options) { + // @todo Is this a good idea at all? I'm very unsure… + if (!this.model instanceof Drupal.edit.EntityModel) { + throw new Error('Drupal.edit.ContextualLinkView only accepts Drupal.edit.EntityModel models.'); + } + + // Initial render. + this.render(); + + // Re-render whenever this entity's isActive attribute changes. + this.model.on('change:isActive', this.render, this); + + // Hide the contextual links whenever an in-place editor is active. + this.options.appModel.on('change:activeEditor', this.toggleContextualLinksVisibility, this); + }, + + /** + * {@inheritdoc} + */ + render: function () { + var strings = this.options.strings; + var text = !this.model.get('isActive') ? strings.quickEdit : strings.stopQuickEdit; + this.$el.find('a').text(text); + return this; + }, + + /** + * Hides the contextual links if an in-place editor is active. + * + * @param Drupal.edit.AppModel model + * @param null|Drupal.edit.FieldModel activeEditor + * The model of the field that is currently being edited, or, if none, null. + */ + toggleContextualLinksVisibility: function (model, activeEditor) { + this.$el.parents('.contextual').toggle(activeEditor === null); + } + +}); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/EditorDecorationView.js b/core/modules/edit/js/views/EditorDecorationView.js new file mode 100644 index 0000000..5854a50 --- /dev/null +++ b/core/modules/edit/js/views/EditorDecorationView.js @@ -0,0 +1,372 @@ +/** + * @file + * A Backbone View that decorates a Property Editor widget. + * + * It listens to state changes of the property editor. + */ +(function ($, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.EditorDecorationView = Backbone.View.extend({ + toolbarId: null, + + _widthAttributeIsEmpty: null, + + events: { + 'mouseenter.edit' : 'onMouseEnter', + 'mouseleave.edit' : 'onMouseLeave', + 'click': 'onClick', + 'tabIn.edit': 'onMouseEnter', + 'tabOut.edit': 'onMouseLeave' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + * + * @param Object options + * An object with the following keys: + * - editorView: the editor object with an 'options' object that has these keys: + * * property: the predicate of the property. + * * editorName: the name of the Editor. + * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. + */ + initialize: function (options) { + this.editorView = options.editorView; + + this.toolbarId = options.toolbarId; + + this.model.on('change:state', this.stateChange, this); + }, + + /** + * {@inheritdoc} + */ + remove: function () { + // The el property is the field, which should not be removed. Remove the + // pointer to it, then call Backbone.View.prototype.remove(). + this.setElement(); + Backbone.View.prototype.remove.call(this); + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + this.undecorate(); + break; + case 'candidate': + this.decorate(); + if (from !== 'inactive') { + this.stopHighlight(); + if (from !== 'highlighted') { + this.stopEdit(); + } + } + break; + case 'highlighted': + this.startHighlight(); + break; + case 'activating': + // NOTE: this state is not used by every editor! It's only used by those + // that need to interact with the server. + this.prepareEdit(); + break; + case 'active': + if (from !== 'activating') { + this.prepareEdit(); + } + this.startEdit(); + break; + case 'changed': + break; + case 'saving': + break; + case 'saved': + break; + case 'invalid': + break; + } + }, + + /** + * Starts hover; transitions to 'highlight' state. + * + * @param jQuery event + */ + onMouseEnter: function (event) { + var that = this; + this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { + that.model.set('state', 'highlighted'); + event.stopPropagation(); + }); + }, + + /** + * Stops hover; transitions to 'candidate' state. + * + * @param jQuery event + */ + onMouseLeave: function (event) { + var that = this; + this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { + that.model.set('state', 'candidate', { reason: 'mouseleave' }); + event.stopPropagation(); + }); + }, + + /** + * Transition to 'activating' stage. + * + * @param jQuery event + */ + onClick: function (event) { + this.model.set('state', 'activating'); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Adds classes used to indicate an elements editable state. + */ + decorate: function () { + this.$el.addClass('edit-animate-fast edit-candidate edit-editable'); + }, + + /** + * Removes classes used to indicate an elements editable state. + */ + undecorate: function () { + this.$el.removeClass('edit-candidate edit-editable edit-highlighted edit-editing'); + }, + + /** + * Adds that class that indicates that an element is highlighted. + */ + startHighlight: function () { + // Animations. + var that = this; + // Use a timeout to grab the next available animation frame. + setTimeout(function () { + that.$el.addClass('edit-highlighted'); + }, 0); + }, + + /** + * Removes the class that indicates that an element is highlighted. + */ + stopHighlight: function () { + this.$el.removeClass('edit-highlighted'); + }, + + /** + * Removes the class that indicates that an element as editable. + */ + prepareEdit: function () { + this.$el.addClass('edit-editing'); + + // While editing, do not show any other editors. + $('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); + }, + + /** + * Updates the display of the editable element once editing has begun. + */ + startEdit: function () { + if (this.getEditUISetting('padding')) { + this._pad(); + } + }, + + /** + * Removes the class that indicates that an element is being edited. + * + * Reapplies the class that indicates that a candidate editable element is + * again available to be edited. + */ + stopEdit: function () { + this.$el.removeClass('edit-highlighted edit-editing'); + + // Make the other editors show up again. + $('.edit-candidate').addClass('edit-editable'); + + if (this.getEditUISetting('padding')) { + this._unpad(); + } + }, + + /** + * Retrieves a setting of the editor-specific Edit UI integration. + * + * @param String setting + * + * @see Drupal.edit.util.getEditUISetting(). + */ + getEditUISetting: function (setting) { + return Drupal.edit.util.getEditUISetting(this.editorView, setting); + }, + + /** + * Adds padding around the editable element in order to make it pop visually. + */ + _pad: function () { + var self = this; + + // Add 5px padding for readability. This means we'll freeze the current + // width and *then* add 5px padding, hence ensuring the padding is added "on + // the outside". + // 1) Freeze the width (if it's not already set); don't use animations. + if (this.$el[0].style.width === "") { + this._widthAttributeIsEmpty = true; + this.$el + .addClass('edit-animate-disable-width') + .css('width', this.$el.width()) + .css('background-color', this._getBgColor(this.$el)); + } + + // 2) Add padding; use animations. + var posProp = this._getPositionProperties(this.$el); + setTimeout(function() { + // Re-enable width animations (padding changes affect width too!). + self.$el.removeClass('edit-animate-disable-width'); + + // Pad the editable. + self.$el + .css({ + 'position': 'relative', + 'top': posProp.top - 5 + 'px', + 'left': posProp.left - 5 + 'px', + 'padding-top' : posProp['padding-top'] + 5 + 'px', + 'padding-left' : posProp['padding-left'] + 5 + 'px', + 'padding-right' : posProp['padding-right'] + 5 + 'px', + 'padding-bottom': posProp['padding-bottom'] + 5 + 'px', + 'margin-bottom': posProp['margin-bottom'] - 10 + 'px' + }); + }, 0); + }, + + /** + * Removes the padding around the element being edited when editing ceases. + */ + _unpad: function () { + var self = this; + + // 1) Set the empty width again. + if (this._widthAttributeIsEmpty) { + this.$el + .addClass('edit-animate-disable-width') + .css('width', '') + .css('background-color', ''); + } + + // 2) Remove padding; use animations (these will run simultaneously with) + // the fading out of the toolbar as its gets removed). + var posProp = this._getPositionProperties(this.$el); + setTimeout(function() { + // Re-enable width animations (padding changes affect width too!). + self.$el.removeClass('edit-animate-disable-width'); + + // Unpad the editable. + self.$el + .css({ + 'position': 'relative', + 'top': posProp.top + 5 + 'px', + 'left': posProp.left + 5 + 'px', + 'padding-top' : posProp['padding-top'] - 5 + 'px', + 'padding-left' : posProp['padding-left'] - 5 + 'px', + 'padding-right' : posProp['padding-right'] - 5 + 'px', + 'padding-bottom': posProp['padding-bottom'] - 5 + 'px', + 'margin-bottom': posProp['margin-bottom'] + 10 + 'px' + }); + }, 0); + }, + + /** + * Gets the background color of an element (or the inherited one). + * + * @param DOM $e + */ + _getBgColor: function ($e) { + var c; + + if ($e === null || $e[0].nodeName === 'HTML') { + // Fallback to white. + return 'rgb(255, 255, 255)'; + } + c = $e.css('background-color'); + // TRICKY: edge case for Firefox' "transparent" here; this is a + // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724 + if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') { + return this._getBgColor($e.parent()); + } + return c; + }, + + /** + * Gets the top and left properties of an element. + * + * Convert extraneous values and information into numbers ready for + * subtraction. + * + * @param DOM $e + */ + _getPositionProperties: function ($e) { + var p, + r = {}, + props = [ + 'top', 'left', 'bottom', 'right', + 'padding-top', 'padding-left', 'padding-right', 'padding-bottom', + 'margin-bottom' + ]; + + var propCount = props.length; + for (var i = 0; i < propCount; i++) { + p = props[i]; + r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10); + } + return r; + }, + + /** + * Replaces blank or 'auto' CSS "position: " values with "0px". + * + * @param String pos + * (optional) The value for a CSS position declaration. + */ + _replaceBlankPosition: function (pos) { + if (pos === 'auto' || !pos) { + pos = '0px'; + } + return pos; + }, + + /** + * Ignores hovering to/from the given closest element. + * + * When a hover occurs to/from another element, invoke the callback. + * + * @param jQuery event + * @param jQuery closest + * A jQuery-wrapped DOM element or compatibale jQuery input. The element + * whose mouseenter and mouseleave events should be ignored. + * @param Function callback + */ + _ignoreHoveringVia: function (event, closest, callback) { + if ($(event.relatedTarget).closest(closest).length > 0) { + event.stopPropagation(); + } + else { + callback(); + } + } +}); + +})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js new file mode 100644 index 0000000..791549e --- /dev/null +++ b/core/modules/edit/js/views/EditorView.js @@ -0,0 +1,263 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * A base implementation that outlines the structure for in-place editors. + * + * Specific in-place editor implementations should subclass (extend) this View + * and override whichever method they deem necessary to override. + * + * Look at Drupal.edit.editors.form and Drupal.edit.editors.direct for + * examples. + */ + EditorView: Backbone.View.extend({ + + /** + * Implements Backbone.View.prototype.initialize(). + * + * If you override this method, you should call this method (the parent + * class' initialize()) first, like this: + * Drupal.edit.EditorView.prototype.initialize.call(this, options); + * + * For an example, @see Drupal.edit.editors.direct. + */ + initialize: function (options) { + this.model.on('change:state', this.stateChange, this); + }, + + /** + * {@inheritdoc} + */ + remove: function () { + // The el property is the field, which should not be removed. Remove the + // pointer to it, then call Backbone.View.prototype.remove(). + this.setElement(); + Backbone.View.prototype.remove.call(this); + }, + + /** + * Returns the edited element. + * + * For some single cardinality fields, it may be necessary or useful to + * not in-place edit (and hence decorate) the DOM element with the + * data-edit-id attribute (which is the field's wrapper), but a specific + * element within the field's wrapper. + * e.g. using a WYSIWYG editor on a body field should happen on the DOM + * element containing the text itself, not on the field wrapper. + * + * For example, @see Drupal.edit.editors.direct. + * + * @return jQuery + * A jQuery-wrapped DOM element. + */ + getEditedElement: function () { + return this.$el; + }, + + /** + * Returns 3 Edit UI settings that depend on the in-place editor: + * - padding: @todo + * - unifiedToolbar: @todo + * - fullWidthToolbar: @todo + */ + getEditUISettings: function () { + return { padding: false, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + // An in-place editor view will not yet exist in this state, hence + // this will never be reached. Listed for sake of completeness. + break; + case 'candidate': + // Nothing to do for the typical in-place editor: it should not be + // visible yet. + + // Except when we come from the 'invalid' state, then we clean up. + if (from === 'invalid') { + this.removeValidationErrors(); + } + break; + case 'highlighted': + // Nothing to do for the typical in-place editor: it should not be + // visible yet. + break; + case 'activating': + // The user has indicated he wants to do in-place editing: if + // something needs to be loaded (CSS/JavaScript/server data/…), then + // do so at this stage, and once the in-place editor is ready, + // set the 'active' state. + // A "loading" indicator will be shown in the UI for as long as the + // field remains in this state. + var that = this; + var loadDependencies = function (callback) { + // Do the loading here. + callback(); + }; + loadDependencies(function () { + that.model.set('state', 'active'); + }); + break; + case 'active': + // The user can now actually use the in-place editor. + break; + case 'changed': + // Nothing to do for the typical in-place editor. The UI will show an + // indicator that the field has changed. + break; + case 'saving': + // When the user has indicated he wants to save his changes to this + // field, this state will be entered. + // If the previous saving attempt resulted in validation errors, the + // previous state will be 'invalid'. Clean up those validation errors + // while the user is saving. + if (from === 'invalid') { + this.removeValidationErrors(); + } + this.save(); + break; + case 'saved': + // Nothing to do for the typical in-place editor. Immediately after + // being saved, a field will go to the 'candidate' state, where it + // should no longer be visible (after all, the field will then again + // just be a *candidate* to be in-place edited). + break; + case 'invalid': + // The modified field value was attempted to be saved, but there were + // validation errors. + this.showValidationErrors(); + break; + } + }, + + /** + * Reverts the modified value back to the original value (before editing + * started). + */ + revert: function () { + // A no-op by default; each editor should implement reverting itself. + + // Note that if the in-place editor does not cause the FieldModel's + // element to be modified, then nothing needs to happen. + }, + + /** + * Saves the modified value in the in-place editor for this field. + */ + save: function () { + var model = this.model; + + function fillAndSubmitForm (value) { + var $form = jQuery('#edit_backstage form'); + // Fill in the value in any that isn't hidden or a submit button. + $form.find(':input[type!="hidden"][type!="submit"]:not(select)') + // Don't mess with the node summary. + .not('[name$="\\[summary\\]"]').val(value); + // Submit the form. + $form.find('.edit-form-submit').trigger('click.edit'); + } + + var value = model.get('currentValue'); + + var formOptions = { + propertyID: model.get('editID'), + $editorElement: $(this.getEditedElement()), + nocssjs: true + }; + Drupal.edit.util.form.load(formOptions, function(form, ajax) { + // Create a backstage area for storing forms that are hidden from view + // (hence "backstage" — since the editing doesn't happen in the form, it + // happens "directly" in the content, the form is only used for saving). + jQuery(Drupal.theme('editBackstage', { id: 'edit_backstage' })).appendTo('body'); + // Direct forms are stuffed into #edit_backstage, apparently. + jQuery('#edit_backstage').append(form); + // Disable the browser's HTML5 validation; we only care about server- + // side validation. (Not disabling this will actually cause problems + // because browsers don't like to set HTML5 validation errors on hidden + // forms.) + jQuery('#edit_backstage form').prop('novalidate', true); + var $submit = jQuery('#edit_backstage form .edit-form-submit'); + var base = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); + + function removeHiddenForm () { + Drupal.edit.util.form.unajaxifySaving($submit); + jQuery('#edit_backstage').remove(); + } + + // Successfully saved. + Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) { + removeHiddenForm(); + + // First, transition the state to 'saved'. + model.set('state', 'saved'); + // Then, set the 'html' attribute on the field model. This will cause the + // field to be rerendered. + model.set('html', response.data); + }; + + // Unsuccessfully saved; validation errors. + Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) { + removeHiddenForm(); + + model.set('validationErrors', response.data); + model.set('state', 'invalid'); + }; + + // The editFieldForm AJAX command is only called upon loading the form + // for the first time, and when there are validation errors in the form; + // Form API then marks which form items have errors. This is useful for + // the form-based in-place editor, but pointless for any other: the form + // itself won't be visible at all anyway! So, we just ignore it. + Drupal.ajax[base].commands.editFieldForm = function() {}; + + fillAndSubmitForm(value); + }); + }, + + /** + * Shows validation error messages. + * + * Should be called when the state is changed to 'invalid'. + */ + showValidationErrors: function () { + var $errors = $('
    ') + .append(this.model.get('validationErrors')); + $(this.model.get('el')) + .addClass('edit-validation-error') + .after($errors); + }, + + /** + * Cleans up validation error messages. + * + * Should be called when the state is changed to 'candidate' or 'saving'. In + * the case of the latter: the user has modified the value in the in-place + * editor again to attempt to save again. In the case of the latter: the + * invalid value was discarded. + */ + removeValidationErrors: function () { + $(this.model.get('el')) + .removeClass('edit-validation-error') + .next('.edit-validation-errors') + .remove(); + } + + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js new file mode 100644 index 0000000..117debb --- /dev/null +++ b/core/modules/edit/js/views/FieldToolbarView.js @@ -0,0 +1,408 @@ +/** + * @file + * A Backbone View that provides an interactive toolbar (1 per property editor). + * + * It listens to state changes of the property editor. It also triggers state + * changes in response to user interactions with the toolbar, including saving. + */ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.FieldToolbarView = Backbone.View.extend({ + $field: null, + + _loader: null, + _loaderVisibleStart: 0, + + _id: null, + + events: { + 'click.edit button.label': 'onClickInfoLabel', + 'mouseleave.edit': 'onMouseLeave', + 'click.edit button.field-save': 'onClickSave', + 'click.edit button.field-close': 'onClickClose' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function (options) { + this.$field = options.$field; + this.editorView = options.editorView; + + this._loader = null; + this._loaderVisibleStart = 0; + + // Generate a DOM-compatible ID for the form container DOM element. + this._id = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_'); + + this.model.on('change:state', this.stateChange, this); + }, + + /** + * Implements Backbone.View.prototype.render(). + * + * Depending on whether the display property of the $el for which a + * toolbar is being inserted into the DOM, it will be inserted differently. + */ + render: function () { + // Render toolbar. + this.setElement($(Drupal.theme('editToolbarContainer', { + id: this._id + }))); + + // Insert in DOM. + if (this.$field.css('display') === 'inline') { + this.$el.prependTo(this.$field.offsetParent()); + var pos = this.$field.position(); + this.$el.css('left', pos.left).css('top', pos.top); + } + else { + this.$el.insertBefore(this.$field); + } + + return this; + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + if (from) { + this.remove(); + } + break; + case 'candidate': + if (from === 'inactive') { + this.render(); + } + else { + // Remove all toolgroups; they're no longer necessary. + this.$el + .removeClass('edit-highlighted edit-editing') + .find('.edit-toolbar .edit-toolgroup').remove(); + if (from !== 'highlighted' && this.getEditUISetting('padding')) { + this._unpad(); + } + } + break; + case 'highlighted': + // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title). + this.startHighlight(); + break; + case 'activating': + this.setLoadingIndicator(true); + break; + case 'active': + this.startEdit(); + this.setLoadingIndicator(false); + if (this.getEditUISetting('fullWidthToolbar')) { + this.$el.addClass('edit-toolbar-fullwidth'); + } + + if (this.getEditUISetting('padding')) { + this._pad(); + } + if (this.getEditUISetting('unifiedToolbar')) { + this.insertWYSIWYGToolGroups(); + } + break; + case 'changed': + this.$el + .find('button.save') + .addClass('blue-button') + .removeClass('gray-button'); + break; + case 'saving': + this.setLoadingIndicator(true); + break; + case 'saved': + this.setLoadingIndicator(false); + break; + case 'invalid': + this.setLoadingIndicator(false); + break; + } + }, + + /** + * Redirects the click.edit-event to the editor DOM element. + * + * @param jQuery event + */ + onClickInfoLabel: function (event) { + event.stopPropagation(); + event.preventDefault(); + // Redirects the event to the editor DOM element. + this.$field.trigger('click.edit'); + }, + + /** + * Controls mouseleave events. + * + * A mouseleave to the editor doesn't matter; a mouseleave to something else + * counts as a mouseleave on the editor itself. + * + * @param jQuery event + */ + onMouseLeave: function (event) { + if (event.relatedTarget !== this.$field[0] && !$.contains(this.$field, event.relatedTarget)) { + this.$field.trigger('mouseleave.edit'); + } + event.stopPropagation(); + }, + + /** + * Set the model state to 'saving' when the save button is clicked. + * + * @param jQuery event + */ + onClickSave: function (event) { + event.stopPropagation(); + event.preventDefault(); + this.model.set('state', 'saving'); + }, + + /** + * Sets the model state to candidate when the cancel button is clicked. + * + * @param jQuery event + */ + onClickClose: function (event) { + event.stopPropagation(); + event.preventDefault(); + this.model.set('state', 'candidate', { reason: 'cancel' }); + }, + + /** + * Indicates in the 'info' toolgroup that we're waiting for a server reponse. + * + * Prevents flickering loading indicator by only showing it after 0.6 seconds + * and if it is shown, only hiding it after another 0.6 seconds. + * + * @param Boolean enabled + * Whether the loading indicator should be displayed or not. + */ + setLoadingIndicator: function (enabled) { + var that = this; + if (enabled) { + this._loader = setTimeout(function() { + that.addClass('info', 'loading'); + that._loaderVisibleStart = new Date().getTime(); + }, 600); + } + else { + var currentTime = new Date().getTime(); + clearTimeout(this._loader); + if (this._loaderVisibleStart) { + setTimeout(function() { + that.removeClass('info', 'loading'); + }, this._loaderVisibleStart + 600 - currentTime); + } + this._loader = null; + this._loaderVisibleStart = 0; + } + }, + + /** + * + */ + startHighlight: function () { + // Retrieve the lavel to show for this field. + var label = this.model.get('label'); + + this.$el + .addClass('edit-highlighted') + .find('.edit-toolbar') + // Append the "info" toolgroup into the toolbar. + .append(Drupal.theme('editToolgroup', { + classes: 'info edit-animate-only-background-and-padding', + buttons: [ + { label: label, classes: 'blank-button label' } + ] + })); + + // Animations. + var that = this; + setTimeout(function () { + that.show('info'); + }, 0); + }, + + /** + * + */ + startEdit: function () { + this.$el + .addClass('edit-editing') + .find('.edit-toolbar') + // Append the "ops" toolgroup into the toolbar. + .append(Drupal.theme('editToolgroup', { + classes: 'ops', + buttons: [ + { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' }, + { label: '' + Drupal.t('Close') + '', classes: 'field-close close gray-button' } + ] + })); + this.show('ops'); + }, + + /** + * Retrieves a setting of the editor-specific Edit UI integration. + * + * @param String setting + * + * @see Drupal.edit.util.getEditUISetting(). + */ + getEditUISetting: function (setting) { + return Drupal.edit.util.getEditUISetting(this.editorView, setting); + }, + + /** + * Adjusts the toolbar to accomodate padding on the editor. + * + * @see EditorDecorationView._pad(). + */ + _pad: function () { + // The whole toolbar must move to the top when the property's DOM element + // is displayed inline. + if (this.$field.css('display') === 'inline') { + this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); + } + + // The toolbar must move to the top and the left. + var $hf = this.$el.find('.edit-toolbar-heightfaker'); + $hf.css({ bottom: '6px', left: '-5px' }); + + if (this.getEditUISetting('fullWidthToolbar')) { + $hf.css({ width: this.$field.width() + 10 }); + } + }, + + /** + * Undoes the changes made by _pad(). + * + * @see EditorDecorationView._unpad(). + */ + _unpad: function () { + // Move the toolbar back to its original position. + var $hf = this.$el.find('.edit-toolbar-heightfaker'); + $hf.css({ bottom: '1px', left: '' }); + + if (this.getEditUISetting('fullWidthToolbar')) { + $hf.css({ width: '' }); + } + }, + + /** + * + */ + insertWYSIWYGToolGroups: function () { + this.$el + .find('.edit-toolbar') + .append(Drupal.theme('editToolgroup', { + id: this.getFloatedWysiwygToolgroupId(), + classes: 'wysiwyg-floated', + buttons: [] + })) + .append(Drupal.theme('editToolgroup', { + id: this.getMainWysiwygToolgroupId(), + classes: 'wysiwyg-main', + buttons: [] + })); + + // Animate the toolgroups into visibility. + var that = this; + setTimeout(function () { + that.show('wysiwyg-floated'); + that.show('wysiwyg-main'); + }, 0); + }, + + /** + * Retrieves the ID for this toolbar's container. + * + * Only used to make sane hovering behavior possible. + * + * @return String + * A string that can be used as the ID for this toolbar's container. + */ + getId: function () { + return 'edit-toolbar-for-' + this._id; + }, + + /** + * Retrieves the ID for this toolbar's floating WYSIWYG toolgroup. + * + * Used to provide an abstraction for any WYSIWYG editor to plug in. + * + * @return String + * A string that can be used as the ID. + */ + getFloatedWysiwygToolgroupId: function () { + return 'edit-wysiwyg-floated-toolgroup-for-' + this._id; + }, + + /** + * Retrieves the ID for this toolbar's main WYSIWYG toolgroup. + * + * Used to provide an abstraction for any WYSIWYG editor to plug in. + * + * @return String + * A string that can be used as the ID. + */ + getMainWysiwygToolgroupId: function () { + return 'edit-wysiwyg-main-toolgroup-for-' + this._id; + }, + + /** + * Shows a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + show: function (toolgroup) { + this._find(toolgroup).removeClass('edit-animate-invisible'); + }, + + /** + * Adds classes to a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + addClass: function (toolgroup, classes) { + this._find(toolgroup).addClass(classes); + }, + + /** + * Removes classes from a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + removeClass: function (toolgroup, classes) { + this._find(toolgroup).removeClass(classes); + }, + + /** + * Finds a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + _find: function (toolgroup) { + return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup); + } +}); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/ModalView.js b/core/modules/edit/js/views/ModalView.js new file mode 100644 index 0000000..15497cc --- /dev/null +++ b/core/modules/edit/js/views/ModalView.js @@ -0,0 +1,80 @@ +/** + * @file + * A Backbone View that provides an interactive modal. + */ +(function ($, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.ModalView = Backbone.View.extend({ + + message: null, + buttons: null, + callback: null, + $elementsToHide: null, + + events: { + 'click button': 'onButtonClick' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + * + * @param Object options + * An object with the following keys: + * - message: a message to show in the modal. + * - buttons: a set of buttons with 'action's defined, ready to be passed to + * Drupal.theme.editButtons(). + * - callback: a callback that will receive the 'action' of the clicked + * button. + * + * @see Drupal.theme.editModal() + * @see Drupal.theme.editButtons() + */ + initialize: function (options) { + this.message = options.message; + this.buttons = options.buttons; + this.callback = options.callback; + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function () { + this.setElement(Drupal.theme('editModal', {})); + this.$el.appendTo('body'); + // Template. + this.$('.main p').text(this.message); + var $actions = $(Drupal.theme('editButtons', { 'buttons' : this.buttons})); + this.$('.actions').append($actions); + + // Show the modal with an animation. + var that = this; + setTimeout(function() { + that.$el.removeClass('edit-animate-invisible'); + }, 0); + }, + + /** + * Passes the clicked button action to the callback; closes the modal. + * + * @param jQuery event + */ + onButtonClick: function (event) { + event.stopPropagation(); + event.preventDefault(); + + // Remove after animation. + var that = this; + this.$el + .addClass('edit-animate-invisible') + .on(Drupal.edit.util.constants.transitionEnd, function(e) { + that.remove(); + }); + + var action = $(event.target).attr('data-edit-modal-action'); + return this.callback(action); + } +}); + +})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/contextuallink-view.js b/core/modules/edit/js/views/contextuallink-view.js deleted file mode 100644 index 472bd6e..0000000 --- a/core/modules/edit/js/views/contextuallink-view.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @file - * A Backbone View that a dynamic contextual link. - */ -(function ($, _, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ - - entity: null, - - events: function () { - // Prevents delay and simulated mouse events. - function touchEndToClick (event) { - event.preventDefault(); - event.target.click(); - } - return { - 'click a': 'onClick', - 'touchEnd a': touchEndToClick - }; - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - entity: the entity ID (e.g. node/1) of the entity - */ - initialize: function (options) { - this.entity = options.entity; - - // Initial render. - this.render(); - - // Re-render whenever the app state's active entity changes. - this.model.on('change:activeEntity', this.render, this); - - // Hide the contextual links whenever an in-place editor is active. - this.model.on('change:activeEditor', this.toggleContextualLinksVisibility, this); - }, - - /** - * Equates clicks anywhere on the overlay to clicking the active editor's (if - * any) "close" button. - * - * @param {Object} event - */ - onClick: function (event) { - event.preventDefault(); - - var that = this; - function updateActiveEntity () { - // The active entity is the current entity, i.e. stop editing the current - // entity. - if (that.model.get('activeEntity') === that.entity) { - that.model.set('activeEntity', null); - } - // The active entity is different from the current entity, i.e. start - // editing this entity instead of the previous one. - else { - that.model.set('activeEntity', that.entity); - } - } - - // If there's an active editor, attempt to set its state to 'candidate', and - // only then do what the user asked. - // (Only when all PropertyEditor widgets of an entity are in the 'candidate' - // state, it is possible to stop editing it.) - var activeEditor = this.model.get('activeEditor'); - if (activeEditor) { - var editableEntity = activeEditor.options.widget; - var predicate = activeEditor.options.property; - editableEntity.setState('candidate', predicate, { reason: 'stop or switch' }, function (accepted) { - if (accepted) { - updateActiveEntity(); - } - else { - // No change. - } - }); - } - // Otherwise, we can immediately do what the user asked. - else { - updateActiveEntity(); - } - }, - - /** - * Render the "Quick edit" contextual link. - */ - render: function () { - var activeEntity = this.model.get('activeEntity'); - var string = (activeEntity !== this.entity) ? Drupal.t('Quick edit') : Drupal.t('Stop quick edit'); - this.$el.find('a').text(string); - return this; - }, - - /** - * Model change handler; hides the contextual links if an editor is active. - * - * @param Drupal.edit.models.EditAppModel model - * An EditAppModel model. - * @param jQuery|null activeEditor - * The active in-place editor (jQuery object) or, if none, null. - */ - toggleContextualLinksVisibility: function (model, activeEditor) { - this.$el.parents('.contextual').toggle(activeEditor === null); - } - -}); - -})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/modal-view.js b/core/modules/edit/js/views/modal-view.js deleted file mode 100644 index b98c876..0000000 --- a/core/modules/edit/js/views/modal-view.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @file - * A Backbone View that provides an interactive modal. - */ -(function($, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ModalView = Backbone.View.extend({ - - message: null, - buttons: null, - callback: null, - $elementsToHide: null, - - events: { - 'click button': 'onButtonClick' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - message: a message to show in the modal. - * - buttons: a set of buttons with 'action's defined, ready to be passed to - * Drupal.theme.editButtons(). - * - callback: a callback that will receive the 'action' of the clicked - * button. - * - * @see Drupal.theme.editModal() - * @see Drupal.theme.editButtons() - */ - initialize: function(options) { - this.message = options.message; - this.buttons = options.buttons; - this.callback = options.callback; - }, - - /** - * Implements Backbone Views' render() function. - */ - render: function() { - this.setElement(Drupal.theme('editModal', {})); - this.$el.appendTo('body'); - // Template. - this.$('.main p').text(this.message); - var $actions = $(Drupal.theme('editButtons', { 'buttons' : this.buttons})); - this.$('.actions').append($actions); - - // Show the modal with an animation. - var that = this; - setTimeout(function() { - that.$el.removeClass('edit-animate-invisible'); - }, 0); - }, - - /** - * When the user clicks on any of the buttons, the modal should be removed - * and the result should be passed to the callback. - * - * @param event - */ - onButtonClick: function(event) { - event.stopPropagation(); - event.preventDefault(); - - // Remove after animation. - var that = this; - this.$el - .addClass('edit-animate-invisible') - .on(Drupal.edit.util.constants.transitionEnd, function(e) { - that.remove(); - }); - - var action = $(event.target).attr('data-edit-modal-action'); - return this.callback(action); - } -}); - -})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js deleted file mode 100644 index bee33d1..0000000 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ /dev/null @@ -1,363 +0,0 @@ -/** - * @file - * A Backbone View that decorates a Property Editor widget. - * - * It listens to state changes of the property editor. - */ -(function($, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ - - toolbarId: null, - - _widthAttributeIsEmpty: null, - - events: { - 'mouseenter.edit' : 'onMouseEnter', - 'mouseleave.edit' : 'onMouseLeave', - 'click': 'onClick', - 'tabIn.edit': 'onMouseEnter', - 'tabOut.edit': 'onMouseLeave' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - editor: the editor object with an 'options' object that has these keys: - * * entity: the VIE entity for the property. - * * property: the predicate of the property. - * * widget: the parent EditableEntity widget. - * * editorName: the name of the PropertyEditor widget - * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. - */ - initialize: function(options) { - this.editor = options.editor; - this.toolbarId = options.toolbarId; - - this.predicate = this.editor.options.property; - this.editorName = this.editor.options.editorName; - - // Only start listening to events as soon as we're no longer in the 'inactive' state. - this.undelegateEvents(); - }, - - /** - * Listens to editor state changes. - */ - stateChange: function(from, to) { - switch (to) { - case 'inactive': - if (from !== null) { - this.undecorate(); - if (from === 'invalid') { - this._removeValidationErrors(); - } - } - break; - case 'candidate': - this.decorate(); - if (from !== 'inactive') { - this.stopHighlight(); - if (from !== 'highlighted') { - this.stopEdit(); - if (from === 'invalid') { - this._removeValidationErrors(); - } - } - } - break; - case 'highlighted': - this.startHighlight(); - break; - case 'activating': - // NOTE: this state is not used by every editor! It's only used by those - // that need to interact with the server. - this.prepareEdit(); - break; - case 'active': - if (from !== 'activating') { - this.prepareEdit(); - } - this.startEdit(); - break; - case 'changed': - break; - case 'saving': - if (from === 'invalid') { - this._removeValidationErrors(); - } - break; - case 'saved': - break; - case 'invalid': - break; - } - }, - - /** - * Starts hover: transition to 'highlight' state. - * - * @param event - */ - onMouseEnter: function(event) { - var that = this; - this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { - var editableEntity = that.editor.options.widget; - editableEntity.setState('highlighted', that.predicate); - event.stopPropagation(); - }); - }, - - /** - * Stops hover: back to 'candidate' state. - * - * @param event - */ - onMouseLeave: function(event) { - var that = this; - this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { - var editableEntity = that.editor.options.widget; - editableEntity.setState('candidate', that.predicate, { reason: 'mouseleave' }); - event.stopPropagation(); - }); - }, - - /** - * Clicks: transition to 'activating' stage. - * - * @param event - */ - onClick: function(event) { - var editableEntity = this.editor.options.widget; - editableEntity.setState('activating', this.predicate); - event.preventDefault(); - event.stopPropagation(); - }, - - decorate: function () { - this.$el.addClass('edit-animate-fast edit-candidate edit-editable'); - this.delegateEvents(); - }, - - undecorate: function () { - this.$el - .removeClass('edit-candidate edit-editable edit-highlighted edit-editing'); - this.undelegateEvents(); - }, - - startHighlight: function () { - // Animations. - var that = this; - setTimeout(function() { - that.$el.addClass('edit-highlighted'); - }, 0); - }, - - stopHighlight: function() { - this.$el - .removeClass('edit-highlighted'); - }, - - prepareEdit: function() { - this.$el.addClass('edit-editing'); - - // While editing, don't show *any* other editors. - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Revisit this. - $('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); - }, - - startEdit: function() { - if (this.getEditUISetting('padding')) { - this._pad(); - } - }, - - stopEdit: function() { - this.$el.removeClass('edit-highlighted edit-editing'); - - // Make the other editors show up again. - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Revisit this. - $('.edit-candidate').addClass('edit-editable'); - - if (this.getEditUISetting('padding')) { - this._unpad(); - } - }, - - /** - * Retrieves a setting of the editor-specific Edit UI integration. - * - * @see Drupal.edit.util.getEditUISetting(). - */ - getEditUISetting: function(setting) { - return Drupal.edit.util.getEditUISetting(this.editor, setting); - }, - - _pad: function () { - var self = this; - - // Add 5px padding for readability. This means we'll freeze the current - // width and *then* add 5px padding, hence ensuring the padding is added "on - // the outside". - // 1) Freeze the width (if it's not already set); don't use animations. - if (this.$el[0].style.width === "") { - this._widthAttributeIsEmpty = true; - this.$el - .addClass('edit-animate-disable-width') - .css('width', this.$el.width()) - .css('background-color', this._getBgColor(this.$el)); - } - - // 2) Add padding; use animations. - var posProp = this._getPositionProperties(this.$el); - setTimeout(function() { - // Re-enable width animations (padding changes affect width too!). - self.$el.removeClass('edit-animate-disable-width'); - - // Pad the editable. - self.$el - .css({ - 'position': 'relative', - 'top': posProp.top - 5 + 'px', - 'left': posProp.left - 5 + 'px', - 'padding-top' : posProp['padding-top'] + 5 + 'px', - 'padding-left' : posProp['padding-left'] + 5 + 'px', - 'padding-right' : posProp['padding-right'] + 5 + 'px', - 'padding-bottom': posProp['padding-bottom'] + 5 + 'px', - 'margin-bottom': posProp['margin-bottom'] - 10 + 'px' - }); - }, 0); - }, - - _unpad: function () { - var self = this; - - // 1) Set the empty width again. - if (this._widthAttributeIsEmpty) { - this.$el - .addClass('edit-animate-disable-width') - .css('width', '') - .css('background-color', ''); - } - - // 2) Remove padding; use animations (these will run simultaneously with) - // the fading out of the toolbar as its gets removed). - var posProp = this._getPositionProperties(this.$el); - setTimeout(function() { - // Re-enable width animations (padding changes affect width too!). - self.$el.removeClass('edit-animate-disable-width'); - - // Unpad the editable. - self.$el - .css({ - 'position': 'relative', - 'top': posProp.top + 5 + 'px', - 'left': posProp.left + 5 + 'px', - 'padding-top' : posProp['padding-top'] - 5 + 'px', - 'padding-left' : posProp['padding-left'] - 5 + 'px', - 'padding-right' : posProp['padding-right'] - 5 + 'px', - 'padding-bottom': posProp['padding-bottom'] - 5 + 'px', - 'margin-bottom': posProp['margin-bottom'] + 10 + 'px' - }); - }, 0); - }, - - /** - * Gets the background color of an element (or the inherited one). - * - * @param $e - * A DOM element. - */ - _getBgColor: function($e) { - var c; - - if ($e === null || $e[0].nodeName === 'HTML') { - // Fallback to white. - return 'rgb(255, 255, 255)'; - } - c = $e.css('background-color'); - // TRICKY: edge case for Firefox' "transparent" here; this is a - // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724 - if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') { - return this._getBgColor($e.parent()); - } - return c; - }, - - /** - * Gets the top and left properties of an element and convert extraneous - * values and information into numbers ready for subtraction. - * - * @param $e - * A DOM element. - */ - _getPositionProperties: function($e) { - var p, - r = {}, - props = [ - 'top', 'left', 'bottom', 'right', - 'padding-top', 'padding-left', 'padding-right', 'padding-bottom', - 'margin-bottom' - ]; - - var propCount = props.length; - for (var i = 0; i < propCount; i++) { - p = props[i]; - r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10); - } - return r; - }, - - /** - * Replaces blank or 'auto' CSS "position: " values with "0px". - * - * @param pos - * The value for a CSS position declaration. - */ - _replaceBlankPosition: function(pos) { - if (pos === 'auto' || !pos) { - pos = '0px'; - } - return pos; - }, - - /** - * Ignores hovering to/from the given closest element, but as soon as a hover - * occurs to/from *another* element, then call the given callback. - */ - _ignoreHoveringVia: function(event, closest, callback) { - if ($(event.relatedTarget).closest(closest).length > 0) { - event.stopPropagation(); - } - else { - callback(); - } - }, - - /** - * Removes validation errors' markup changes, if any. - * - * Note: this only needs to happen for type=direct, because for type=direct, - * the property DOM element itself is modified; this is not the case for - * type=form. - */ - _removeValidationErrors: function() { - if (this.editorName !== 'form') { - this.$el - .removeClass('edit-validation-error') - .next('.edit-validation-errors') - .remove(); - } - } - -}); - -})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js deleted file mode 100644 index f4b2123..0000000 --- a/core/modules/edit/js/views/toolbar-view.js +++ /dev/null @@ -1,490 +0,0 @@ -/** - * @file - * A Backbone View that provides an interactive toolbar (1 per property editor). - * - * It listens to state changes of the property editor. It also triggers state - * changes in response to user interactions with the toolbar, including saving. - */ -(function ($, _, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; -Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.ToolbarView = Backbone.View.extend({ - - editor: null, - $storageWidgetEl: null, - - entity: null, - predicate : null, - editorName: null, - - _loader: null, - _loaderVisibleStart: 0, - - _id: null, - - events: { - 'click.edit button.label': 'onClickInfoLabel', - 'mouseleave.edit': 'onMouseLeave', - 'click.edit button.field-save': 'onClickSave', - 'click.edit button.field-close': 'onClickClose' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - editor: the editor object with an 'options' object that has these keys: - * * entity: the VIE entity for the property. - * * property: the predicate of the property. - * * editorName: the editor name. - * * element: the jQuery-wrapped editor DOM element - * - $storageWidgetEl: the DOM element on which the Create Storage widget is - * initialized. - */ - initialize: function(options) { - this.editor = options.editor; - this.$storageWidgetEl = options.$storageWidgetEl; - - this.entity = this.editor.options.entity; - this.predicate = this.editor.options.property; - this.editorName = this.editor.options.editorName; - - this._loader = null; - this._loaderVisibleStart = 0; - - // Generate a DOM-compatible ID for the toolbar DOM element. - this._id = Drupal.edit.util.calcPropertyID(this.entity, this.predicate).replace(/\//g, '_'); - }, - - /** - * Listens to editor state changes. - */ - stateChange: function(from, to) { - switch (to) { - case 'inactive': - if (from) { - this.remove(); - if (this.editorName !== 'form') { - Backbone.syncDirectCleanUp(); - } - } - break; - case 'candidate': - if (from === 'inactive') { - this.render(); - } - else { - if (this.editorName !== 'form') { - Backbone.syncDirectCleanUp(); - } - // Remove all toolgroups; they're no longer necessary. - this.$el - .removeClass('edit-highlighted edit-editing') - .find('.edit-toolbar .edit-toolgroup').remove(); - if (from !== 'highlighted' && this.getEditUISetting('padding')) { - this._unpad(); - } - } - break; - case 'highlighted': - // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title). - this.startHighlight(); - break; - case 'activating': - this.setLoadingIndicator(true); - break; - case 'active': - this.startEdit(); - this.setLoadingIndicator(false); - if (this.getEditUISetting('fullWidthToolbar')) { - this.$el.addClass('edit-toolbar-fullwidth'); - } - - if (this.getEditUISetting('padding')) { - this._pad(); - } - if (this.getEditUISetting('unifiedToolbar')) { - this.insertWYSIWYGToolGroups(); - } - break; - case 'changed': - this.$el - .find('button.save') - .addClass('blue-button') - .removeClass('gray-button'); - break; - case 'saving': - this.setLoadingIndicator(true); - this.save(); - break; - case 'saved': - this.setLoadingIndicator(false); - break; - case 'invalid': - this.setLoadingIndicator(false); - break; - } - }, - - /** - * Saves a property. - * - * This method deals with the complexity of the editor-dependent ways of - * inserting updated content and showing validation error messages. - * - * One might argue that this does not belong in a view. However, there is no - * actual "save" logic here, that lives in Backbone.sync. This is just some - * glue code, along with the logic for inserting updated content as well as - * showing validation error messages, the latter of which is certainly okay. - */ - save: function() { - var that = this; - var editor = this.editor; - var editableEntity = editor.options.widget; - var entity = editor.options.entity; - var predicate = editor.options.property; - - // Use Create.js' Storage widget to handle saving. (Uses Backbone.sync.) - this.$storageWidgetEl.createStorage('saveRemote', entity, { - editor: editor, - - // Successfully saved without validation errors. - success: function (model) { - editableEntity.setState('saved', predicate); - - // Now that the changes to this property have been saved, the saved - // attributes are now the "original" attributes. - entity._originalAttributes = entity._previousAttributes = _.clone(entity.attributes); - - // Get data necessary to rerender property before it is unavailable. - var updatedProperty = entity.get(predicate + '/rendered'); - var $propertyWrapper = editor.element.closest('.edit-field'); - var $context = $propertyWrapper.parent(); - - editableEntity.setState('candidate', predicate); - // Unset the property, because it will be parsed again from the DOM, iff - // its new value causes it to still be rendered. - entity.unset(predicate, { silent: true }); - entity.unset(predicate + '/rendered', { silent: true }); - // Trigger event to allow for proper clean-up of editor-specific views. - editor.element.trigger('destroyedPropertyEditor.edit', editor); - - // Replace the old content with the new content. - $propertyWrapper.replaceWith(updatedProperty); - Drupal.attachBehaviors($context); - }, - - // Save attempted but failed due to validation errors. - error: function (validationErrorMessages) { - editableEntity.setState('invalid', predicate); - - if (that.editorName === 'form') { - editor.$formContainer - .find('.edit-form') - .addClass('edit-validation-error') - .find('form') - .prepend(validationErrorMessages); - } - else { - var $errors = $('
    ') - .append(validationErrorMessages); - editor.element - .addClass('edit-validation-error') - .after($errors); - } - } - }); - }, - - /** - * When the user clicks the info label, nothing should happen. - * @note currently redirects the click.edit-event to the editor DOM element. - * - * @param event - */ - onClickInfoLabel: function(event) { - event.stopPropagation(); - event.preventDefault(); - // Redirects the event to the editor DOM element. - this.editor.element.trigger('click.edit'); - }, - - /** - * A mouseleave to the editor doesn't matter; a mouseleave to something else - * counts as a mouseleave on the editor itself. - * - * @param event - */ - onMouseLeave: function(event) { - var el = this.editor.element[0]; - if (event.relatedTarget != el && !$.contains(el, event.relatedTarget)) { - this.editor.element.trigger('mouseleave.edit'); - } - event.stopPropagation(); - }, - - /** - * Upon clicking "Save", trigger a custom event to save this property. - * - * @param event - */ - onClickSave: function(event) { - event.stopPropagation(); - event.preventDefault(); - this.editor.options.widget.setState('saving', this.predicate); - }, - - /** - * Upon clicking "Close", trigger a custom event to stop editing. - * - * @param event - */ - onClickClose: function(event) { - event.stopPropagation(); - event.preventDefault(); - this.editor.options.widget.setState('candidate', this.predicate, { reason: 'cancel' }); - }, - - /** - * Indicates in the 'info' toolgroup that we're waiting for a server reponse. - * - * Prevents flickering loading indicator by only showing it after 0.6 seconds - * and if it is shown, only hiding it after another 0.6 seconds. - * - * @param bool enabled - * Whether the loading indicator should be displayed or not. - */ - setLoadingIndicator: function(enabled) { - var that = this; - if (enabled) { - this._loader = setTimeout(function() { - that.addClass('info', 'loading'); - that._loaderVisibleStart = new Date().getTime(); - }, 600); - } - else { - var currentTime = new Date().getTime(); - clearTimeout(this._loader); - if (this._loaderVisibleStart) { - setTimeout(function() { - that.removeClass('info', 'loading'); - }, this._loaderVisibleStart + 600 - currentTime); - } - this._loader = null; - this._loaderVisibleStart = 0; - } - }, - - startHighlight: function() { - // We get the label to show for this property from VIE's type system. - var label = this.predicate; - var attributeDef = this.entity.get('@type').attributes.get(this.predicate); - if (attributeDef && attributeDef.metadata) { - label = attributeDef.metadata.label; - } - - this.$el - .addClass('edit-highlighted') - .find('.edit-toolbar') - // Append the "info" toolgroup into the toolbar. - .append(Drupal.theme('editToolgroup', { - classes: 'info edit-animate-only-background-and-padding', - buttons: [ - { label: label, classes: 'blank-button label' } - ] - })); - - // Animations. - var that = this; - setTimeout(function () { - that.show('info'); - }, 0); - }, - - startEdit: function() { - this.$el - .addClass('edit-editing') - .find('.edit-toolbar') - // Append the "ops" toolgroup into the toolbar. - .append(Drupal.theme('editToolgroup', { - classes: 'ops', - buttons: [ - { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' }, - { label: '' + Drupal.t('Close') + '', classes: 'field-close close gray-button' } - ] - })); - this.show('ops'); - }, - - /** - * Retrieves a setting of the editor-specific Edit UI integration. - * - * @see Drupal.edit.util.getEditUISetting(). - */ - getEditUISetting: function(setting) { - return Drupal.edit.util.getEditUISetting(this.editor, setting); - }, - - /** - * Adjusts the toolbar to accomodate padding on the PropertyEditor widget. - * - * @see PropertyEditorDecorationView._pad(). - */ - _pad: function() { - // The whole toolbar must move to the top when the property's DOM element - // is displayed inline. - if (this.editor.element.css('display') === 'inline') { - this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); - } - - // The toolbar must move to the top and the left. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '6px', left: '-5px' }); - - if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: this.editor.element.width() + 10 }); - } - }, - - /** - * Undoes the changes made by _pad(). - * - * @see PropertyEditorDecorationView._unpad(). - */ - _unpad: function() { - // Move the toolbar back to its original position. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '1px', left: '' }); - - if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: '' }); - } - }, - - insertWYSIWYGToolGroups: function() { - this.$el - .find('.edit-toolbar') - .append(Drupal.theme('editToolgroup', { - id: this.getFloatedWysiwygToolgroupId(), - classes: 'wysiwyg-floated', - buttons: [] - })) - .append(Drupal.theme('editToolgroup', { - id: this.getMainWysiwygToolgroupId(), - classes: 'wysiwyg-main', - buttons: [] - })); - - // Animate the toolgroups into visibility. - var that = this; - setTimeout(function () { - that.show('wysiwyg-floated'); - that.show('wysiwyg-main'); - }, 0); - }, - - /** - * Renders the Toolbar's markup into the DOM. - * - * Note: depending on whether the 'display' property of the $el for which a - * toolbar is being inserted into the DOM, it will be inserted differently. - */ - render: function () { - // Render toolbar. - this.setElement($(Drupal.theme('editToolbarContainer', { - id: this.getId() - }))); - - // Insert in DOM. - if (this.editor.element.css('display') === 'inline') { - this.$el.prependTo(this.editor.element.offsetParent()); - var pos = this.editor.element.position(); - this.$el.css('left', pos.left).css('top', pos.top); - } - else { - this.$el.insertBefore(this.editor.element); - } - }, - - /** - * Retrieves the ID for this toolbar's container. - * - * Only used to make sane hovering behavior possible. - * - * @return string - * A string that can be used as the ID for this toolbar's container. - */ - getId: function () { - return 'edit-toolbar-for-' + this._id; - }, - - /** - * Retrieves the ID for this toolbar's floating WYSIWYG toolgroup. - * - * Used to provide an abstraction for any WYSIWYG editor to plug in. - * - * @return string - * A string that can be used as the ID. - */ - getFloatedWysiwygToolgroupId: function () { - return 'edit-wysiwyg-floated-toolgroup-for-' + this._id; - }, - - /** - * Retrieves the ID for this toolbar's main WYSIWYG toolgroup. - * - * Used to provide an abstraction for any WYSIWYG editor to plug in. - * - * @return string - * A string that can be used as the ID. - */ - getMainWysiwygToolgroupId: function () { - return 'edit-wysiwyg-main-toolgroup-for-' + this._id; - }, - - /** - * Shows a toolgroup. - * - * @param string toolgroup - * A toolgroup name. - */ - show: function (toolgroup) { - this._find(toolgroup).removeClass('edit-animate-invisible'); - }, - - /** - * Adds classes to a toolgroup. - * - * @param string toolgroup - * A toolgroup name. - */ - addClass: function (toolgroup, classes) { - this._find(toolgroup).addClass(classes); - }, - - /** - * Removes classes from a toolgroup. - * - * @param string toolgroup - * A toolgroup name. - */ - removeClass: function (toolgroup, classes) { - this._find(toolgroup).removeClass(classes); - }, - - /** - * Finds a toolgroup. - * - * @param string toolgroup - * A toolgroup name. - */ - _find: function (toolgroup) { - return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup); - } -}); - -})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/lib/Drupal/edit/EditorBase.php b/core/modules/edit/lib/Drupal/edit/EditorBase.php index 2bef9cf..39abaee 100644 --- a/core/modules/edit/lib/Drupal/edit/EditorBase.php +++ b/core/modules/edit/lib/Drupal/edit/EditorBase.php @@ -12,7 +12,7 @@ use Drupal\field\Plugin\Core\Entity\FieldInstance; /** - * Defines a base editor (Create.js PropertyEditor widget) implementation. + * Defines a base editor implementation. */ abstract class EditorBase extends PluginBase implements EditPluginInterface { diff --git a/core/modules/edit/lib/Drupal/edit/EditorSelector.php b/core/modules/edit/lib/Drupal/edit/EditorSelector.php index ce80def..c42cb02 100644 --- a/core/modules/edit/lib/Drupal/edit/EditorSelector.php +++ b/core/modules/edit/lib/Drupal/edit/EditorSelector.php @@ -17,7 +17,7 @@ class EditorSelector implements EditorSelectorInterface { /** - * The manager for editor (Create.js PropertyEditor widget) plugins. + * The manager for editor plugins. * * @var \Drupal\Component\Plugin\PluginManagerInterface */ @@ -34,7 +34,7 @@ class EditorSelector implements EditorSelectorInterface { * Constructs a new EditorSelector. * * @param \Drupal\Component\Plugin\PluginManagerInterface - * The manager for Create.js PropertyEditor widget plugins. + * The manager for editor plugins. */ public function __construct(PluginManagerInterface $editor_manager) { $this->editorManager = $editor_manager; @@ -102,22 +102,6 @@ public function getEditorAttachments(array $editor_ids) { $attachments[] = $editor->getAttachments(); } - // JavaScript settings for Edit. - $definitions = $this->editorManager->getDefinitions(); - foreach ($definitions as $definition) { - $attachments[] = array( - // This will be used in Create.js' propertyEditorWidgetsConfiguration. - 'js' => array( - array( - 'type' => 'setting', - 'data' => array('edit' => array('editors' => array( - $definition['id'] => array('widget' => $definition['jsClassName']) - ))) - ) - ) - ); - } - return NestedArray::mergeDeepArray($attachments); } diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php index b613442..cecc676 100644 --- a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php +++ b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php @@ -33,7 +33,7 @@ class MetadataGenerator implements MetadataGeneratorInterface { protected $editorSelector; /** - * The manager for editor (Create.js PropertyEditor widget) plugins. + * The manager for editor plugins. * * @var \Drupal\Component\Plugin\PluginManagerInterface */ diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php b/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php index 8d0032f..fbcbda7 100644 --- a/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php +++ b/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php @@ -17,7 +17,7 @@ /** * Editor manager. * - * The "Form" Create.js PropertyEditor widget must always be available. + * The form editor must always be available. */ class EditorManager extends PluginManagerBase { diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php index c3b9b55..c598604 100644 --- a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php +++ b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/DirectEditor.php @@ -12,7 +12,7 @@ use Drupal\field\Plugin\Core\Entity\FieldInstance; /** - * Defines the "direct" Create.js PropertyEditor widget. + * Defines the direct editor. * * @Plugin( * id = "direct", diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php index c8d8bc8..c5f5fe3 100644 --- a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php +++ b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/FormEditor.php @@ -12,7 +12,7 @@ use Drupal\field\Plugin\Core\Entity\FieldInstance; /** - * Defines the "form" Create.js PropertyEditor widget. + * Defines the form editor. * * @Plugin( * id = "form", diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php b/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php index e1ce851..d3e5e97 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php @@ -110,7 +110,7 @@ function testUserWithPermission() { $this->assertRaw('data-edit-id="node/1/body/und/full"'); // Retrieving the metadata should result in a 200 response, containing: - // 1. a settings command with correct in-place editor metadata + // 1. a settings command with useless metadata: AjaxController is dumb // 2. an insert command that loads the required in-place editors // 3. a metadata command with correct per-field metadata $response = $this->retrieveMetadata(array('node/1/body/und/full')); @@ -120,15 +120,10 @@ function testUserWithPermission() { // First command: settings. $this->assertIdentical('settings', $ajax_commands[0]['command'], 'The first AJAX command is a settings command.'); - $edit_editors = array( - 'direct' => array('widget' => 'direct'), - 'form' => array('widget' => 'formEditEditor'), - ); - $this->assertIdentical($edit_editors, $ajax_commands[0]['settings']['edit']['editors'], 'The settings command contains the expected settings.'); // Second command: insert libraries into DOM. $this->assertIdentical('insert', $ajax_commands[1]['command'], 'The second AJAX command is an append command.'); - $command = new AppendCommand('body', '' . "\n"); + $command = new AppendCommand('body', '' . "\n"); $this->assertIdentical($command->render(), $ajax_commands[1], 'The append command contains the expected data.'); // Third command: actual metadata. diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php b/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php index 26ec6c0..767c570 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php @@ -16,7 +16,7 @@ class EditorSelectionTest extends EditTestBase { /** - * The manager for editor (Create.js PropertyEditor widget) plugins. + * The manager for editor plugins. * * @var \Drupal\Component\Plugin\PluginManagerInterface */ @@ -106,8 +106,7 @@ function testText() { * processing, but with varying text format compatibility. */ function testTextWysiwyg() { - // Enable edit_test module so that the 'wysiwyg' Create.js PropertyEditor - // widget becomes available. + // Enable edit_test module so that the 'wysiwyg' editor becomes available. $this->enableModules(array('edit_test')); $field_name = 'field_textarea'; diff --git a/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php b/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php index 6c4569f..1394285 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php @@ -18,7 +18,7 @@ class MetadataGeneratorTest extends EditTestBase { /** - * The manager for editor (Create.js PropertyEditor widget) plugins. + * The manager for editor plugins. * * @var \Drupal\Component\Plugin\PluginManagerInterface */ @@ -128,8 +128,7 @@ function testEditorWithCustomMetadata() { $this->installSchema('system', 'url_alias'); $this->enableModules(array('user', 'filter')); - // Enable edit_test module so that the WYSIWYG Create.js PropertyEditor - // widget becomes available. + // Enable edit_test module so that the WYSIWYG editor becomes available. $this->enableModules(array('edit_test')); // Create a rich text field. diff --git a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/WysiwygEditor.php b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/WysiwygEditor.php index e40e8b4..81ff964 100644 --- a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/WysiwygEditor.php +++ b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/WysiwygEditor.php @@ -12,7 +12,7 @@ use Drupal\field\Plugin\Core\Entity\FieldInstance; /** - * Defines the "wysiwyg" Create.js PropertyEditor widget. + * Defines the wysiwyg editor. * * @Plugin( * id = "wysiwyg", diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index 10f938b..0f317b9 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -78,12 +78,12 @@ function editor_library_info() { array('system', 'jquery.once'), ), ); - // Create.js PropertyEditor widget library names begin with "edit.editor". - $libraries['edit.editorWidget.editor'] = array( - 'title' => '"Editor" Create.js PropertyEditor widget', + + $libraries['edit.formattedTextEditor.editor'] = array( + 'title' => 'Formatted text editor', 'version' => VERSION, 'js' => array( - $path . '/js/editor.createjs.js' => array( + $path . '/js/editor.formattedTextEditor.js' => array( 'scope' => 'footer', 'attributes' => array('defer' => TRUE), ), diff --git a/core/modules/editor/js/editor.createjs.js b/core/modules/editor/js/editor.createjs.js deleted file mode 100644 index 693d9d9..0000000 --- a/core/modules/editor/js/editor.createjs.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * @file - * Text editor-based Create.js widget for processed text content in Drupal. - * - * Depends on editor.module. Works with any (WYSIWYG) editor that implements the - * editor.js API, including the optional attachInlineEditor() and onChange() - * methods. - * For example, assuming that a hypothetical editor's name was "Magical Editor" - * and its editor.js API implementation lived at Drupal.editors.magical, this - * JavaScript would use: - * - Drupal.editors.magical.attachInlineEditor() - * - Drupal.editors.magical.onChange() - * - Drupal.editors.magical.detach() - */ -(function (jQuery, Drupal, drupalSettings) { - -"use strict"; - -// @todo D8: use jQuery UI Widget bridging. -// @see http://drupal.org/node/1874934#comment-7124904 -jQuery.widget('Midgard.editor', jQuery.Midgard.editWidget, { - - textFormat: null, - textFormatHasTransformations: null, - textEditor: null, - - /** - * Implements Create.editWidget.getEditUISettings. - */ - getEditUISettings: function () { - return { padding: true, unifiedToolbar: true, fullWidthToolbar: true }; - }, - - /** - * Implements jQuery.widget._init. - * - * @todo D8: Remove this. - * @see http://drupal.org/node/1874934 - */ - _init: function () {}, - - /** - * Implements Create.editWidget._initialize. - */ - _initialize: function () { - var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); - var metadata = Drupal.edit.metadataCache[propertyID].custom; - - this.textFormat = drupalSettings.editor.formats[metadata.format]; - this.textFormatHasTransformations = metadata.formatHasTransformations; - this.textEditor = Drupal.editors[this.textFormat.editor]; - }, - - /** - * Implements Create.editWidget.stateChange. - */ - stateChange: function (from, to) { - var that = this; - switch (to) { - case 'inactive': - break; - - case 'candidate': - // Detach the text editor when entering the 'candidate' state from one - // of the states where it could have been attached. - if (from !== 'inactive' && from !== 'highlighted') { - this.textEditor.detach(this.element.get(0), this.textFormat); - } - break; - - case 'highlighted': - break; - - case 'activating': - // When transformation filters have been been applied to the processed - // text of this field, then we'll need to load a re-processed version of - // it without the transformation filters. - if (this.textFormatHasTransformations) { - var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property); - this._getUntransformedText(propertyID, this.element, function (untransformedText) { - that.element.html(untransformedText); - that.options.activated(); - }); - } - // When no transformation filters have been applied: start WYSIWYG - // editing immediately! - else { - this.options.activated(); - } - break; - - case 'active': - this.textEditor.attachInlineEditor( - this.element.get(0), - this.textFormat, - this.toolbarView.getMainWysiwygToolgroupId(), - this.toolbarView.getFloatedWysiwygToolgroupId() - ); - // Set the state to 'changed' whenever the content has changed. - this.textEditor.onChange(this.element.get(0), function (html) { - that.options.changed(html); - }); - break; - - case 'changed': - break; - - case 'saving': - break; - - case 'saved': - break; - - case 'invalid': - break; - } - }, - - /** - * Loads untransformed text for a given property. - * - * More accurately: it re-processes processed text to exclude transformation - * filters used by the text format. - * - * @param String propertyID - * A property ID that uniquely identifies the given property. - * @param jQuery $editorElement - * The property's PropertyEditor DOM element. - * @param Function callback - * A callback function that will receive the untransformed text. - * - * @see \Drupal\editor\Ajax\GetUntransformedTextCommand - */ - _getUntransformedText: function (propertyID, $editorElement, callback) { - // Create a Drupal.ajax instance to load the form. - Drupal.ajax[propertyID] = new Drupal.ajax(propertyID, $editorElement, { - url: Drupal.edit.util.buildUrl(propertyID, drupalSettings.editor.getUntransformedTextURL), - event: 'editor-internal.editor', - submit: { nocssjs : true }, - progress: { type : null } // No progress indicator. - }); - // Implement a scoped editorGetUntransformedText AJAX command: calls the - // callback. - Drupal.ajax[propertyID].commands.editorGetUntransformedText = function(ajax, response, status) { - callback(response.data); - // Delete the Drupal.ajax instance that called this very function. - delete Drupal.ajax[propertyID]; - $editorElement.off('editor-internal.editor'); - }; - // This will ensure our scoped editorGetUntransformedText AJAX command - // gets called. - $editorElement.trigger('editor-internal.editor'); - } - -}); - -})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/editor/js/editor.formattedTextEditor.js b/core/modules/editor/js/editor.formattedTextEditor.js new file mode 100644 index 0000000..8d1ace7 --- /dev/null +++ b/core/modules/editor/js/editor.formattedTextEditor.js @@ -0,0 +1,181 @@ +/** + * @file + * Text editor-based in-place editor for processed text content in Drupal. + * + * Depends on editor.module. Works with any (WYSIWYG) editor that implements the + * editor.js API, including the optional attachInlineEditor() and onChange() + * methods. + * For example, assuming that a hypothetical editor's name was "Magical Editor" + * and its editor.js API implementation lived at Drupal.editors.magical, this + * JavaScript would use: + * - Drupal.editors.magical.attachInlineEditor() + */ +(function ($, Drupal, drupalSettings) { + +"use strict"; + +Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ + + textFormat: null, + textFormatHasTransformations: null, + textEditor: null, + $textElement: null, + + /** + * {@inheritdoc} + */ + initialize: function (options) { + Drupal.edit.EditorView.prototype.initialize.call(this, options); + + var editID = this.model.get('editID'); + var metadata = Drupal.edit.Metadata.get(editID, 'custom'); + this.textFormat = drupalSettings.editor.formats[metadata.format]; + this.textFormatHasTransformations = metadata.formatHasTransformations; + this.textEditor = Drupal.editors[this.textFormat.editor]; + + // Store the actual value of this field. We'll need this to restore the + // original value when the user discards his modifications. + this.$textElement = this.$el.find('.field-item:first'); + this.model.set('originalValue', this.$textElement.html()); + }, + + /** + * {@inheritdoc} + */ + getEditedElement: function () { + return this.$textElement; + }, + + /** + * {@inheritdoc} + */ + stateChange: function (model, state) { + var that = this; + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + + case 'candidate': + // Detach the text editor when entering the 'candidate' state from one + // of the states where it could have been attached. + if (from !== 'inactive' && from !== 'highlighted') { + this.textEditor.detach(this.$textElement.get(0), this.textFormat); + } + if (from === 'invalid') { + this.removeValidationErrors(); + } + break; + + case 'highlighted': + break; + + case 'activating': + // When transformation filters have been been applied to the processed + // text of this field, then we'll need to load a re-processed version of + // it without the transformation filters. + if (this.textFormatHasTransformations) { + var editID = this.model.get('editID'); + this._getUntransformedText(editID, this.$el, function (untransformedText) { + that.$textElement.html(untransformedText); + that.model.set('state', 'active'); + }); + } + // When no transformation filters have been applied: start WYSIWYG + // editing immediately! + else { + // As soon as the current state change has propagated, apply this one. + // @see http://jsfiddle.net/5MVzp/2/ vs. http://jsfiddle.net/5MVzp/3/ + _.defer(function() { + that.model.set('state', 'active'); + }); + } + break; + + case 'active': + this.textEditor.attachInlineEditor( + this.$textElement.get(0), + this.textFormat, + this.model.get('toolbarView').getMainWysiwygToolgroupId(), + this.model.get('toolbarView').getFloatedWysiwygToolgroupId() + ); + // Set the state to 'changed' whenever the content has changed. + this.textEditor.onChange(this.$textElement.get(0), function (htmlText) { + that.model.set('currentValue', htmlText); + that.model.set('state', 'changed'); + }); + break; + + case 'changed': + break; + + case 'saving': + if (from === 'invalid') { + this.removeValidationErrors(); + } + this.save(); + break; + + case 'saved': + break; + + case 'invalid': + this.showValidationErrors(); + break; + } + }, + + /** + * {@inheritdoc} + */ + getEditUISettings: function () { + return { padding: true, unifiedToolbar: true, fullWidthToolbar: true }; + }, + + /** + * {@inheritdoc} + */ + revert: function () { + this.$textElement.html(this.model.get('originalValue')); + }, + + /** + * Loads untransformed text for a given field. + * + * More accurately: it re-processes processed text to exclude transformation + * filters used by the text format. + * + * @param String editID + * A edit ID that uniquely identifies the given field. + * @param jQuery $editorElement + * The property's PropertyEditor DOM element. + * @param Function callback + * A callback function that will receive the untransformed text. + * + * @see \Drupal\editor\Ajax\GetUntransformedTextCommand + */ + _getUntransformedText: function (editID, $editorElement, callback) { + // Create a Drupal.ajax instance to load the form. + Drupal.ajax[editID] = new Drupal.ajax(editID, $editorElement, { + url: Drupal.edit.util.buildUrl(editID, drupalSettings.editor.getUntransformedTextURL), + event: 'editor-internal.editor', + submit: { nocssjs : true }, + progress: { type : null } // No progress indicator. + }); + // Implement a scoped editorGetUntransformedText AJAX command: calls the + // callback. + Drupal.ajax[editID].commands.editorGetUntransformedText = function(ajax, response, status) { + callback(response.data); + // Delete the Drupal.ajax instance that called this very function. + delete Drupal.ajax[editID]; + $editorElement.off('editor-internal.editor'); + }; + // This will ensure our scoped editorGetUntransformedText AJAX command + // gets called. + $editorElement.trigger('editor-internal.editor'); + } + +}); + +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/editor/js/editor.js b/core/modules/editor/js/editor.js index 6483170..f07aa73 100644 --- a/core/modules/editor/js/editor.js +++ b/core/modules/editor/js/editor.js @@ -83,8 +83,12 @@ Drupal.behaviors.editor = { var $this = $(this); var activeFormatID = $this.val(); var field = behavior.findFieldForFormatSelector($this); - - Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger); + if ('activeFormatID' in settings.editor.formats) { + Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger); + } + else { + console.log('%c editor.js: The format ' + activeFormatID + ' does not have an editor.', 'background-color: red; color: white;'); + } }); }, diff --git a/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php b/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php index 81c23d7..08cd210 100644 --- a/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php +++ b/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php @@ -14,7 +14,7 @@ use Drupal\field\Plugin\Core\Entity\FieldInstance; /** - * Defines the "editor" Create.js PropertyEditor widget. + * Defines the formatted text editor. * * @Plugin( * id = "editor", @@ -90,8 +90,8 @@ public function getAttachments() { // Get the attachments for all text editors that the user might use. $attachments = $manager->getAttachments($formats); - // Also include editor.module's Create.js PropertyEditor widget. - $attachments['library'][] = array('editor', 'edit.editorWidget.editor'); + // Also include editor.module's formatted text editor. + $attachments['library'][] = array('editor', 'edit.formattedTextEditor.editor'); return $attachments; } diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php index 1dcadc8..6e8e36f 100644 --- a/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php +++ b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php @@ -21,7 +21,7 @@ class EditIntegrationTest extends EditTestBase { /** - * The manager for editor (Create.js PropertyEditor widget) plug-ins. + * The manager for editor plug-ins. * * @var \Drupal\Component\Plugin\PluginManagerInterface */ @@ -143,7 +143,7 @@ function testEditorSelection() { } /** - * Tests (custom) metadata when the "Editor" Create.js editor is used. + * Tests (custom) metadata when the formatted text editor is used. */ function testMetadata() { $this->editorManager = new EditorManager($this->container->get('container.namespaces')); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 8b8b4d0..615e5dc 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2109,20 +2109,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',