core/modules/edit/css/edit.css | 457 +++ core/modules/edit/edit.info | 6 + core/modules/edit/edit.module | 403 +++ core/modules/edit/images/attention.png | 6 + core/modules/edit/images/close.png | 78 + core/modules/edit/images/throbber.gif | 73 + core/modules/edit/includes/form.inc | 147 + core/modules/edit/includes/missing-api.inc | 43 + core/modules/edit/includes/pages.inc | 191 + core/modules/edit/js/app.js | 306 ++ core/modules/edit/js/backbone.drupalform.js | 148 + core/modules/edit/js/createjs/editable.js | 40 + .../createjs/editingWidgets/drupalalohawidget.js | 157 + .../editingWidgets/drupalcontenteditablewidget.js | 111 + .../edit/js/createjs/editingWidgets/formwidget.js | 150 + core/modules/edit/js/createjs/storage.js | 4 + core/modules/edit/js/edit.js | 41 + core/modules/edit/js/lib/create.js | 1643 +++++++++ core/modules/edit/js/lib/vie.js | 3682 ++++++++++++++++++++ core/modules/edit/js/models/edit-app-model.js | 12 + core/modules/edit/js/routers/edit-router.js | 50 + core/modules/edit/js/theme.js | 150 + core/modules/edit/js/util.js | 140 + core/modules/edit/js/viejs/SparkEditService.js | 202 ++ core/modules/edit/js/views/fielddecorator-view.js | 319 ++ core/modules/edit/js/views/menu-view.js | 38 + core/modules/edit/js/views/modal-view.js | 108 + core/modules/edit/js/views/overlay-view.js | 80 + core/modules/edit/js/views/toolbar-view.js | 443 +++ .../field/formatter/TextDefaultFormatter.php | 3 + .../Plugin/field/formatter/TextPlainFormatter.php | 3 + .../formatter/TextSummaryOrTrimmedFormatter.php | 3 + .../field/formatter/TextTrimmedFormatter.php | 3 + 33 files changed, 9240 insertions(+) diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css new file mode 100644 index 0000000..9109d62 --- /dev/null +++ b/core/modules/edit/css/edit.css @@ -0,0 +1,457 @@ +/** + * Animations. + */ +.edit-animate-invisible { + opacity: 0 !important; +} + +.edit-animate-fast { +-webkit-transition: all .2s ease; + -moz-transition: all .2s ease; + -ie-transition: all .2s ease; + -o-transition: all .2s ease; + transition: all .2s ease; +} + +.edit-animate-default { + -webkit-transition: all .4s ease; + -moz-transition: all .4s ease; + -ie-transition: all .4s ease; + -o-transition: all .4s ease; + transition: all .4s ease; +} + +.edit-animate-slow { +-webkit-transition: all .6s ease; + -moz-transition: all .6s ease; + -ie-transition: all .6s ease; + -o-transition: all .6s ease; + transition: all .6s ease; +} + +.edit-animate-delay-veryfast { + -webkit-transition-delay: .05s; +} + +.edit-animate-delay-fast { + -webkit-transition-delay: .2s; +} + +.edit-animate-delay-default { + -webkit-transition-delay: .4s; +} + +.edit-animate-delay-slow { + -webkit-transition-delay: .6s; +} + +.edit-animate-disable-width { + -webkit-transition: width 0s; +} + +.edit-animate-only-visibility { + -webkit-transition: opacity .2s ease; + -moz-transition: opacity .2s ease; + -ie-transition: opacity .2s ease; + -o-transition: opacity .2s ease; + transition: opacity .2s ease; + -webkit-transition-delay: 0s; +} + + + + +/** + * Edit's bar — inspired by core's toolbar.module & shortcut.module. + */ +#editbar, +#editbar * { + border: 0; + font-size: 100%; + line-height: inherit; + list-style: none; + margin: 0; + outline: 0; + padding: 0; + text-align: left; /* LTR */ + vertical-align: baseline; +} +#editbar { + position: relative; + background: #666; + color: #ccc; + font: normal small "Lucida Grande", Verdana, sans-serif; + margin: 0 -20px; + padding: 0 20px; + -moz-box-shadow: 0 3px 20px #000; + -webkit-box-shadow: 0 3px 20px #000; + box-shadow: 0 3px 20px #000; + z-index: 500; +} +#editbar ul { + padding: 5px 0 2px 0; + height: 28px; + line-height: 24px; + margin-left:5px; /* LTR */ +} +#editbar ul li, +#editbar ul li a { + float: left; /* LTR */ + padding: 0 5px 0 5px; + margin-right: 5px; /* LTR */ + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} +#editbar a { + padding: 5px 10px 5px 5px; + line-height: 24px; + color: #fefefe; + font-size: .846em; + text-decoration: none; +} +#editbar a:focus, +#editbar a:hover, +#editbar a.active { + color: #fff; +} +#editbar ul li a:focus, +#editbar ul li a:hover, +#editbar ul li a.active:focus { + background: #555; +} +#editbar ul li a.active:hover, +#editbar ul li a.active { + background: #000; +} + + + + +/** + * Edit mode: overlay + candidate editables + editables being edited. + * + * Note: every class is prefixed with "edit-" to prevent collisions with modules + * or themes. In IPE-specific DOM subtrees, this is not necessary. + */ + +#edit_overlay { + position: fixed; + z-index: 250; + width: 100%; + height: 100%; + background-color: rgba(255,255,255,.5); + top: 0px; /* offset for navbar, modified later */ + left: 0px; +} + +/* Editable. */ +.edit-editable { + z-index: 300; + position: relative; +} +.edit-editable:focus { + outline: none; +} +.edit-field.edit-editable, +.edit-field.edit-type-direct .edit-editable { + box-shadow: 0px 0px 1px 1px #4D9DE9; +} + +/* Highlighted (hovered) editable. */ +.edit-editable.edit-highlighted { + min-width: 200px; /* TODO: we even need them to be at least fairly wide! */ +} +.edit-field.edit-editable.edit-highlighted, +.edit-form.edit-editable.edit-highlighted, +.edit-field.edit-type-direct .edit-editable.edit-highlighted { + box-shadow: 0px 0px 1px 1px #0199FF, 0px 0px 3px 3px rgba(153, 153, 153, .5); +} +.edit-field.edit-editable.edit-highlighted.edit-validation-error, +.edit-form.edit-editable.edit-highlighted.edit-validation-error, +.edit-field.edit-type-direct .edit-editable.edit-highlighted.edit-validation-error { + box-shadow: 0px 0px 1px 1px red, 0px 0px 3px 3px rgba(153, 153, 153, .5); +} +.edit-form.edit-editable .form-item .error { + border: 1px solid #eea0a0; +} + + +/* Editing (focused) editable. */ +.edit-form.edit-editable.edit-editing, +.edit-field.edit-type-direct .edit-editable.edit-editing { + /* In the latest design, there's no special styling when editing as opposed to + just hovering. */ +} + + + + +/** + * Edit mode: modal. + */ +#edit_modal { + z-index: 350; + position: fixed; + top: 40%; + left: 40%; + box-shadow: 3px 3px 5px #333; + background-color: white; + border: 1px solid #0199FF; + font-family: 'Droid sans', 'Lucida Grande', sans-serif; +} + +#edit_modal .main { + font-size: 130%; + margin: 25px; + padding-left: 40px; + background: transparent url('../images/attention.png') no-repeat; +} + +#edit_modal .actions { + border-top: 1px solid #ddd; + padding: 3px inherit; + text-align: right; + background: #f5f5f5; +} + +/* Modal active: prevent user from interacting with toolbar & editables. */ +.edit-form-container.edit-belowoverlay, +.edit-toolbar-container.edit-belowoverlay, +.edit-validation-errors.edit-belowoverlay { + z-index: 210; +} +.edit-editable.edit-belowoverlay { + z-index: 200; +} + + + + +/** + * Edit mode: type=direct. + */ +.edit-validation-errors { + z-index: 300; + position: relative; +} + +.edit-validation-errors .messages.error { + position: absolute; + top: 6px; + left: -5px; + margin: 0; + border: none; + box-shadow: 0px 0px 1px 1px red, 0px 0px 3px 3px rgba(153, 153, 153, .5); + background-color: white; +} + + + + +/** + * Edit mode: type=form. + */ +#edit_backstage { + display: none; +} + +.edit-form { + position: absolute; + z-index: 300; + box-shadow: 0 0 30px 4px #4F4F4F; + max-width: 35em; +} + +.edit-form .placeholder { + min-height: 22px; +} + +/* Default form styling overrides. */ +.edit-form form { padding: 1em; } +.edit-form .form-item { margin: 0; } +.edit-form .form-wrapper { margin: .5em; } +.edit-form .form-actions { display: none; } +.edit-form input { max-width: 100%; } + + + + +/** + * Edit mode: toolbars + */ + +/* Trick: wrap statically positioned elements in relatively positioned element + without changing its location. This allows us to absolutely position the + toolbar. +*/ +.edit-toolbar-container, +.edit-form-container { + position: relative; + padding: 0; + border: 0; + margin: 0; + vertical-align: baseline; + z-index: 310; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.edit-toolbar-heightfaker { + height: auto; + position: absolute; + bottom: 1px; + box-shadow: 0px 0px 1px 1px #0199FF, 0px 0px 3px 3px rgba(153, 153, 153, .5); + background: #FFF; +} + +/* The toolbar; these are not necessarily visible. */ +.edit-toolbar { + position: relative; + height: 100%; + font-family: 'Droid sans', 'Lucida Grande', sans-serif; +} +.edit-toolbar-heightfaker { + clip: rect(-1000px, 1000px, auto, -1000px); /* Remove bottom box-shadow. */ +} +/* Exception: when used for a directly WYSIWYG editable field that is actively + being edited. */ +.edit-type-direct-with-wysiwyg .edit-editing .edit-toolbar-heightfaker { + width: 100%; + clip: auto; +} + + +/* The toolbar contains toolgroups; these are visible. */ +.edit-toolgroup { + float: left; /* LTR */ +} + +/* Info toolgroup. */ +.edit-toolgroup.info { + float: left; /* LTR */ + font-weight: bolder; + padding: 0 5px; + background: #FFF url('../images/throbber.gif') no-repeat -60px 60px; +} +.edit-toolgroup.info.loading { + padding-right: 35px; + background-position: 90% 50%; +} + +/* Operations toolgroup. */ +.edit-toolgroup.ops { + float: right; /* LTR */ + margin-left: 5px; +} + +.edit-toolgroup.wysiwyg-tabs { + float: right; +} +.edit-toolgroup.wysiwyg { + clear: left; + width: 100%; + padding-left: none; +} + + + +/** + * Edit mode: buttons (in both modal and toolbar). + */ +#edit_modal a, +.edit-toolbar a { + float: left; /* LTR */ + display: block; + height: 21px; + min-width: 21px; + padding: 3px 6px 3px 6px; + margin: 4px 5px 1px 0; + border: 1px solid #fff; + border-radius: 3px; + color: white; + text-decoration: none; + font-size: 13px; +} +#edit_modal a { + float: none; + display: inline-block; +} + +#edit_modal a:link, +#edit_modal a:visited, +#edit_modal a:hover, +#edit_modal a:active, +.edit-toolbar a:link, +.edit-toolbar a:visited, +.edit-toolbar a:hover, +.edit-toolbar a:active { + text-decoration: none; +} + +/* Button with icons. */ +#edit_modal a span, +.edit-toolbar a span { + width: 22px; + height: 19px; + display: block; + float: left; +} +.edit-toolbar a span.close { + background: url('../images/close.png') no-repeat 2px 2px; +} + +.edit-toolbar a span.close:hover { + /* TODO: use a different "close" image */ +} + + +.edit-toolbar a.blank-button { + color: black; +} + +#edit_modal a.blue-button, +.edit-toolbar a.blue-button { + color: white; + background-image: -webkit-linear-gradient(top, #6fc2f2 0%, #4e97c0 100%); + background-image: linear-gradient(top, #6fc2f2 0%, #4e97c0 100%); + border-radius: 5px; +} + +#edit_modal a.gray-button, +.edit-toolbar a.gray-button { + color: #666; + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #ccc 100%); + background-image: linear-gradient(top, #f5f5f5 0%, #ccc 100%); + border-radius: 5px; +} + +#edit_modal a img.gray-button.close img, .gray-button.save img, .blue-button.save img, +.edit-toolbar a img.gray-button.close img, .gray-button.save img, .blue-button.save img { + padding: 0; +} + +.gray-button img, .blue-button img, +.gray-button img, .blue-button img { + padding-right: 5px; +} + +#edit_modal a.blue-button:hover, +.edit-toolbar a.blue-button:hover, +#edit_modal a.blue-button:active, +.edit-toolbar a.blue-button:active { + border: 1px solid #55a5d3; + box-shadow: 0px 2px 1px rgba(0,0,0,0.2); +} + +#edit_modal a.gray-button:hover, +.edit-toolbar a.gray-button:hover, +#edit_modal a.gray-button:active, +.edit-toolbar a.gray-button:active { + border: 1px solid #cdcdcd; + box-shadow: 0px 2px 1px rgba(0,0,0,0.1); +} diff --git a/core/modules/edit/edit.info b/core/modules/edit/edit.info new file mode 100644 index 0000000..630c825 --- /dev/null +++ b/core/modules/edit/edit.info @@ -0,0 +1,6 @@ +name = Edit +description = In-place content editing. +package = User interface +core = 8.x + +dependencies[] = field diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module new file mode 100644 index 0000000..1d0c70c --- /dev/null +++ b/core/modules/edit/edit.module @@ -0,0 +1,403 @@ + array(TRUE), + 'access callback' => TRUE, + 'page callback' => 'edit_field_edit', + 'page arguments' => array(3, 4, 5, 6, 7), + 'file' => 'includes/pages.inc', + 'delivery callback'=> 'ajax_deliver', + 'theme callback' => 'ajax_base_page_theme', + ); + $items['admin/render-without-transformations/field/%/%/%/%/%'] = array( + // Access is controlled after we have inspected the entity, which can't + // easily happen until after the callback. + 'access arguments' => array(TRUE), + 'access callback' => TRUE, + 'page callback' => 'edit_text_field_render_without_transformation_filters', + 'page arguments' => array(3, 4, 5, 6, 7), + 'file' => 'includes/pages.inc', + 'delivery callback'=> 'ajax_deliver', + 'theme callback' => 'ajax_base_page_theme', + ); + + return $items; +} + +/** + * Implements hook_page_alter(). + */ +function edit_page_alter(&$page) { + if (path_is_admin(current_path())) { + return; + } + + $page['page_top']['edit'] = array( + 'view_edit_toggle' => array( + '#prefix' => '

' . t('In-place edit operations') . '

', + 'content' => array( + array( + '#theme' => 'menu_local_task', + '#link' => array('title' => t('View'), 'href' => current_path(), 'localized_options' => array('fragment' => 'view', 'attributes' => array('class' => array('edit_view-edit-toggle', 'edit-view')))), + '#active' => TRUE, + ), + array( + '#theme' => 'menu_local_task', + '#link' => array('title' => t('Quick edit'), 'href' => current_path(), 'localized_options' => array('fragment' => 'quick-edit', 'attributes' => array('class' => array('edit_view-edit-toggle', 'edit-edit')))), + ), + ), + '#attached' => array( + 'library' => array( + array('edit', 'edit'), + ), + ), + ), + '#post_render' => array( + 'edit_editbar_post_render', + ), + ); +} + +/** + * Post-render function to remove the editbar if nothing editable is present. + */ +function edit_editbar_post_render($html) { + global $editbar; + return ($editbar !== TRUE) ? '' : $html; +} + +/** + * Implements hook_library(). + */ +function edit_library_info() { + $path = drupal_get_path('module', 'edit'); + $libraries['edit'] = array( + 'title' => 'Edit: in-place editing', + 'website' => 'http://drupal.org/project/edit', + 'version' => VERSION, + 'js' => array( + $path . '/js/edit.js' => array( + 'defer' => TRUE, + ), + $path . '/js/util.js' => array( + 'defer' => TRUE, + ), + $path . '/js/theme.js' => array( + 'defer' => TRUE, + ), + // Basic settings. + array( + 'data' => array('edit' => array( + 'fieldFormURL' => url('admin/edit/field/!entity_type/!id/!field_name/!langcode/!view_mode'), + 'rerenderProcessedTextURL' => url('admin/render-without-transformations/field/!entity_type/!id/!field_name/!langcode/!view_mode'), + 'context' => 'body', + )), + 'type' => 'setting', + ), + ), + 'css' => array( + $path . '/css/edit.css', + ), + 'dependencies' => array( + array('system', 'jquery.form'), + array('system', 'drupal.form'), + array('system', 'drupal.ajax'), + array('system', 'backbone'), + array('edit', 'edit.createjs'), + ), + ); + + $libraries['edit.createjs'] = array( + 'title' => 'CreateJS and deps', + 'website' => 'http://createjs.org', + 'version' => NULL, + 'js' => array( + $path . '/js/lib/create.js' => array('defer' => TRUE), + $path . '/js/viejs/SparkEditService.js' => array('defer' => TRUE), + $path . '/js/createjs/editable.js' => array('defer' => TRUE), + $path . '/js/createjs/storage.js' => array('defer' => TRUE), + $path . '/js/createjs/editingWidgets/formwidget.js' => array('defer' => TRUE), + $path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => array('defer' => TRUE), + $path . '/js/createjs/editingWidgets/drupalalohawidget.js' => array('defer' => TRUE), + $path . '/js/views/menu-view.js' => array('defer' => TRUE), + $path . '/js/views/overlay-view.js' => array('defer' => TRUE), + $path . '/js/views/toolbar-view.js' => array('defer' => TRUE), + $path . '/js/views/fielddecorator-view.js' => array('defer' => TRUE), + $path . '/js/views/modal-view.js' => array('defer' => TRUE), + $path . '/js/routers/edit-router.js' => array('defer' => TRUE), + $path . '/js/models/edit-app-model.js' => array('defer' => TRUE), + $path . '/js/backbone.drupalform.js' => array('defer' => TRUE), + $path . '/js/app.js' => array('defer' => TRUE), + ), + 'dependencies' => array( + array('system', 'jquery.ui.widget'), + array('system', 'backbone'), + array('edit', 'edit.vie'), + ), + ); + + $libraries['edit.vie'] = array( + 'title' => 'Vienna IKS Editables', + 'website' => 'http://wiki.iks-project.eu/index.php/VIE', + 'version' => '2.0', + 'js' => array( + $path . '/js/lib/vie.js' => array('defer' => TRUE), + ), + 'dependencies' => array( + array('system', 'backbone'), + ), + ); + + // Only add dependencies on the WYSIWYG editor when it's actually available. + if (count(module_implements('edit_wysiwyg_info'))) { + $libraries['edit']['dependencies'][] = _edit_get_wysiwyg_info('javascript library'); + } + + return $libraries; +} + +/** + * Implements hook_field_attach_view_alter(). + */ +function edit_field_attach_view_alter(&$output, $context) { + // Special case for this special mode. + if ($context['display'] == 'edit-render-without-transformation-filters') { + $children = element_children($output); + $field = reset($children); + $langcode = $output[$field]['#language']; + foreach (array_keys($output[$field]['#items']) as $item) { + $text = $output[$field]['#items'][$item]['value']; + $format_id = $output[$field]['#items'][$item]['format']; + $untransformed = check_markup($text, $format_id, $langcode, FALSE, array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE)); + $output[$field][$item]['#markup'] = $untransformed; + } + } +} + +/** + * Implements hook_preprocess_HOOK() for field.tpl.php. + */ +function edit_preprocess_field(&$variables) { + $entity = $variables['element']['#object']; + $name = $variables['element']['#field_name']; + $langcode = $variables['element']['#language']; + $view_mode = $variables['element']['#view_mode']; + $formatter_type = $variables['element']['#formatter']; + $field = $entity->{$name}[$langcode]; + $instance_info = field_info_instance($entity->entityType(), $name, $entity->bundle()); + + $entity_access = edit_entity_access('update', $entity->entityType(), $entity); + $field_access = field_access('edit', $name, $entity->entityType(), $entity); + $editability = _edit_analyze_field_editability($field, $instance_info, $formatter_type); + if ($entity_access && $field_access && $editability != 'disabled') { + global $editbar; + $editbar = TRUE; + + // Mark this field as editable and provide metadata through data- attributes. + $variables['attributes']['data-edit-field-label'] = $instance_info->definition['label']; + $variables['attributes']['data-edit-id'] = $entity->entityType() . ':' . $entity->id() . ':' . $name . ':' . $langcode . ':' . $view_mode; + $variables['attributes']['class'][] = 'edit-field'; + $variables['attributes']['class'][] = 'edit-allowed'; + $variables['attributes']['class'][] = 'edit-type-' . $editability; + if ($editability == 'direct-with-wysiwyg') { + $variables['attributes']['class'][] = 'edit-type-direct'; + $format_id = $entity->{$name}[$langcode][0]['format']; + _edit_preprocess_field_wysiwyg($variables, $format_id); + } + } +} + +/** + * Sets attributes on a field that have 'direct-with-wysiwyg' editability. + * + * @param array $variables + * An associative array containing: the key 'attributes'. See the + * theme_field() function for information about these variables. + * @param string $format_id + * A text format id. + * + * @see theme_field() + */ +function _edit_preprocess_field_wysiwyg(&$variables, $format_id) { + // Let the WYSIWYG editor know the text format. + $variables['attributes']['data-edit-text-format'] = $format_id; + + // Ensure the WYSIWYG editor has the necessary text format related + // metadata. + $settings_callback = _edit_get_wysiwyg_info('javascript settings callback'); + $settings_callback(); + + // Let the JavaScript logic know whether transformation filters are used + // in this format, so it can decide whether to re-render the text or not. + $filter_types = filter_get_filter_types_by_format($format_id); + $transformation_filter_types = array( + FILTER_TYPE_TRANSFORM_REVERSIBLE, + FILTER_TYPE_TRANSFORM_IRREVERSIBLE + ); + if (count(array_intersect($transformation_filter_types, $filter_types))) { + $variables['attributes']['class'][] = 'edit-text-with-transformation-filters'; + } + else { + $variables['attributes']['class'][] = 'edit-text-without-transformation-filters'; + } +} + +/** + * Determines editability given a field, its instance info and its formatter. + * + * @param array $field + * The field's field array. + * @param FieldInstance $instance_info + * The field's instance info. + * @param string $formatter_type + * The field's formatter type name. + * + * @return string + * The editability: 'disabled', 'form', 'direct' or 'direct-with-wysiwyg'. + */ +function _edit_analyze_field_editability($field, FieldInstance $instance_info, $formatter_type) { + $name = $instance_info->definition['field_name']; + + // If the formatter doesn't contain the edit property, default it to 'form' + // editability, which should always work. + $formatter_info = field_info_formatter_types($formatter_type); + if (empty($formatter_info['edit']['editability'])) { + $formatter_info['edit']['editability'] = 'form'; + } + + $editability = $formatter_info['edit']['editability']; + + // If editing is explicitly disabled for this field, return early to avoid + // any further processing. + if ($editability == 'disabled') { + return; + } + + // If directly editable, check the cardinality. If the cardinality is greater + // than 1, use a form to edit the field. + if ($editability == 'direct') { + $info = field_info_field($name); + if ($info['cardinality'] != 1) { + $editability = 'form'; + } + } + + // If still directly editable, check whether "regular" direct editing (almost + // bare contentEditable) editing should be used or WYSIWYG-based direct + // editing should be used. In the latter case + if ($editability == 'direct') { + // If this field is configured to not use text processing; it is plain text + // "regular" direct editing should be used, which is already set. + // On the other hand, if it is configured to use text processing; then we + // must check whether 'direct-with-wysiwyg' or 'form' editability should be + // used. + if (!empty($instance_info->definition['settings']['text_processing'])) { + $format_id = $field[0]['format']; + $editability = _edit_wysiwyg_analyze_field_editability($format_id); + } + } + + return $editability; +} + +/** + * Determines editability given a directly editable field with text processing. + * + * Given a text field (with cardinality 1) that defaults to 'direct' editability + * and has text processing enabled, check whether the text format allows it to + * use WYSIWYG-powered direct editing or whether 'form' based editing needs to + * be used. + * + * @param string|NULL $format_id + * The field's current text format. + * + * @return string + * The editability: 'direct-with-wysiwyg' or 'form'. + */ +function _edit_wysiwyg_analyze_field_editability($format_id = NULL) { + // If no format is assigned yet, (e.g. when the field is still empty (NULL)), + // then provide form-based editing, so that the user is able to select a text + // format. (Direct editing doesn't allow the user to change the format.) + if (empty($format_id)) { + return 'form'; + } + // If no WYSIWYG editor is available, then fall back to form-based editing. + elseif (count(_edit_get_wysiwyg_info()) == 0) { + return 'form'; + } + // If the WYSIWYG editor is not compatible with the current format, then fall + // back to form-based editing. + else { + $compatibility_callback = _edit_get_wysiwyg_info('format compatibility callback'); + if (!$compatibility_callback($format_id)) { + return 'form'; + } + } + + return 'direct-with-wysiwyg'; +} + +/** + * Retrieves a list of all available WYSIWYG integration for Edit. Only the + * first is actually used. + * + * @todo Convert to the plug-in system! + * + * @param string $key + * The key to get a value for. + * + * @see hook_edit_wysiwyg_info() + * @see hook_edit_wysiwyg_info_alter() + */ +function _edit_get_wysiwyg_info($key = NULL) { + $edit_wysiwyg_info = &drupal_static(__FUNCTION__, array()); + + if (empty($edit_wysiwyg_info)) { + $cache = cache()->get('edit_wysiwyg_info'); + if ($cache === FALSE) { + // Rebuild the cache and save it. + $edit_wysiwyg_info = module_invoke_all('edit_wysiwyg_info'); + drupal_alter('edit_wysiwyg_info', $edit_wysiwyg_info); + } + else { + $edit_wysiwyg_info = $cache->data; + } + } + + if (isset($key)) { + $modules = array_keys($edit_wysiwyg_info); + $first = $modules[0]; + + return $edit_wysiwyg_info[$first][$key]; + } + else { + return $edit_wysiwyg_info; + } +} diff --git a/core/modules/edit/images/attention.png b/core/modules/edit/images/attention.png new file mode 100644 index 0000000..3c833c9 --- /dev/null +++ b/core/modules/edit/images/attention.png @@ -0,0 +1,6 @@ +PNG + + IHDR ǍtEXtSoftwareAdobe ImageReadyqe<$iTXtXML:com.adobe.xmp ! IDATxڼV]HQ.aRcEPP$Rڏd`ѣ[o `JH$f{:n;θ3zg{{ 2q(^uYnx2K_B3Y_ us۝Uޖ ai/0p6~s_LyXڜ~=F0p͏dV7>fP,;Ӊ^Nw-ȁ`ޔԜiW1PǫC;5^q wp}C&Yk{Ao[9܄Àu-`pP:iю _tU"zAL)DZ0pU4 "c2Jo^h'{ Rrt6R <H; >K3P { ]<Fے"%آv#LB%#䶁eބSj\Y zTV|+lIUa % +,4f {euU#nKΓQv +%lkyN/0 Ӭk%t f:kӶvioI'eT LNEm1b %q(B+`IIV#k,$%$=6㢝2'"ܼCmw{8{%ބ݆?u*q XLIENDB` \ No newline at end of file diff --git a/core/modules/edit/images/close.png b/core/modules/edit/images/close.png new file mode 100644 index 0000000..2f5d665 --- /dev/null +++ b/core/modules/edit/images/close.png @@ -0,0 +1,78 @@ +PNG + + IHDRa +iTXtXML:com.adobe.xmp + + + + + + Creative Commons Attribution-NonCommercial license + + + + + Gentleface custom toolbar icons design + + + + + Wireframe mono toolbar icons + + + + + custom icon design + toolbar icons + custom icons + interface design + ui design + gui design + taskbar icons + + + + + Creative Commons Attribution-NonCommercial license + + + + + + + + + + + + + + + + + + + + + +KKtEXtSoftwareAdobe ImageReadyqe<..c2j:Em ꈠJͳ@qk$ `өltDkd2a.ÑiBrvxݗqߓFd4/ ^A/`0`޲ 2S^`K&FSIENDB` \ No newline at end of file diff --git a/core/modules/edit/images/throbber.gif b/core/modules/edit/images/throbber.gif new file mode 100644 index 0000000..58f4a42 --- /dev/null +++ b/core/modules/edit/images/throbber.gif @@ -0,0 +1,73 @@ +GIF89a  !!!"""######$$$%%%%%%&&&&&&&&&''''''''''''''''''''''''(((((()))******,,,...000222444555666888999:::;;;<<<===???AAACCCEEEFFFHHHJJJKKKLLLLLLMMMNNNOOOOOOPPPPPPQQQRRRRRRRRRSSSSSSSSSTTTTTTTTTTTTUUUUUUVVVXXXYYYZZZ\\\\\\]]]]]]^^^^^^______``````bbbdddfffggghhhiiijjjlllnnnooovvv{{{! NETSCAPE2.0! +, 8P@{%#\ -"V !׾AWhc}(qR2@( JK>^ WK>$T`4U՚h&d`pʘNlxeIZ-(+8jZ+T5r.RI>x_IecʰF&Kr*\5}¤dW05U/OT鐒}Պ%+>Kb')E,FՔVS@! +, + + + + + +  """"""###$$$&&&(((***,,,...111222333444444555666666777888888999::::::;;;<<<<<<===>>>>>>??????@@@@@@@@@AAABBBBBBCCCCCCDDDDDDEEEEEEGGGHHHJJJLLLOOORRRUUUWWWXXXZZZ[[[]]]______``````aaaaaaaaaaaabbbbbbbbbccccccdddfffggghhhhhhkkkmmmqqqvvvzzz||| 8pHQ"Ȱ(]!P#ҏB aGKc[E rcHx}hR`G?YTE Rl%F Sz"*№/T.ZΟR49EUY.]%L+Es4UT%O)!(mUSoZV+R"B T˖8qtYį""՘q[uE RuI*_H=|uJGJ\ku QZt 6ՔJt0* D%4#L,E4LqJ! +, + + + !!!"""""""""######$$$$$$%%%&&&&&&''''''((()))***+++,,,---...000111222333333333444555666777888888999:::;;;<<<<<<===???@@@BBBCCCEEEFFFHHHHHHHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQQQQRRRSSSSSSTTTTTTUUUUUUVVVVVVWWWWWWXXXYYY[[[\\\___aaacccdddfffhhhiiikkkmmmoooqqqssstttuuuwwwxxxzzz{{{|||}}}~~~ 8PM]#Yb5#0I 3 پJ3죅%u sc}8 R2S{fV %c>d\ 'LѝNmZ%K/ZH0@(L&PD`Zr0x[2i8 #8'Qu3z.5X/YLI.x˙@tSĴ$:帱,yD)Dqjt PJDJS&Q"I(^nj52Y・S+cNEkUVDE) +d'K'd0JS5mn/>MXU+-Q, tJ&4uK2!# {VS@! +,  !!!"""###$$$%%%&&&&&&'''((()))***+++,,,...000333666777888888999999999:::::::::::::::;;;;;;;;;<<<===>>>???@@@AAACCCDDDEEEFFFHHHJJJLLLMMMOOOPPPRRRSSSUUUXXXYYYZZZ[[[\\\^^^^^^___`````````aaabbbcccccceeefffgggggghhhiiiiiijjjjjjkkklllnnnoooppprrrsssuuuwwwzzz||| 8Tk#fWq$SG ֲD +*<"#QƁ8]cHE?:),K}bB٬XDVU԰+PefH# ('P\ &0רJF=F" 2kM)>dV*Tkwe'>>>>>??????@@@@@@AAABBBCCCDDDFFFHHHJJJLLLMMMNNNOOOOOOPPPQQQRRRRRRSSSTTTTTTUUUWWWYYY[[[]]]^^^___```aaabbbcccdddeeegggiiilllooorrrvvvxxx{{{ 8pעU{=#0.h]$U!T ښ$ +"+A*@d('8$OZ#'վhL)mV@ERT+\5hJQ4Yd6, C I4c %Tbv>U7*c[@YM"ifeEVȬ`DJlY;8Q=HfR AI3g DT뚔ɕ3hD*ثHIca5 Rj!4ɕ0־Yda5TD 7a@HQDc4 +T&e! +, + + +  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666666777888::::::;;;<<<>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUUUUVVVWWWYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddfffmmmsssvvvyyy||| 8Z\M4h mAR% +b$$>$NZ碴^B% >gVGT}>A:M_!SM@'& c@d*qJk%]3}ɜoÐT2U-ܘÊG2< g5~] Uf$)2<8#R3s!1&@Ir3 + )還 3h@ +<Idhؾhc RjрN4._&[FU pB-") 0 f4 T&! +, + + +  !!!"""###$$$%%%&&&'''((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIIIIKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkkkkkmmmnnnooopppuuuxxx 8P$X5#Yea +%Ж&W 2i!,? dUƄa2 IN +>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrr{{{ 8PX5#Yec %Ж&W 2ia,? dhUƄa2 IN +>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJJJJLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrzzz 8PX5#Yec %Ж&W 2ia,? dhUƄa2 IN +>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKKKKMMMNNNOOOPPPQQQRRRRRRSSSTTTUUUWWWXXXYYYYYYZZZ[[[\\\^^^______aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnoooqqqrrrsss 8PX5#Yec +%W 2ia,?i2*cBH0~J$a&CeBx,gZ&?x˓ePȃZyEV#yϢ C`ҫjTXL]IcSXu2pLY rX, +/\1OإF +0=>^^R§->>>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMOOOPPPQQQRRRRRRSSSTTTUUUWWWXXXYYYYYYZZZ[[[\\\^^^___```aaaaaacccdddeeefffggghhhiiilllooosssxxx||| 8$Z5#-fi +%VX Bi!A ˄UƄa2UIN +< +Q^,u9BZ3LH2U XL̎Jyk*! h/dž"}z(PN*Z8pU<0TR2EW-S Ԡ/Q@ Z2iϢ%՝Q2]r*aPBf&0|"ȫ+f&!$l_S*'ZzVsEEn~o9@ իT>c-\&8zrz4"&J J!n1sKP2%@}%S@! +, + + + + + +  !!!"""######$$$%%%%%%%%%&&&'''(((***+++---///111222444444555666777888999:::;;;<<<<<<<<<======>>>???@@@AAABBBCCCDDDFFFFFFFFFGGGGGGHHHHHHHHHIIIJJJJJJLLLMMMOOOPPPQQQRRRSSSTTTUUUVVVXXXYYY[[[]]]^^^```aaacccdddfffgggjjjmmmppprrrtttuuuvvvxxx{{{}}} 8֤Uy#P#\W%$PVT Vj`׾U:iѩIc}MR 2: p٩Erl.?{ QtmdLXHd3eAADPS%m2&p$Ī QT  +BADJTMV)^ukћ57 +eYp|gN=iÇuժ)ѫ.9*Es1OxD lӢDG<| #xk)c&-䱔:p˴I׫QBJ!ʴ,}HJqZUiQ)r}MJTj_|0G52Ms&rX* I"'KQ5CGrX@; \ No newline at end of file diff --git a/core/modules/edit/includes/form.inc b/core/modules/edit/includes/form.inc new file mode 100644 index 0000000..573f162 --- /dev/null +++ b/core/modules/edit/includes/form.inc @@ -0,0 +1,147 @@ + $form_state['field_name']); + field_attach_form($entity->entityType(), $entity, $form, $form_state, $langcode, $options); + + $form['#validate'][] = 'edit_field_form_validate'; + // @todo Verify that this is indeed not necessary anymore, see edit_field_form_validate(). + // $form['#submit'][] = ''; + + // Add revisions form items if necessary. + // @todo We may be able to get rid of this when http://drupal.org/node/1678002 is solved. + list($use_revisions, $control_revisions) = edit_entity_allows_revisions($entity->entityType(), $entity->bundle(), $entity); + if ($use_revisions) { + $form_state['use revisions'] = TRUE; + $form['revision_information'] = array( + '#weight' => 11, + ); + + $form['revision_information']['revision'] = array( + '#type' => 'checkbox', + '#title' => t('Create new revision'), + '#default_value' => $entity->revision, + '#id' => 'edit-revision', + '#access' => $control_revisions, + ); + + if ($control_revisions || $entity->revision) { + $form['revision_information']['log'] = array( + '#type' => 'textarea', + '#title' => t('Log message'), + '#description' => t('Provide an explanation of the changes you are making. This will help other authors understand your motivations.'), + '#default_value' => $entity->log, + ); + + if ($control_revisions) { + $form['revision_information']['log']['#dependency'] = array('edit-revision' => array(1)); + } + } + $form['#submit'][] = 'edit_field_form_revision_submit'; + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + // Simplify the form. + _simplify_edit_field_edit_form($form); + + return $form; +} + +/** + * Helper function to simplify the field edit form for in-place editing. + */ +function _simplify_edit_field_edit_form(&$form) { + $elements = element_children($form); + + // Required internal form properties. + $internal_elements = array('actions', 'form_build_id', 'form_token', 'form_id'); + + // Calculate the remaining form elements. + $remaining_elements = array_diff($elements, $internal_elements); + + // Only simplify the form if there is a single element remaining. + if (count($remaining_elements) === 1) { + $element = $remaining_elements[0]; + + if ($form[$element]['#type'] == 'container') { + $language = $form[$element]['#language']; + $children = element_children($form[$element][$language]); + + // Certain fields require different processing depending on the form + // structure. + if (count($children) == 0) { + // Checkbox elements don't have a title. + if ($form[$element][$language]['#type'] != 'checkbox') { + $form[$element][$language]['#title_display'] = 'invisible'; + } + } + elseif (count($children) == 1) { + $form[$element][$language][0]['value']['#title_display'] = 'invisible'; + + // UX improvement: make the number of rows of textarea form elements + // fit the content. (i.e. no wads of whitespace) + if (isset($form[$element][$language][0]['value']['#type']) + && $form[$element][$language][0]['value']['#type'] == 'textarea') + { + $lines = count(explode("\n", $form[$element][$language][0]['value']['#default_value'])); + $form[$element][$language][0]['value']['#rows'] = $lines + 1; + } + } + } + + // Handle pseudo-fields that are language-independent, such as title, + // author, and creation date. + elseif (empty($form[$element]['#language'])) { + $form[$element]['#title_display'] = 'invisible'; + } + } + + // Make it easy for the JavaScript to identify the submit button. + $form['actions']['submit']['#attributes'] = array('class' => array('edit-form-submit')); +} + +/** + * Validate field editing form. + */ +function edit_field_form_validate($form, &$form_state) { + $entity = $form_state['entity']; + $options = array('field_name' => $form_state['field_name']); + + // 'submit' in D8 is for "building the entity object", not for actual + // submission. It appears though that if there were no validation errors, it + // is submitted automatically. + field_attach_submit($entity->entityType(), $entity, $form, $form_state, $options); + + // Validation. + field_attach_form_validate($entity->entityType(), $entity, $form, $form_state, $options); +} + +/** + * Submit callback that handles entity revisioning. + */ +function edit_field_form_revision_submit($form, &$form_state) { + $entity = $form_state['entity']; + if (!empty($form_state['use revisions'])) { + $entity->revision = $form_state['values']['revision']; + $entity->log = $form_state['values']['log']; + } +} diff --git a/core/modules/edit/includes/missing-api.inc b/core/modules/edit/includes/missing-api.inc new file mode 100644 index 0000000..1b2f08a --- /dev/null +++ b/core/modules/edit/includes/missing-api.inc @@ -0,0 +1,43 @@ +revision = $retval[0]; + $entity->log = ''; + return $retval; +} + +/** + * @} End of "ingroup Missing in Entity API.". + */ diff --git a/core/modules/edit/includes/pages.inc b/core/modules/edit/includes/pages.inc new file mode 100644 index 0000000..8fc1477 --- /dev/null +++ b/core/modules/edit/includes/pages.inc @@ -0,0 +1,191 @@ +bundle()); + if (empty($field_instance)) { + throw new NotFoundHttpException(); + } + + $form_state = array( + 'entity' => $entity, + 'field_name' => $field_name, + 'langcode' => $langcode, + 'no_redirect' => TRUE, + 'build_info' => array('args' => array()), + ); + $commands = array(); + form_load_include($form_state, 'inc', 'edit', 'includes/form'); + $form = drupal_build_form('edit_field_form', $form_state); + + $id = "$entity_type:$entity_id:$field_name:$langcode:$view_mode"; + if (!empty($form_state['executed'])) { + $form_state['entity']->save(); + + $options = array('field_name' => $field_name); + // Reload the entity. This is necessary for some fields; otherwise we'd + // render the field without the updated values. + $entity = entity_load($entity_type, $entity_id, TRUE); + field_attach_prepare_view($entity->entityType(), array($entity->id() => $entity), $view_mode, $langcode, $options); + $output = field_attach_view($entity->entityType(), $entity, $view_mode, $langcode, $options); + + $commands[] = array( + 'command' => 'edit_field_form_saved', + 'id' => $id, + 'data' => drupal_render($output), + ); + } + else { + $commands[] = array( + 'command' => 'edit_field_form', + 'id' => $id, + 'data' => drupal_render($form), + ); + + $errors = form_get_errors(); + if (count($errors)) { + $commands[] = array( + 'command' => 'edit_field_form_validation_errors', + 'id' => $id, + 'data' => theme('status_messages'), + ); + } + } + + // When working with a hidden form, we don't want any CSS or JS to be loaded. + if (isset($_POST['nocssjs']) && $_POST['nocssjs'] === 'true') { + drupal_static_reset('drupal_add_css'); + drupal_static_reset('drupal_add_js'); + } + + return array('#type' => 'ajax', '#commands' => $commands); +} + +/** + * Page callback: render a processed text field without transformation filters. + * + * @param string $entity_type + * The entity type of the entity of which a processed text field is being + * rerendered. + * @param int $entity_id + * The entity ID of the entity of which a processed text field is being + * rerendered. + * @param string $field_name + * The name of the (processed text) field that that is being rerendered + * @param string $langcode + * The name of the language for which the processed text field is being + * rererendered. + * @param string $view_mode + * The view mode the processed text field should be rerendered in. + * @return array + * A render array. + */ +function edit_text_field_render_without_transformation_filters($entity_type, $entity_id, $field_name, $langcode, $view_mode) { + // Ensure the entity type is valid. + if (empty($entity_type)) { + throw new NotFoundHttpException(); + } + + $entity_info = entity_get_info($entity_type); + if (!$entity_info) { + throw new NotFoundHttpException(); + } + + $entities = entity_load_multiple($entity_type, array($entity_id)); + if (!$entities) { + throw new NotFoundHttpException(); + } + + $entity = reset($entities); + if (!$entity) { + throw new NotFoundHttpException(); + } + + // Ensure a valid language code is set. + $langcode = field_valid_language($langcode); + + // Ensure access to update this particular entity is granted. + if (!edit_entity_access('update', $entity_type, $entity)) { + throw new AccessDeniedHttpException(); + } + + // Ensure access to update this particular field is granted. + if (!field_access('edit', $field_name, $entity_type, $entity)) { + throw new AccessDeniedHttpException(); + } + + $field_instance = field_info_instance($entity_type, $field_name, $entity->bundle()); + if (empty($field_instance)) { + throw new NotFoundHttpException(); + } + + $commands = array(); + + // Render the field in our custom display mode; retrieve the re-rendered + // markup, this is what we're after. + $field_output = field_view_field($entity_type, $entity, $field_name, 'edit-render-without-transformation-filters'); + $output = $field_output[0]['#markup']; + + $commands[] = array( + 'command' => 'edit_field_rendered_without_transformation_filters', + 'id' => "$entity_type:$entity_id:$field_name:$langcode:$view_mode", + 'data' => $output, + ); + + return array('#type' => 'ajax', '#commands' => $commands); +} diff --git a/core/modules/edit/js/app.js b/core/modules/edit/js/app.js new file mode 100644 index 0000000..49aab71 --- /dev/null +++ b/core/modules/edit/js/app.js @@ -0,0 +1,306 @@ +(function ($, undefined) { + Drupal.edit = Drupal.edit || {}; + Drupal.edit.EditAppView = Backbone.View.extend({ + vie: null, + domService: null, + + // Configuration for state handling. + states: [], + activeEditorStates: [], + singleEditorStates: [], + + // State. + $entityElements: [], + + /** + * 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.SparkEditService()); + 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); + + // Use Create's Storage widget. + this.$el.createStorage({ + vie: this.vie, + editableNs: 'createeditable' + }); + + // Instantiate an EditableEntity widget for each property. + var that = this; + this.$entityElements = this.domService.findSubjectElements().each(function() { + $(this).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); + } + }); + }); + + // Instantiate OverlayView + var overlayView = new Drupal.edit.views.OverlayView({ + model: this.model + }); + + // Instantiate MenuView + var editMenuView = new Drupal.edit.views.MenuView({ + el: this.el, + model: this.model + }); + + // When view/edit mode is toggled in the menu, update the editor widgets. + this.model.on('change:isViewing', this.appStateChange); + }, + + /** + * Sets the state of PropertyEditor widgets when edit mode begins or ends. + * + * Should be called whenever EditAppModel's "isViewing" changes. + */ + appStateChange: function() { + // @todo: we're currently setting the state on EditableEntity widgets + // instead of PropertyEditor widgets, because of + // https://github.com/bergie/create/issues/140 + var newState = (this.model.get('isViewing')) ? 'inactive' : 'candidate'; + this.$entityElements.each(function() { + $(this).createEditable('setState', newState); + }); + }, + + /** + * 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 (this.model.get('isViewing')) { + if (to !== 'inactive') { + accept = false; + } + } + // 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', 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 get rid of this once https://github.com/bergie/create/issues/133 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') { + this.model.set('activeEditor', null); + } + + // Propagate the state change to the decoration and toolbar views. + // @todo enable this once https://github.com/bergie/create/issues/133 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.FieldDecorationView({ + el: editor.element, + editor: editor, + toolbarId: editor.toolbarView.getId() + }); + + // @todo get rid of this once https://github.com/bergie/create/issues/133 is solved. + editor.options.widget.element.bind('createeditablestatechange', function(event, data) { + editor.decorationView.stateChange(data.previous, data.current); + editor.toolbarView.stateChange(data.previous, data.current); + }); + } + }); +})(jQuery); diff --git a/core/modules/edit/js/backbone.drupalform.js b/core/modules/edit/js/backbone.drupalform.js new file mode 100644 index 0000000..67facaf --- /dev/null +++ b/core/modules/edit/js/backbone.drupalform.js @@ -0,0 +1,148 @@ +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 + * + * @todo: HTTP status handling. + */ +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.edit_field_form_saved = function(ajax, response, status) { + Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); + + // Call Backbone.sync's success callback with the rerendered field. + var changedAttributes = {}; + changedAttributes[predicate] = '@todo: JSON-LD representation N/A yet.'; + changedAttributes[predicate + '/rendered'] = response.data; + options.success(changedAttributes); + }; + + // Unsuccessfully saved; validation errors. + Drupal.ajax[base].commands.edit_field_form_validation_errors = 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.edit_field_form = 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 + * + * @todo: HTTP status handling. + */ +Backbone.syncDirect = function(method, model, options) { + if (method === 'update') { + var fillAndSubmitForm = function(value) { + jQuery('#edit_backstage form') + // Fill in the value in any that isn't hidden or a submit button. + .find(':input[type!="hidden"][type!="submit"]:not(select)').val(value).end() + // Submit the 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').attr('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.edit_field_form_saved = function (ajax, response, status) { + Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); + jQuery('#edit_backstage form').remove(); + + options.success(); + }; + + // Unsuccessfully saved; validation errors. + Drupal.ajax[base].commands.edit_field_form_validation_errors = 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. 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.edit_field_form = function(ajax, response, status) { + // no-op + }; + + fillAndSubmitForm(value); + }); + } + else { + fillAndSubmitForm(value); + } + } +}; diff --git a/core/modules/edit/js/createjs/editable.js b/core/modules/edit/js/createjs/editable.js new file mode 100644 index 0000000..9713c32 --- /dev/null +++ b/core/modules/edit/js/createjs/editable.js @@ -0,0 +1,40 @@ +(function (jQuery, undefined) { + // # Create.js editing widget for Spark + // + // This widget inherits from the Create.js editable widget to accommodate + // for the fact that Spark is using custom data attributes and not RDFa + // to communicate editable fields. + 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'; + + this.options.editors.direct = { + widget: 'drupalContentEditableWidget', + options: {} + }; + this.options.editors['direct-with-wysiwyg'] = { + widget: 'drupalAlohaWidget', + options: {} + }; + this.options.editors.form = { + widget: 'drupalFormWidget', + options: {} + }; + + jQuery.Midgard.midgardEditable.prototype._create.call(this); + }, + + _propertyEditorName: function (data) { + if (Drupal.settings.edit.wysiwyg && jQuery(this.element).hasClass('edit-type-direct')) { + if (jQuery(this.element).hasClass('edit-type-direct-with-wysiwyg')) { + return 'direct-with-wysiwyg'; + } + return 'direct'; + } + return 'form'; + } + }); +})(jQuery); diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalalohawidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalalohawidget.js new file mode 100644 index 0000000..b080d53 --- /dev/null +++ b/core/modules/edit/js/createjs/editingWidgets/drupalalohawidget.js @@ -0,0 +1,157 @@ +/** + * @file drupalalohawidget.js + * + * Override of Create.js' default Aloha Editor widget. + * + * This does in fact use zero code of jQuery.create.alohaWidget. + */ + +(function (jQuery, undefined) { + jQuery.widget('Drupal.drupalAlohaWidget', jQuery.Create.alohaWidget, { + + // @todo: actually use this when restoring original content, but for that we + // first need to know how to restore content in a Create.js context + originalTransformedContent: null, + + /** + * Implements jQuery UI widget factory's _init() method. + * + * @todo: get rid of this once https://github.com/bergie/create/issues/142 + * is solved. + */ + _init: function() {}, + + /** + * Implements Create's _initialize() method. + */ + _initialize: function() { + this._bindEvents(); + + // Immediately initialize Aloha, this can take some time. By doing it now + // already, it will most likely already be ready when the user actually + // wants to use Aloha Editor. + Drupal.aloha.init(); + }, + + /** + * Binds to events. + * + * @todo: get rid of this helper function and move it into _initialize() + * once https://github.com/alohaeditor/Aloha-Editor/issues/693 is solved. + */ + _bindEvents: function() { + var that = this; + + // Sets the state to 'activated' upon clicking the element. + this.element.bind("click.edit", function(event) { + event.stopPropagation(); + event.preventDefault(); + that.options.activating(); + }); + + // Sets the state to 'changed' whenever the content has changed. + this.element.bind('aloha-content-changed', function(event, $alohaEditable, data) { + if (!data.editable.isModified()) { + return true; + } + that.options.changed(data.editable.getContents()); + data.editable.setUnmodified(); + }); + }, + + /** + * Makes this PropertyEditor widget react to state changes. + * + * @todo revisit this once https://github.com/bergie/create/issues/133 is + * solved. + */ + stateChange: function(from, to) { + switch (to) { + case 'inactive': + break; + case 'candidate': + if (from !== 'inactive') { + Drupal.aloha.detach(this.element); + this._removeValidationErrors(); + this._cleanUp(); + + // TRICKY: work-around for major AE bug. See: + // - http://drupal.org/node/1725032 + // - https://github.com/alohaeditor/Aloha-Editor/issues/693. + // @todo: get rid of this once https://github.com/alohaeditor/Aloha-Editor/issues/693 is solved. + this._bindEvents(); + } + 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-rendered version of + // it without the transformation filters. + if (this.options.widget.element.hasClass('edit-text-with-transformation-filters')) { + this.originalTransformedContent = this.element.html(); + + var that = this; + Drupal.edit.util.loadRerenderedProcessedText({ + $editorElement: this.element, + propertyID: Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property), + callback: function (rerendered) { + that.element.html(rerendered); + that.options.activated(); + } + }); + } + // When no transformation filters have been applied: start WYSIWYG + // editing immediately! + else { + this.options.activated(); + } + break; + case 'active': + // Attach Aloha Editor with this field's text format. + var formatID = this.options.widget.element.attr('data-edit-text-format'); + var format = Drupal.settings.aloha.formats[formatID]; + Drupal.aloha.attach(this.element, format); + Drupal.aloha.activate(this.element, format); + break; + case 'changed': + break; + case 'saving': + this._removeValidationErrors(); + break; + case 'saved': + break; + case 'invalid': + break; + } + }, + + /** + * 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() { + this.element + .removeClass('edit-validation-error') + .next('.edit-validation-errors').remove(); + }, + + /** + * Cleans up after the widget has been saved. + * + * Note: this is where the Create.Storage and accompanying Backbone.sync + * abstractions "leak" implementation details. That is only the case because + * we have to use Drupal's Form API as a transport mechanism. It is + * unfortunately a stateful transport mechanism, and that's why we have to + * clean it up here. This clean-up is only necessary when canceling the + * editing of a property after having attempted to save at least once. + */ + _cleanUp: function() { + Drupal.edit.util.form.unajaxifySaving(jQuery('#edit_backstage form .edit-form-submit')); + jQuery('#edit_backstage form').remove(); + } + }); +})(jQuery); diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js new file mode 100644 index 0000000..51d8539 --- /dev/null +++ b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js @@ -0,0 +1,111 @@ +/** + * @file drupalcontenteditablewidget.js + * + * Override of Create.js' default "base" (plain contentEditable) widget. + */ + +(function (jQuery, undefined) { + jQuery.widget('Drupal.drupalContentEditableWidget', jQuery.Create.editWidget, { + + /** + * Implements jQuery UI widget factory's _init() method. + * + * @todo: get rid of this once https://github.com/bergie/create/issues/142 + * is solved. + */ + _init: function() {}, + + /** + * Implements Create's _initialize() method. + */ + _initialize: function() { + var that = this; + + // Sets the state to 'activated' upon clicking the element. + this.element.bind("click.edit", function(event) { + event.stopPropagation(); + event.preventDefault(); + that.options.activated(); + }); + + // Sets the state to 'changed' whenever the content has changed. + var before = jQuery.trim(this.element.text()); + this.element.bind('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. + * + * @todo revisit this once https://github.com/bergie/create/issues/133 is + * solved. + */ + stateChange: function(from, to) { + switch (to) { + case 'inactive': + break; + case 'candidate': + if (from !== 'inactive') { + // Removes the "contenteditable" attribute. + this.disable(); + this._removeValidationErrors(); + this._cleanUp(); + } + break; + case 'highlighted': + break; + case 'activating': + break; + case 'active': + // Sets the "contenteditable" attribute to "true". + this.enable(); + break; + case 'changed': + break; + case 'saving': + this._removeValidationErrors(); + break; + case 'saved': + break; + case 'invalid': + break; + } + }, + + /** + * 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() { + this.element + .removeClass('edit-validation-error') + .next('.edit-validation-errors').remove(); + }, + + /** + * Cleans up after the widget has been saved. + * + * Note: this is where the Create.Storage and accompanying Backbone.sync + * abstractions "leak" implementation details. That is only the case because + * we have to use Drupal's Form API as a transport mechanism. It is + * unfortunately a stateful transport mechanism, and that's why we have to + * clean it up here. This clean-up is only necessary when canceling the + * editing of a property after having attempted to save at least once. + */ + _cleanUp: function() { + Drupal.edit.util.form.unajaxifySaving(jQuery('#edit_backstage form .edit-form-submit')); + jQuery('#edit_backstage form').remove(); + } + }); +})(jQuery); diff --git a/core/modules/edit/js/createjs/editingWidgets/formwidget.js b/core/modules/edit/js/createjs/editingWidgets/formwidget.js new file mode 100644 index 0000000..1f5f8a3 --- /dev/null +++ b/core/modules/edit/js/createjs/editingWidgets/formwidget.js @@ -0,0 +1,150 @@ +/** + * @file drupalformwidget.js + * + * Form-based Create.js widget for structured content in Drupal. + */ + +(function ($, undefined) { + // Drupal form-based editing widget for Create.js + $.widget('Drupal.drupalFormWidget', $.Create.editWidget, { + + id: null, + $formContainer: null, + + /** + * Implements jQuery UI widget factory's _init() method. + * + * @todo: get rid of this once https://github.com/bergie/create/issues/142 + * is solved. + */ + _init: function() {}, + + /** + * Implements Create's _initialize() method. + */ + _initialize: function() { + // Sets the state to 'activating' upon clicking the element. + var that = this; + this.element.bind("click.edit", function(event) { + event.stopPropagation(); + event.preventDefault(); + that.options.activating(); + }); + }, + + /** + * Makes this PropertyEditor widget react to state changes. + * + * @todo revisit this once https://github.com/bergie/create/issues/133 is + * solved. + */ + 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 = jQuery(Drupal.theme('editFormContainer', { + id: this.id, + loadingMsg: Drupal.t('Loading…')} + )); + this.$formContainer + .find('.edit-form') + .addClass('edit-editable edit-highlighted edit-editing') + .css('background-color', $editorElement.css('background-color')); + + // Insert form container in DOM. + if ($editorElement.css('display') === 'inline') { + // @todo: 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 + .delegate(':input', 'formUpdated.edit', function () { + // Sets the state to 'changed'. + widget.options.changed(); + }) + .delegate('input', 'keypress.edit', 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')); + this.$formContainer + .undelegate(':input', 'change.edit') + .undelegate('input', 'keypress.edit') + .remove(); + this.$formContainer = null; + } + }); +})(jQuery); diff --git a/core/modules/edit/js/createjs/storage.js b/core/modules/edit/js/createjs/storage.js new file mode 100644 index 0000000..00da4ef --- /dev/null +++ b/core/modules/edit/js/createjs/storage.js @@ -0,0 +1,4 @@ +(function (jQuery, undefined) { + // Subclass jQuery.Midgard.midgardStorage just to have consistent namespaces. + jQuery.widget('Drupal.createStorage', jQuery.Midgard.midgardStorage, {}); +})(jQuery); diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js new file mode 100644 index 0000000..7afd177 --- /dev/null +++ b/core/modules/edit/js/edit.js @@ -0,0 +1,41 @@ +(function ($, VIE) { + +Drupal.edit = Drupal.edit || {}; + +Drupal.behaviors.editDiscoverEditables = { + attach: function(context) { + // @todo: we need to separate the discovery of editables if we want updated + // or new content (added by code other than Edit) to be detected + // automatically. Once we implement this, we'll be able to get rid of all + // calls to Drupal.edit.domService.findSubjectElements() :) + } +}; + +/** + * Attach toggling behavior and in-place editing. + */ +Drupal.behaviors.edit = { + attach: function(context) { + $('#edit_view-edit-toggles').once('edit-init', Drupal.edit.init); + } +}; + +Drupal.edit.init = function() { + // Instantiate EditAppView, which is the controller of it all. EditAppModel + // instance tracks global state (viewing/editing in-place). + var appModel = new Drupal.edit.models.EditAppModel(); + var app = new Drupal.edit.EditAppView({ + el: $('body'), + model: appModel + }); + + // Instantiate EditRouter. + var editRouter = new Drupal.edit.routers.EditRouter({ + appModel: appModel + }); + + // Start Backbone's history/route handling. + Backbone.history.start(); +}; + +})(jQuery, VIE); diff --git a/core/modules/edit/js/lib/create.js b/core/modules/edit/js/lib/create.js new file mode 100644 index 0000000..1d76bff --- /dev/null +++ b/core/modules/edit/js/lib/create.js @@ -0,0 +1,1643 @@ +// 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.bind('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.bind('add remove reset', widget.checkCollectionConstraints, widget); + + widget._bindCollectionView(widget.options.view); + }, + + _bindCollectionView: function (view) { + var widget = this; + view.bind('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.bind('add', function (itemView) { + //itemView.el.effect('slide'); + widget._makeEditable(itemView); + widget._refreshButtons(); + }); + view.bind('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()); + + _.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.disableEditor({ + widget: this, + editable: editable, + entity: this.options.model, + element: jQuery(editable) + }); + }, 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(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) { + var widgetName = jQuery(data.element).data('createWidgetName'); + + 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'); + + 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('Create.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.bind('focus', function () { + if (self.options.disabled) { + return; + } + self.options.activated(); + }); + this.element.bind('blur', function () { + if (self.options.disabled) { + return; + } + self.options.deactivated(); + }); + var before = this.element.html(); + this.element.bind('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('Create.alohaWidget', jQuery.Create.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('Create.ckeditorWidget', jQuery.Create.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(); + }); + 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()); + }); + }, + + 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('Create.halloWidget', jQuery.Create.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).bind('halloactivated', function (event, data) { + self.options.activated(); + }); + jQuery(this.element).bind('hallodeactivated', function (event, data) { + self.options.deactivated(); + }); + jQuery(this.element).bind('hallomodified', function (event, data) { + self.options.changed(data.content); + data.editable.setUnmodified(); + }); + + jQuery(document).bind('midgardtoolbarstatechange', function(event, data) { + // Switch between Hallo configurations when toolbar state changes + if (data.display === self.options.toolbarState) { + return; + } + self.options.toolbarState = data.display; + 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('Create.redactorWidget', jQuery.Create.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).bind('focus', function (event) { + self.options.activated(); + }); + /* + jQuery(this.element).bind('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.bind('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.bind('startPreventSave', function () { + if (timeout) { + window.clearInterval(timeout); + timeout = null; + } + widget.disableAutoSave(); + }); + this.element.bind('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.bind(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.bind(widget.options.editableNs + 'disable', function (event, options) { + widget._restoreLocal(options.instance); + }); + + widget.element.bind(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.bind('midgardcreatestatechange', function (event, options) { + if (options.state === 'browse' || widget.restorables.length === 0) { + widget.restorables = []; + if (restorer) { + restorer.close(); + } + return; + } + restorer = widget.checkRestore(); + }); + + widget.element.bind('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.restoreLocal(); + 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; + }, + + restoreLocal: 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)); + }, + + _restoreLocal: function (model) { + var widget = this; + + // Remove unsaved collection members + if (!model) { return; } + _.each(model.attributes, function (attributeValue, property) { + if (attributeValue instanceof widget.vie.Collection) { + var removables = []; + attributeValue.forEach(function (model) { + if (model.isNew()) { + removables.push(model); + } + }); + attributeValue.remove(removables); + } + }); + + // Restore original object properties + if (!model.changedAttributes()) { + if (model._originalAttributes) { + model.set(model._originalAttributes); + } + return; + } + + model.set(model.previousAttributes()); + }, + + _removeLocal: function (model) { + if (!this.options.localStorage) { + return; + } + + window.localStorage.removeItem(model.getSubjectUri()); + } + }); +})(jQuery); +if (window.midgardCreate === undefined) { + window.midgardCreate = {}; +} +if (window.midgardCreate.locale === undefined) { + window.midgardCreate.locale = {}; +} + +window.midgardCreate.locale.en = { + // Session-state buttons for the main toolbar + 'Save': 'Save', + 'Saving': 'Saving', + 'Cancel': 'Cancel', + 'Edit': 'Edit', + // Storage status messages + 'localModification': 'Item "<%= label %>" has local modifications', + 'localModifications': '<%= number %> items on this page have local modifications', + 'Restore': 'Restore', + 'Ignore': 'Ignore', + 'saveSuccess': 'Item "<%= label %>" saved successfully', + 'saveSuccessMultiple': '<%= number %> items saved successfully', + 'saveError': 'Error occurred while saving
<%= error %>', + // Tagging + 'Item tags': 'Item tags', + 'Suggested tags': 'Suggested tags', + 'Tags': 'Tags', + 'add a tag': 'add a tag', + // Collection widgets + 'Add': 'Add', + 'Choose type to add': 'Choose type to add' +}; diff --git a/core/modules/edit/js/lib/vie.js b/core/modules/edit/js/lib/vie.js new file mode 100644 index 0000000..ca5c079 --- /dev/null +++ b/core/modules/edit/js/lib/vie.js @@ -0,0 +1,3682 @@ +/*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])); + 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); + return Backbone.Model.prototype.set.call(this, attrs, options); + }, + + // **`.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; +})(); \ No newline at end of file diff --git a/core/modules/edit/js/models/edit-app-model.js b/core/modules/edit/js/models/edit-app-model.js new file mode 100644 index 0000000..464741b --- /dev/null +++ b/core/modules/edit/js/models/edit-app-model.js @@ -0,0 +1,12 @@ +Drupal.edit = Drupal.edit || {}; +Drupal.edit.models = Drupal.edit.models || {}; +Drupal.edit.models.EditAppModel = Backbone.Model.extend({ + defaults: { + // We always begin in view mode. + isViewing: true, + highlightedEditor: null, + activeEditor: null, + // Reference to a ModalView-instance if a transition requires confirmation. + activeModal: null + } +}); diff --git a/core/modules/edit/js/routers/edit-router.js b/core/modules/edit/js/routers/edit-router.js new file mode 100644 index 0000000..19c885b --- /dev/null +++ b/core/modules/edit/js/routers/edit-router.js @@ -0,0 +1,50 @@ +/** + * @file edit-router.js + * + * A Backbone Router enabling URLs to make the user enter edit mode directly. + */ + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.routers = {}; +Drupal.edit.routers.EditRouter = Backbone.Router.extend({ + + appModel: null, + + routes: { + "quick-edit": "edit", + "view": "view", + "": "view" + }, + + initialize: function(options) { + this.appModel = options.appModel; + }, + + edit: function() { + this.appModel.set('isViewing', false); + }, + + view: function(query, page) { + var that = this; + + // If there's an active editor, attempt to set its state to 'candidate', and + // then act according to the user's choice. + var activeEditor = this.appModel.get('activeEditor'); + if (activeEditor) { + var editableEntity = activeEditor.options.widget; + var predicate = activeEditor.options.property; + editableEntity.setState('candidate', predicate, { reason: 'menu' }, function(accepted) { + if (accepted) { + that.appModel.set('isViewing', true); + } + else { + that.navigate('#quick-edit'); + } + }); + } + // Otherwise, we can switch to view mode directly. + else { + that.appModel.set('isViewing', true); + } + } +}); diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js new file mode 100644 index 0000000..9e5d14b --- /dev/null +++ b/core/modules/edit/js/theme.js @@ -0,0 +1,150 @@ +(function($) { + +/** + * Theme function for the overlay of the Edit module. + * + * @param settings + * An object with the following keys: + * - None. + * @return + * The corresponding HTML. + */ +Drupal.theme.editOverlay = function(settings) { + var html = ''; + html += '
'; + return html; +}; + +/** + * Theme function for a "backstage" for the Edit module. + * + * @param settings + * An object with the following keys: + * - id: the id to apply to the backstage. + * @return + * The corresponding HTML. + */ +Drupal.theme.editBackstage = function(settings) { + var html = ''; + html += '
'; + return html; +}; + +/** + * Theme function for a modal of the Edit module. + * + * @param settings + * An object with the following keys: + * - None. + * @return + * The corresponding HTML. + */ +Drupal.theme.editModal = function(settings) { + var classes = 'edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast'; + var html = ''; + html += '
'; + html += '

'; + html += '
'; + html += '
'; + return html; +}; + +/** + * Theme function for a toolbar container of the Edit module. + * + * @param settings + * An object with the following keys: + * - id: the id to apply to the toolbar container. + * @return + * The corresponding HTML. + */ +Drupal.theme.editToolbarContainer = function(settings) { + var html = ''; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + html += '
'; + return html; +}; + +/** + * Theme function for a toolbar toolgroup of the Edit module. + * + * @param settings + * An object with the following keys: + * - classes: the class of the toolgroup. + * - buttons: @see Drupal.theme.prototype.editButtons(). + * @return + * The corresponding HTML. + */ +Drupal.theme.editToolgroup = function(settings) { + var classes = 'edit-toolgroup edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast'; + var html = ''; + html += '
'; + html += Drupal.theme('editButtons', { buttons: settings.buttons }); + html += '
'; + return html; +}; + +/** + * Theme function for buttons of the Edit module. + * + * Can be used for the buttons both in the toolbar toolgroups and in the modal. + * + * @param settings + * An object with the following keys: + * - buttons: an array of objects with the following keys: + * - url: the URL the button should point to. + * - classes: the classes of the button. + * - label: the label of the button. + * - hasButtonRole: whether this button should have its "role" attribute set + * to "button". + * - action: sets a data-edit-modal-action attribute. + * @return + * The corresponding HTML. + */ +Drupal.theme.editButtons = function(settings) { + var html = ''; + for (var i = 0; i < settings.buttons.length; i++) { + var button = settings.buttons[i]; + if (!button.hasOwnProperty('url')) { + button.url = ''; + } + if (!button.hasOwnProperty('hasButtonRole')) { + button.hasButtonRole = true; + } + + html += ''; + html += '
'; + html += '
'; + html += settings.loadingMsg; + html += '
'; + html += '
'; + html += '
'; + return html; +}; + +})(jQuery); diff --git a/core/modules/edit/js/util.js b/core/modules/edit/js/util.js new file mode 100644 index 0000000..3d86a57 --- /dev/null +++ b/core/modules/edit/js/util.js @@ -0,0 +1,140 @@ +(function($) { + +/** + * @file util.js + * + * Utilities for Edit module. + */ + +Drupal.edit = Drupal.edit || {}; +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; +}; + +Drupal.edit.util.buildUrl = function(id, urlFormat) { + var parts = id.split('/'); + return Drupal.formatString(decodeURIComponent(urlFormat), { + '!entity_type': parts[0], + '!id' : parts[1], + '!field_name' : parts[2], + '!langcode' : parts[3], + '!view_mode' : parts[4] + }); +}; + +/** + * Loads rerendered processed text for a given property. + * + * Leverages Drupal.ajax' ability to have scoped (per-instance) command + * implementations to be able to call a callback. + * + * @param options + * An object with the following keys: + * - $editorElement (required): the PredicateEditor DOM element. + * - propertyID (required): the property ID that uniquely identifies the + * property for which this form will be loaded. + * - callback (required: A callback function that will receive the rerendered + * processed text. + */ +Drupal.edit.util.loadRerenderedProcessedText = function(options) { + // Create a Drupal.ajax instance to load the form. + Drupal.ajax[options.propertyID] = new Drupal.ajax(options.propertyID, options.$editorElement, { + url: Drupal.edit.util.buildUrl(options.propertyID, Drupal.settings.edit.rerenderProcessedTextURL), + event: 'edit-internal.edit', + submit: { nocssjs : true }, + progress: { type : null } // No progress indicator. + }); + // Implement a scoped edit_field_form AJAX command: calls the callback. + Drupal.ajax[options.propertyID].commands.edit_field_rendered_without_transformation_filters = function(ajax, response, status) { + options.callback(response.data); + // Delete the Drupal.ajax instance that called this very function. + delete Drupal.ajax[options.propertyID]; + options.$editorElement.unbind('edit-internal.edit'); + }; + // This will ensure our scoped edit_field_form AJAX command gets called. + options.$editorElement.trigger('edit-internal.edit'); +}; + +Drupal.edit.util.form = { + /** + * Loads a form, calls a callback to inserts. + * + * Leverages Drupal.ajax' ability to have scoped (per-instance) command + * implementations to be able to call a callback. + * + * @param options + * An object with the following keys: + * - $editorElement (required): the PredicateEditor DOM element. + * - propertyID (required): the property ID that uniquely identifies the + * property for which this form will be loaded. + * - nocssjs (required): boolean indicating whether no CSS and JS should be + * returned (necessary when the form is invisible to the user). + * @param callback + * A callback function that will receive the form to be inserted, as well as + * the ajax object, necessary if the callback wants to perform other AJAX + * commands. + */ + load: function(options, callback) { + // Create a Drupal.ajax instance to load the form. + Drupal.ajax[options.propertyID] = new Drupal.ajax(options.propertyID, options.$editorElement, { + url: Drupal.edit.util.buildUrl(options.propertyID, Drupal.settings.edit.fieldFormURL), + event: 'edit-internal.edit', + submit: { nocssjs : options.nocssjs }, + progress: { type : null } // No progress indicator. + }); + // Implement a scoped edit_field_form AJAX command: calls the callback. + Drupal.ajax[options.propertyID].commands.edit_field_form = function(ajax, response, status) { + callback(response.data, ajax); + // Delete the Drupal.ajax instance that called this very function. + delete Drupal.ajax[options.propertyID]; + options.$editorElement.unbind('edit-internal.edit'); + }; + // This will ensure our scoped edit_field_form AJAX command gets called. + options.$editorElement.trigger('edit-internal.edit'); + }, + + /** + * Creates a Drupal.ajax instance that is used to save a form. + * + * @param options + * An object with the following keys: + * - nocssjs (required): boolean indicating whether no CSS and JS should be + * returned (necessary when the form is invisible to the user). + * + * @return + * The key of the Drupal.ajax instance. + */ + ajaxifySaving: function(options, $submit) { + // Re-wire the form to handle submit. + var element_settings = { + url: $submit.closest('form').attr('action'), + setClick: true, + event: 'click.edit', + progress: { type:'throbber' }, + submit: { nocssjs : options.nocssjs } + }; + var base = $submit.attr('id'); + + Drupal.ajax[base] = new Drupal.ajax(base, $submit[0], element_settings); + + return base; + }, + + /** + * Cleans up the Drupal.ajax instance that is used to save the form. + * + * @param $submit + * The jQuery-wrapped submit DOM element that should be unajaxified. + */ + unajaxifySaving: function($submit) { + delete Drupal.ajax[$submit.attr('id')]; + $submit.unbind('click.edit'); + } +}; + +})(jQuery); diff --git a/core/modules/edit/js/viejs/SparkEditService.js b/core/modules/edit/js/viejs/SparkEditService.js new file mode 100644 index 0000000..fa492fd --- /dev/null +++ b/core/modules/edit/js/viejs/SparkEditService.js @@ -0,0 +1,202 @@ +// VIE DOM parsing service for Spark Edit +(function () { + + VIE.prototype.SparkEditService = 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.SparkEditService.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 = Drupal.settings.edit.context; + } + } else { + element = loadable.options.element; + } + + var entities = this.readEntities(element); + loadable.resolve(entities); + }, + + // 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. + // @todo: check the above. + 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); + } + + // Register with VIE + return this._registerEntity(entity); + }, + + _registerEntity: function (entityData) { + var entityInstance = new this.vie.Entity(entityData); + return this.vie.entities.addOrUpdate(entityInstance, { + updateOptions: { + silent: true + } + }); + }, + + _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 label = element.data('edit-field-label'); + var range = 'Form'; + if (element.hasClass('edit-type-direct')) { + range = 'Direct'; + } + if (element.hasClass('edit-type-direct-with-wysiwyg')) { + range = 'Wysiwyg'; + } + 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(predicateElement); + if (value === null && !emptyValues) { + return; + } + + entityPredicates[predicate] = value; + }); + return entityPredicates; + }, + + _readElementValue: function (element) { + return jQuery.trim(element.html()); + }, + + // Subject elements are the DOM elements containing a single or multiple + // editable fields. In Spark Edit these elements are called _Fields_, + // and the actual DOM elements which are edited are called _Editables_. + findSubjectElements: function (element) { + if (!element) { + element = Drupal.settings.edit.context; + } + return jQuery(this.options.subjectSelector, element); + }, + + // Predicate Elements are the actual DOM elements that users will be able + // to edit. In regular Spark Edit they are called _Editables_. + // + // They are contained within Entity elements, which in Spark Edit are called + // _Fields_. + // @todo: clarify and document what the Best Way to do this is? Should VIE's + // entities map to Drupal's entities etc? Also see higher comments. + findPredicateElements: function (subject, element, allowNestedPredicates, stop) { + var predicates = jQuery(); + + // 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; + } + }; +})(); diff --git a/core/modules/edit/js/views/fielddecorator-view.js b/core/modules/edit/js/views/fielddecorator-view.js new file mode 100644 index 0000000..491114c --- /dev/null +++ b/core/modules/edit/js/views/fielddecorator-view.js @@ -0,0 +1,319 @@ +/** + * @file fielddecorator-view.js + * + * A Backbone View that decorates properties. + * It listens to state changes of the property editor. + * + * @todo rename to propertydecorator-view.js + PropertyDecorationView. + */ + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.views = Drupal.edit.views || {}; +Drupal.edit.views.FieldDecorationView = Backbone.View.extend({ + + editor: null, + entity: null, + predicate : null, + editorName: null, + toolbarId: null, + + _widthAttributeIsEmpty: null, + + events: { + 'mouseenter.edit' : 'onMouseEnter', + 'mouseleave.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. + * * editorName: the editor name: 'form', 'direct' or + * 'direct-with-wysiwyg'. + * * widget: the parent EditableeEntity 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.entity = this.editor.options.entity; + this.predicate = this.editor.options.property; + this.editorName = this.editor.options.editorName; + + this.$el.css('background-color', this._getBgColor(this.$el)); + }, + + /** + * Listens to editor state changes. + */ + stateChange: function(from, to) { + switch (to) { + case 'inactive': + if (from !== null) { + this.undecorate(); + } + break; + case 'candidate': + this.decorate(); + if (from !== 'inactive') { + this.stopHighlight(); + if (from !== 'highlighted') { + this.stopEdit(this.editorName); + } + } + break; + case 'highlighted': + this.startHighlight(); + break; + case 'activating': + // NOTE: this step only exists for the 'form' editor! It is skipped by + // the 'direct' and 'direct-with-wysiwyg' editors, because no loading is + // necessary. + this.prepareEdit(this.editorName); + break; + case 'active': + if (this.editorName !== 'form') { + this.prepareEdit(this.editorName); + } + this.startEdit(this.editorName); + break; + case 'changed': + break; + case 'saving': + 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(); + }); + }, + + decorate: function () { + this.$el.addClass('edit-animate-fast edit-candidate edit-editable'); + }, + + undecorate: function () { + this.$el + .removeClass('edit-candidate edit-editable edit-highlighted edit-editing edit-belowoverlay'); + }, + + startHighlight: function () { + // Animations. + var that = this; + setTimeout(function() { + that.$el.addClass('edit-highlighted'); + }, 0); + }, + + stopHighlight: function() { + this.$el + .removeClass('edit-highlighted'); + }, + + prepareEdit: function(editorName) { + this.$el.addClass('edit-editing'); + + // While editing, don't show *any* other editors. + // @todo: revisit this once https://github.com/bergie/create/issues/133 is solved. + jQuery('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); + + if (editorName === 'form') { + this.$el.addClass('edit-belowoverlay'); + } + }, + + startEdit: function(editorName) { + if (editorName !== 'form') { + this._pad(); + } + }, + + stopEdit: function(editorName) { + this.$el.removeClass('edit-highlighted edit-editing'); + + // Make the other editors show up again. + // @todo: revisit this once https://github.com/bergie/create/issues/133 is solved. + jQuery('.edit-candidate').addClass('edit-editable'); + + if (editorName === 'form') { + this.$el.removeClass('edit-belowoverlay'); + } + else { + this._unpad(); + } + }, + + _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()); + } + + // 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', ''); + } + + // 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) { + // @todo: this was pos == NaN (which always returns false, keeping this + // comment in case we find a regression. + 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 (jQuery(event.relatedTarget).closest(closest).length > 0) { + event.stopPropagation(); + } + else { + callback(); + } + } +}); diff --git a/core/modules/edit/js/views/menu-view.js b/core/modules/edit/js/views/menu-view.js new file mode 100644 index 0000000..085e562 --- /dev/null +++ b/core/modules/edit/js/views/menu-view.js @@ -0,0 +1,38 @@ +/** + * @file menu-view.js + * + * A Backbone View that provides the app-level interactive menu. + */ + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.views = Drupal.edit.views || {}; +Drupal.edit.views.MenuView = Backbone.View.extend({ + + /** + * Implements Backbone Views' initialize() function. + */ + initialize: function() { + _.bindAll(this, 'stateChange'); + this.model.on('change:isViewing', this.stateChange); + + // We have to call stateChange() here, because URL fragments are not passed + // the server, thus the wrong anchor may be marked as active. + this.stateChange(); + }, + + /** + * Listens to app state changes. + */ + stateChange: function() { + // Unmark whichever one is currently marked as active. + this.$('a.edit_view-edit-toggle') + .removeClass('active') + .parent().removeClass('active'); + + // Mark the correct one as active. + var activeAnchor = this.model.get('isViewing') ? 'view' : 'edit'; + this.$('a.edit_view-edit-toggle.edit-' + activeAnchor) + .addClass('active') + .parent().addClass('active'); + } +}); diff --git a/core/modules/edit/js/views/modal-view.js b/core/modules/edit/js/views/modal-view.js new file mode 100644 index 0000000..76d0085 --- /dev/null +++ b/core/modules/edit/js/views/modal-view.js @@ -0,0 +1,108 @@ +/** + * @file modal-view.js + * + * A Backbone View that provides an interactive modal. + */ + +(function($) { + +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 a[role=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() { + // Step 1: move certain UI elements below the overlay. + var editor = this.model.get('activeEditor'); + this.$elementsToHide = $([]) + .add((editor.element.hasClass('edit-belowoverlay')) ? null : editor.element) + .add(editor.toolbarView.$el) + .add((editor.options.editorName === 'form') + ? editor.$formContainer + : editor.element.next('.edit-validation-errors') + ); + this.$elementsToHide.addClass('edit-belowoverlay'); + + // Step 2: the modal. When the user makes a choice, the UI elements that + // were moved below the overlay will be restored, and the callback will be + // called. + 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); + + // Step 3; 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') + .bind(Drupal.edit.util.constants.transitionEnd, function(e) { + that.remove(); + }); + + var action = $(event.target).attr('data-edit-modal-action'); + return this.callback(action); + }, + + /** + * Overrides Backbone Views' remove() function. + */ + remove: function() { + // Move the moved UI elements on top of the overlay again. + this.$elementsToHide.removeClass('edit-belowoverlay'); + + // Remove the modal itself. + this.$el.remove(); + } +}); + +})(jQuery); diff --git a/core/modules/edit/js/views/overlay-view.js b/core/modules/edit/js/views/overlay-view.js new file mode 100644 index 0000000..db8d150 --- /dev/null +++ b/core/modules/edit/js/views/overlay-view.js @@ -0,0 +1,80 @@ +/** + * @file overlay-view.js + * + * A Backbone View that provides the app-level overlay. + * + * The overlay sits on top of the existing content, the properties that are + * candidates for editing sit on top of the overlay. + */ + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.views = Drupal.edit.views || {}; +Drupal.edit.views.OverlayView = Backbone.View.extend({ + + events: { + 'click': 'onClick' + }, + + /** + * Implements Backbone Views' initialize() function. + */ + initialize: function(options) { + _.bindAll(this, 'stateChange'); + this.model.on('change:isViewing', this.stateChange); + }, + + /** + * Listens to app state changes. + */ + stateChange: function() { + if (this.model.get('isViewing')) { + this.remove(); + return; + } + this.render(); + }, + + /** + * Equates clicks anywhere on the overlay to clicking the active editor's (if + * any) "close" button. + * + * @param event + */ + onClick: function(event) { + event.preventDefault(); + var activeEditor = this.model.get('activeEditor'); + if (activeEditor) { + var editableEntity = activeEditor.options.widget; + var predicate = activeEditor.options.property; + editableEntity.setState('candidate', predicate, { reason: 'overlay' }); + } + }, + + /** + * Inserts the overlay element and appends it to the body. + */ + render: function() { + this.setElement( + jQuery(Drupal.theme('editOverlay', {})) + .appendTo('body') + .addClass('edit-animate-slow edit-animate-invisible') + ); + // Animations + this.$el.css('top', jQuery('#navbar').outerHeight()); + this.$el.removeClass('edit-animate-invisible'); + }, + + /** + * Remove the overlay element. + */ + remove: function() { + var that = this; + this.$el + .addClass('edit-animate-invisible') + .bind(Drupal.edit.util.constants.transitionEnd, function (event) { + that.$el.remove(); + // @todo - should the overlay really do this? + jQuery('.edit-form-container, .edit-toolbar-container, #edit_modal, .edit-curtain, .edit-validation-errors').remove(); + }); + } +}); diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js new file mode 100644 index 0000000..72bfef3 --- /dev/null +++ b/core/modules/edit/js/views/toolbar-view.js @@ -0,0 +1,443 @@ +/** + * @file toolbar-view.js + * + * 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. + */ + +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, + + _id: null, + + events: { + 'click.edit a.label': 'onClickInfoLabel', + 'mouseleave.edit': 'onMouseLeave', + 'click.edit a.field-save': 'onClickSave', + 'click.edit a.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: 'form', 'direct' or + * 'direct-with-wysiwyg'. + * * 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; + + // Generate a DOM-compatible ID for the toolbar DOM element. + var propertyID = Drupal.edit.util.calcPropertyID(this.entity, this.predicate); + this._id = 'edit-toolbar-for-' + propertyID.replace(/\//g, '_'); + }, + + /** + * Listens to editor state changes. + */ + stateChange: function(from, to) { + switch (to) { + case 'inactive': + // Nothing happens in this stage. + break; + case 'candidate': + if (from !== 'inactive') { + if (from !== 'highlighted' && this.editorName !== 'form') { + this._unpad(this.editorName); + } + this.remove(); + } + break; + case 'highlighted': + // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title). + this.render(); + this.startHighlight(); + break; + case 'activating': + this.setLoadingIndicator(true); + break; + case 'active': + this.startEdit(this.editorName); + this.setLoadingIndicator(false); + if (this.editorName !== 'form') { + this._pad(this.editorName); + } + if (this.editorName === 'direct-with-wysiwyg') { + this.insertWYSIWYGToolGroups(); + } + break; + case 'changed': + this.$el + .find('a.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); + + // Replace the old content with the new content. + var updatedField = entity.get(predicate + '/rendered'); + var $inner = jQuery(updatedField).html(); + editor.element.html($inner); + + // @todo: VIE doesn't seem to like this? :) It seems that if I delete/ + // overwrite an existing field, that VIE refuses to find the same + // predicate again for the same entity? + // self.$el.replaceWith(updatedField); + // debugger; + // console.log(self.$el, self.el, Drupal.edit.domService.findSubjectElements(self.$el)); + // Drupal.edit.domService.findSubjectElements(self.$el).each(Drupal.edit.prepareFieldView); + + editableEntity.setState('candidate', predicate); + }, + + // 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 = jQuery('
') + .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 && !jQuery.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' }); + }, + + /** + * Indicate in the 'info' toolgroup that we're waiting for a server reponse. + * + * @param bool enabled + * Whether the loading indicator should be displayed or not. + */ + setLoadingIndicator: function(enabled) { + if (enabled) { + this.addClass('info', 'loading'); + } + else { + // Only stop showing the loading indicator after half a second to prevent + // it from flashing, which is bad UX. + var that = this; + setTimeout(function() { + that.removeClass('info', 'loading'); + }, 500); + } + }, + + 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 + .find('.edit-toolbar') + // Append the "info" toolgroup into the toolbar. + .append(Drupal.theme('editToolgroup', { + classes: 'info', + buttons: [ + { label: label, classes: 'blank-button label', hasButtonRole: false } + ] + })); + + // 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'), classes: 'field-save save gray-button' }, + { label: '', classes: 'field-close close gray-button' } + ] + })); + this.show('ops'); + }, + + /** + * Adjusts the toolbar to accomodate padding on the PropertyEditor widget. + * + * @see FieldDecorationView._pad(). + */ + _pad: function(editorName) { + // 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' }); + // When using a WYSIWYG editor, the width of the toolbar must match the + // width of the editable. + if (editorName === 'direct-with-wysiwyg') { + $hf.css({ width: this.editor.element.width() + 10 }); + } + }, + + /** + * Undoes the changes made by _pad(). + * + * @see FieldDecorationView._unpad(). + */ + _unpad: function(editorName) { + // Move the toolbar back to its original position. + var $hf = this.$el.find('.edit-toolbar-heightfaker'); + $hf.css({ bottom: '1px', left: '' }); + // When using a WYSIWYG editor, restore the width of the toolbar. + if (editorName === 'direct-with-wysiwyg') { + $hf.css({ width: '' }); + } + }, + + insertWYSIWYGToolGroups: function() { + this.$el + .find('.edit-toolbar') + .append(Drupal.theme('editToolgroup', { + classes: 'wysiwyg-tabs', + buttons: [] + })) + .append(Drupal.theme('editToolgroup', { + classes: 'wysiwyg', + buttons: [] + })); + + // Animate the toolgroups into visibility. + var that = this; + setTimeout(function () { + that.show('wysiwyg-tabs'); + that.show('wysiwyg'); + }, 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(jQuery(Drupal.theme('editToolbarContainer', { + id: this.getId() + }))); + + // Insert in DOM. + if (this.$el.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); + } + + var that = this; + // Animate the toolbar into visibility. + setTimeout(function () { + that.$el.removeClass('edit-animate-invisible'); + }, 0); + }, + + remove: function () { + if (!this.$el) { + return; + } + + // Remove after animation. + var that = this; + var $el = this.$el; + this.$el + .addClass('edit-animate-invisible') + // Prevent this toolbar from being detected *while* it is being removed. + .removeAttr('id') + .find('.edit-toolbar .edit-toolgroup') + .addClass('edit-animate-invisible') + .bind(Drupal.edit.util.constants.transitionEnd, function (e) { + $el.remove(); + }); + // @todo: verify/confirm that this really necessary. Messing with this.$el + // is not recommended - maybe temporarily unbind/undelegate events? + // Immediately set to null, so that if the user hovers over the property + // before the removal been completed, a new toolbar can be created. + // this.$el = null; + }, + + /** + * Calculates the ID for this toolbar container. + * + * Only used to make sane hovering behavior possible. + * + * @return string + * A string that can be used as the ID for this toolbar container. + */ + getId: function() { + return 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); + } +}); diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php index 6b34ba9..85d2878 100644 --- a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php +++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php @@ -23,6 +23,9 @@ * "text", * "text_long", * "text_with_summary" + * }, + * edit = { + * "editability" = "direct" * } * ) */ diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextPlainFormatter.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextPlainFormatter.php index 0f7b615..8dc4bf1 100644 --- a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextPlainFormatter.php +++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextPlainFormatter.php @@ -23,6 +23,9 @@ * "text", * "text_long", * "text_with_summary" + * }, + * edit = { + * "editability" = "direct" * } * ) */ diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextSummaryOrTrimmedFormatter.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextSummaryOrTrimmedFormatter.php index 11f0c14..c69e5e0 100644 --- a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextSummaryOrTrimmedFormatter.php +++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextSummaryOrTrimmedFormatter.php @@ -22,6 +22,9 @@ * }, * settings = { * "trim_length" = "600" + * }, + * edit = { + * "editability" = "form" * } * ) */ diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php index 349cf63..add5d55 100644 --- a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php +++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php @@ -31,6 +31,9 @@ * }, * settings = { * "trim_length" = "600" + * }, + * edit = { + * "editability" = "form" * } * ) */