core/modules/edit/css/edit.css | 419 ++++++++++++ core/modules/edit/edit.info | 6 + core/modules/edit/edit.module | 369 +++++++++++ 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 | 179 +++++ core/modules/edit/js/ajax.js | 124 ++++ core/modules/edit/js/edit.js | 689 ++++++++++++++++++++ core/modules/edit/js/theme.js | 154 +++++ core/modules/edit/js/ui-editables.js | 179 +++++ core/modules/edit/js/ui.js | 82 +++ core/modules/edit/js/util.js | 98 +++ .../field/formatter/TextDefaultFormatter.php | 3 + .../Plugin/field/formatter/TextPlainFormatter.php | 3 + .../formatter/TextSummaryOrTrimmedFormatter.php | 3 + .../field/formatter/TextTrimmedFormatter.php | 3 + 19 files changed, 2658 insertions(+) diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css new file mode 100644 index 0000000..5a99f11 --- /dev/null +++ b/core/modules/edit/css/edit.css @@ -0,0 +1,419 @@ +/** + * 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); +} + +/* 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-toolbar-container.edit-belowoverlay { + z-index: 210; +} +.edit-editable.edit-belowoverlay { + z-index: 200; +} + + + + +/** + * Edit mode: 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; +} + +.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..9ca87e7 --- /dev/null +++ b/core/modules/edit/edit.module @@ -0,0 +1,369 @@ + 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('attributes' => array('class' => array('edit_view-edit-toggle', 'edit-view')))), + '#active' => TRUE, + ), + array( + '#theme' => 'menu_local_task', + '#link' => array('title' => t('Quick edit'), 'href' => '#', 'localized_options' => array('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/ui.js' => array( + 'defer' => TRUE, + ), + $path . '/js/ui-editables.js' => array( + 'defer' => TRUE, + ), + $path . '/js/theme.js' => array( + 'defer' => TRUE, + ), + $path . '/js/ajax.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'), + ), + ); + + // 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..d59442a --- /dev/null +++ b/core/modules/edit/includes/pages.inc @@ -0,0 +1,179 @@ +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); + 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), + ); + } + + // 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/ajax.js b/core/modules/edit/js/ajax.js new file mode 100644 index 0000000..f2bdd3b --- /dev/null +++ b/core/modules/edit/js/ajax.js @@ -0,0 +1,124 @@ +(function($) { + +/** + * @file ajax.js + * + * AJAX commands for Edit module. + */ + +// Hide these in a ready to ensure that Drupal.ajax is set up first. +$(function() { + Drupal.ajax.prototype.commands.edit_field_form = function(ajax, response, status) { + console.log('edit_field_form', ajax, response, status); + + // Only apply the form immediately if this form is currently being edited. + if (Drupal.edit.state.editedEditable == response.id && ajax.$field.hasClass('edit-type-form')) { + Drupal.ajax.prototype.commands.insert(ajax, { + data: response.data, + selector: '.edit-form-container .placeholder' + }); + + // Indicate in the 'info' toolgroup that the form has loaded, but only do + // it after half a second to prevent it from flashing, which is bad UX. + setTimeout(function() { + Drupal.edit.toolbar.removeClass(ajax.$editable, 'info', 'loading'); + }, 500); + + // Detect changes in this form. + Drupal.edit.form.get(ajax.$editable) + .delegate(':input', 'formUpdated.edit', function() { + ajax.$editable + .data('edit-content-changed', true) + .trigger('edit-content-changed.edit'); + }) + .delegate('input', 'keypress.edit', function(event) { + if (event.keyCode == 13) { + return false; + } + }); + + var $submit = Drupal.edit.form.get(ajax.$editable).find('.edit-form-submit'); + var element_settings = { + url : $submit.closest('form').attr('action'), + setClick : true, + event : 'click.edit', + progress : { type : 'throbber' }, + // IPE-specific settings. + $editable : ajax.$editable, + $field : ajax.$field + }; + var base = $submit.attr('id'); + Drupal.ajax[base] = new Drupal.ajax(base, $submit[0], element_settings); + + // Give focus to the first input in the form. + //$('.edit-form').find('form :input:visible:enabled:first').focus() + } + else if (Drupal.edit.state.editedEditable == response.id && ajax.$field.hasClass('edit-type-direct')) { + Drupal.edit.state.directEditableFormResponse = response; + $('#edit_backstage').append(response.data); + + var $submit = $('#edit_backstage form .edit-form-submit'); + var element_settings = { + url : $submit.closest('form').attr('action'), + setClick : true, + event : 'click.edit', + progress : { type : 'throbber' }, + // IPE-specific settings. + $editable : ajax.$editable, + $field : ajax.$field + }; + var base = $submit.attr('id'); + Drupal.ajax[base] = new Drupal.ajax(base, $submit[0], element_settings); + } + else { + console.log('queueing', response); + } + + // Animations. + Drupal.edit.toolbar.show(ajax.$editable, 'ops'); + ajax.$editable.trigger('edit-form-loaded.edit'); + }; + Drupal.ajax.prototype.commands.edit_field_rendered_without_transformation_filters = function(ajax, response, status) { + console.log('edit_field_rendered_without_transformation_filters', ajax, response, status); + + if (Drupal.edit.state.editedEditable == response.id + && ajax.$field.hasClass('edit-type-direct') + && ajax.$field.hasClass('edit-text-with-transformation-filters') + ) + { + // Indicate in the 'info' toolgroup that the form has loaded, but only do + // it after half a second to prevent it from flashing, which is bad UX. + setTimeout(function() { + Drupal.edit.toolbar.removeClass(ajax.$editable, 'info', 'loading'); + }, 500); + + // Update the HTML of the editable and enable WYSIWYG editing on it. + ajax.$editable.html(response.data); + Drupal.edit.editables._wysiwygify(ajax.$editable); + } + }; + Drupal.ajax.prototype.commands.edit_field_form_saved = function(ajax, response, status) { + console.log('edit_field_form_saved', ajax, response, status); + + // Stop the editing. + Drupal.edit.editables.stopEdit(ajax.$editable); + + // Response.data contains the updated rendering of the field, if any. + if (response.data) { + // Replace the old content with the new content. + var $field = $('.edit-field[data-edit-id="' + response.id + '"]'); + var $parent = $field.parent(); + if ($field.css('display') == 'inline') { + $parent.html(response.data); + } + else { + $field.replaceWith(response.data); + } + + // Make the freshly rendered field(s) in-place-editable again. + Drupal.edit.startEditableFields(Drupal.edit.findEditableFields($parent)); + } + }; +}); + +})(jQuery); diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js new file mode 100644 index 0000000..d851ee2 --- /dev/null +++ b/core/modules/edit/js/edit.js @@ -0,0 +1,689 @@ +(function ($) { + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.wysiwyg = Drupal.edit.wysiwyg || {}; + +/** + * Attach toggling behavior and in-place editing. + */ +Drupal.behaviors.edit = { + attach: function(context) { + $('#edit_view-edit-toggles').once('edit-init', Drupal.edit.init); + $('#edit_view-edit-toggles').once('edit-toggle', Drupal.edit.toggle.render); + + // TODO: remove this; this is to make the current prototype somewhat usable. + $('a.edit_view-edit-toggle').click(function() { + $(this).trigger('click.edit'); + }); + } +}; + +Drupal.edit.const = {}; +Drupal.edit.const.transitionEnd = "transitionEnd.edit webkitTransitionEnd.edit transitionend.edit msTransitionEnd.edit oTransitionEnd.edit"; + +Drupal.edit.init = function() { + Drupal.edit.state = {}; + // We always begin in view mode. + Drupal.edit.state.isViewing = true; + Drupal.edit.state.fieldBeingHighlighted = []; + Drupal.edit.state.fieldBeingEdited = []; + Drupal.edit.state.higlightedEditable = null; + Drupal.edit.state.editedEditable = null; + Drupal.edit.state.queues = {}; + Drupal.edit.state.wysiwygReady = false; + + // Build inventory. + var IDMapper = function() { return Drupal.edit.getID($(this)); }; + Drupal.edit.state.fields = Drupal.edit.findEditableFields().map(IDMapper); + console.log('Fields:', Drupal.edit.state.fields.length, ';', Drupal.edit.state.fields); + + // Form preloader. + Drupal.edit.state.queues.preload = Drupal.edit.findEditableFields().filter('.edit-type-form').map(IDMapper); + console.log('Fields with (server-generated) forms:', Drupal.edit.state.queues.preload); + + // Initialize WYSIWYG, if any. + if (Drupal.settings.edit.wysiwyg) { + $(document).bind('edit-wysiwyg-ready.edit', function() { + Drupal.edit.state.wysiwygReady = true; + console.log('edit: WYSIWYG ready'); + }); + Drupal.edit.wysiwyg[Drupal.settings.edit.wysiwyg].init(); + } + + // Create a backstage area. + $(Drupal.theme('editBackstage', {})).appendTo('body'); + + // Transition between view/edit states. + $("a.edit_view-edit-toggle").bind('click.edit', function() { + var wasViewing = Drupal.edit.state.isViewing; + var isViewing = Drupal.edit.state.isViewing = $(this).hasClass('edit-view'); + // Swap active class among the two links. + $('a.edit_view-edit-toggle').removeClass('active'); + $('a.edit_view-edit-toggle').parent().removeClass('active'); + $('a.edit_view-edit-toggle.edit-' + (isViewing ? 'view' : 'edit')).addClass('active'); + $('a.edit_view-edit-toggle.edit-' + (isViewing ? 'view' : 'edit')).parent().addClass('active'); + + if (wasViewing && !isViewing) { + $(Drupal.theme('editOverlay', {})) + .appendTo('body') + .addClass('edit-animate-slow edit-animate-invisible') + .bind('click.edit', Drupal.edit.clickOverlay);; + + var $f = Drupal.edit.findEditableFields(); + Drupal.edit.startEditableFields($f); + + // TODO: preload forms. We could do one request per form, but that's more + // RTTs than needed. Instead, the server should support batch requests. + console.log('Preloading forms that we might need!', Drupal.edit.state.queues.preload); + + // Animations. Integrate with both navbar and toolbar. + $('#edit_overlay').css('top', $('#navbar, #toolbar').outerHeight()); + $('#edit_overlay').removeClass('edit-animate-invisible'); + + // Disable contextual links in edit mode. + $('.contextual-links-region') + .addClass('edit-contextual-links-region') + .removeClass('contextual-links-region'); + } + else if (!wasViewing && isViewing) { + // Animations. + $('#edit_overlay') + .addClass('edit-animate-invisible') + .bind(Drupal.edit.const.transitionEnd, function(e) { + $('#edit_overlay, .edit-form-container, .edit-toolbar-container, #edit_modal, #edit_backstage').remove(); + }); + + var $f = Drupal.edit.findEditableFields(); + Drupal.edit.stopEditableFields($f); + + // Re-enable contextual links in view mode. + $('.edit-contextual-links-region') + .addClass('contextual-links-region') + .removeClass('edit-contextual-links-region'); + } + else { + // No state change. + } + return false; + }); +}; + +Drupal.edit.findEditableFields = function(context) { + return $('.edit-field.edit-allowed', context || Drupal.settings.edit.context); +}; + +/* + * findEditableFields() just looks for fields that are editable, i.e. for the + * field *wrappers*. Depending on the field, however, either the whole field wrapper + * will be marked as editable (in this case, an inline form will be used for editing), + * *or* a specific (field-specific even!) DOM element within that field wrapper will be + * marked as editable. + * This function is for finding the *editables* themselves, given the *editable fields*. + */ +Drupal.edit.findEditablesForFields = function($fields) { + var $editables = $(); + + // type = form + $editables = $editables.add($fields.filter('.edit-type-form')); + + // type = direct + var $direct = $fields.filter('.edit-type-direct'); + $editables = $editables.add($direct.find('.field-item')); + // Edge case: "title" pseudofield on pages with lists of nodes. + $editables = $editables.add($direct.filter('h2').find('a')); + // Edge case: "title" pseudofield on node pages. + $editables = $editables.add($direct.find('h1')); + + return $editables; +}; + +Drupal.edit.getID = function($field) { + return $field.data('edit-id'); +}; + +Drupal.edit.findFieldForID = function(id, context) { + return $('[data-edit-id="' + id + '"]', context || $('#content')); +}; + +Drupal.edit.findFieldForEditable = function($editable) { + return $editable.filter('.edit-type-form').length ? $editable : $editable.closest('.edit-type-direct'); +}; + +Drupal.edit.startEditableFields = function($fields) { + var $fields = $fields.once('edit'); + // Ignore fields that need a WYSIWYG editor if no WYSIWYG editor is present + if (!Drupal.settings.edit.wysiwyg) { + $fields = $fields.filter(':not(.edit-type-direct-with-wysiwyg)'); + } + var $editables = Drupal.edit.findEditablesForFields($fields); + + $editables + .addClass('edit-animate-fast') + .addClass('edit-candidate edit-editable') + .bind('mouseenter.edit', function(e) { + var $editable = $(this); + Drupal.edit.util.ignoreHoveringVia(e, '.edit-toolbar-container', function() { + console.log('field:mouseenter'); + if (!$editable.hasClass('edit-editing')) { + Drupal.edit.editables.startHighlight($editable); + } + }); + }) + .bind('mouseleave.edit', function(e) { + var $editable = $(this); + Drupal.edit.util.ignoreHoveringVia(e, '.edit-toolbar-container', function() { + console.log('field:mouseleave'); + if (!$editable.hasClass('edit-editing')) { + Drupal.edit.editables.stopHighlight($editable); + } + }); + }) + .bind('click.edit', function() { + Drupal.edit.editables.startEdit($(this)); return false; + }) + // Some transformations are editable-specific. + .map(function() { + $(this).data('edit-background-color', Drupal.edit.util.getBgColor($(this))); + }); +}; + +Drupal.edit.stopEditableFields = function($fields) { + var $editables = Drupal.edit.findEditablesForFields($fields); + + $fields + .removeClass('edit-processed'); + + $editables + .removeClass('edit-candidate edit-editable edit-highlighted edit-editing edit-belowoverlay') + .unbind('mouseenter.edit mouseleave.edit click.edit edit-content-changed.edit') + .removeAttr('contenteditable') + .removeData(['edit-content-original', 'edit-content-changed']); +}; + +Drupal.edit.clickOverlay = function(e) { + console.log('clicked overlay'); + + if (Drupal.edit.modal.get().length == 0) { + Drupal.edit.toolbar.get(Drupal.edit.state.fieldBeingEdited) + .find('a.close').trigger('click.edit'); + } +}; + +/* +2. Each field MAY (if it is editable by the user) contain exactly one Editable, + in which the editing itself occurs, this can be either: + a. type=direct, here some child element of the Field element is marked as editable + b. type=form, here the field itself is marked as editable, upon edit, a form is used + */ + +// Field editables. +Drupal.edit.editables = { + startHighlight: function($editable) { + console.log('editables.startHighlight'); + if (Drupal.edit.toolbar.create($editable)) { + var label = $editable.filter('.edit-type-form').data('edit-field-label') + || $editable.closest('.edit-type-direct').data('edit-field-label'); + + Drupal.edit.toolbar.get($editable) + .find('.edit-toolbar:not(:has(.edit-toolgroup.info))') + .append(Drupal.theme('editToolgroup', { + classes: 'info', + buttons: [ + { url: '#', label: label, classes: 'blank-button label', hasButtonRole: false }, + ] + })) + .delegate('a.label', 'click.edit', function(e) { + // Clicking the label equals clicking the editable itself. + $editable.trigger('click.edit'); + return false; + }); + } + + // Animations. + setTimeout(function() { + $editable.addClass('edit-highlighted'); + Drupal.edit.toolbar.show($editable, 'info'); + }, 0); + + Drupal.edit.state.fieldBeingHighlighted = $editable; + Drupal.edit.state.higlightedEditable = Drupal.edit.getID(Drupal.edit.findFieldForEditable($editable)); + }, + + stopHighlight: function($editable) { + console.log('editables.stopHighlight'); + if ($editable.length == 0) { + return; + } + + // Animations. + Drupal.edit.toolbar.remove($editable); + $editable.removeClass('edit-highlighted'); + + Drupal.edit.state.fieldBeingHighlighted = []; + Drupal.edit.state.highlightedEditable = null; + }, + + startEdit: function($editable) { + if ($editable.hasClass('edit-editing')) { + return; + } + + console.log('editables.startEdit: ', $editable); + var self = this; + var $field = Drupal.edit.findFieldForEditable($editable); + + // Highlight if not already highlighted. + if (Drupal.edit.state.fieldBeingHighlighted[0] != $editable[0]) { + Drupal.edit.editables.startHighlight($editable); + } + + $editable + .addClass('edit-editing') + .bind('edit-content-changed.edit', function(e) { + self._buttonFieldSaveToBlue(e, $editable, $field); + }) + // Some transformations are editable-specific. + .map(function() { + $(this).css('background-color', $(this).data('edit-background-color')); + }); + + // While editing, don't show *any* other field as editable. + $('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); + + // Toolbar (already created in the highlight). + Drupal.edit.toolbar.get($editable) + .addClass('edit-editing') + .find('.edit-toolbar:not(:has(.edit-toolgroup.ops))') + .append(Drupal.theme('editToolgroup', { + classes: 'ops', + buttons: [ + { url: '#', label: Drupal.t('Save'), classes: 'field-save save gray-button' }, + { url: '#', title: 'Undo changes', label: '', classes: 'field-close close gray-button' } + ] + })) + .delegate('a.field-save', 'click.edit', function(e) { + return self._buttonFieldSaveClicked(e, $editable, $field); + }) + .delegate('a.field-close', 'click.edit', function(e) { + return self._buttonFieldCloseClicked(e, $editable, $field); + }); + + // Changes to $editable based on the type. + var callback = ($field.hasClass('edit-type-direct')) + ? self._updateDirectEditable + : self._updateFormEditable; + callback($editable); + + // Regardless of the type, load the form for this field. We always use forms + // to submit the changes. + self._loadForm($editable, $field); + + Drupal.edit.state.fieldBeingEdited = $editable; + Drupal.edit.state.editedEditable = Drupal.edit.getID($field); + }, + + stopEdit: function($editable) { + console.log('editables.stopEdit: ', $editable); + var self = this; + var $field = Drupal.edit.findFieldForEditable($editable); + if ($editable.length == 0) { + return; + } + + $editable + .removeClass('edit-highlighted edit-editing edit-belowoverlay') + // Some transformations are editable-specific. + .map(function() { + $(this).css('background-color', ''); + }); + + // Make the other fields and entities editable again. + $('.edit-candidate').addClass('edit-editable'); + + // Changes to $editable based on the type. + var callback = ($field.hasClass('edit-type-direct')) + ? self._restoreDirectEditable + : self._restoreFormEditable; + callback($editable); + + Drupal.edit.toolbar.remove($editable); + Drupal.edit.form.remove($editable); + + Drupal.edit.state.fieldBeingEdited = []; + Drupal.edit.state.editedEditable = null; + }, + + _loadRerenderedProcessedText: function($editable, $field) { + // Indicate in the 'info' toolgroup that the form is loading. + Drupal.edit.toolbar.addClass($editable, 'info', 'loading'); + + var edit_id = Drupal.edit.getID($field); + var element_settings = { + url : Drupal.edit.util.calcRerenderProcessedTextURL(edit_id), + event : 'edit-internal-load-rerender.edit', + $field : $field, + $editable: $editable, + submit : { nocssjs : true }, + progress : { type : null }, // No progress indicator. + }; + if (Drupal.ajax.hasOwnProperty(edit_id)) { + delete Drupal.ajax[edit_id]; + $editable.unbind('edit-internal-load-rerender.edit'); + } + Drupal.ajax[edit_id] = new Drupal.ajax(edit_id, $editable, element_settings); + $editable.trigger('edit-internal-load-rerender.edit'); + }, + + // Attach, activate and show the WYSIWYG editor. + _wysiwygify: function($editable) { + var $field = Drupal.edit.findFieldForEditable($editable); + $editable.addClass('edit-wysiwyg-attached'); + var formatID = $field.attr('data-edit-text-format'); + var format = Drupal.settings.aloha.formats[formatID]; + Drupal.edit.wysiwyg[Drupal.settings.edit.wysiwyg].attach($editable, format); + Drupal.edit.wysiwyg[Drupal.settings.edit.wysiwyg].activate($editable); + Drupal.edit.toolbar.show($editable, 'wysiwyg-tabs'); + Drupal.edit.toolbar.show($editable, 'wysiwyg'); + }, + + _updateDirectEditable: function($editable) { + var $field = Drupal.edit.findFieldForEditable($editable); + + Drupal.edit.editables._padEditable($editable, $field); + + if ($field.hasClass('edit-type-direct-with-wysiwyg')) { + Drupal.edit.toolbar.get($editable) + .find('.edit-toolbar:not(:has(.edit-toolgroup.wysiwyg-tabs))') + .append(Drupal.theme('editToolgroup', { + classes: 'wysiwyg-tabs', + buttons: [] + })) + .end() + .find('.edit-toolbar:not(:has(.edit-toolgroup.wysiwyg))') + .append(Drupal.theme('editToolgroup', { + classes: 'wysiwyg', + buttons: [] + })); + + // 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 ($field.hasClass('edit-text-with-transformation-filters')) { + Drupal.edit.editables._loadRerenderedProcessedText($editable, $field); + // Also store the "real" original content, i.e. the transformed one. + $editable.data('edit-content-original-transformed', $editable.html()) + } + // When no transformation filters have been applied: start WYSIWYG editing + // immediately! + else { + setTimeout(function() { + Drupal.edit.editables._wysiwygify($editable); + }, 0); + } + } + else { + $editable.attr('contenteditable', true); + } + + $editable + .data('edit-content-original', $editable.html()) + .data('edit-content-changed', false); + + // Detect content changes ourselves only when not using a WYSIWYG editor. + var markContentChanged = function() { + $editable.data('edit-content-changed', true); + $editable.trigger('edit-content-changed.edit'); + }; + if (!$field.hasClass('edit-type-direct-with-wysiwyg')) { + // We cannot use Drupal.behaviors.formUpdated here because we're not dealing + // with a form! + $editable + .bind('blur.edit keyup.edit paste.edit', function() { + if ($editable.html() != $editable.data('edit-content-original')) { + markContentChanged(); + } + }) + // Disallow return/enter key when editing titles. + .bind('keypress.edit', function(event) { + if (event.keyCode == 13) { + return false; + } + }); + } + else { + $editable.bind('edit-wysiwyg-content-changed.edit', function() { + markContentChanged(); + }); + } + }, + + _restoreDirectEditable: function($editable) { + var $field = Drupal.edit.findFieldForEditable($editable); + + Drupal.edit.editables._padEditable($editable, $field); + + if ($field.hasClass('edit-type-direct-with-wysiwyg') && $editable.hasClass('edit-wysiwyg-attached')) { + $editable.removeClass('edit-wysiwyg-attached'); + Drupal.edit.wysiwyg[Drupal.settings.edit.wysiwyg].detach($editable); + + // Work-around for major AE bug. See: + // - http://drupal.org/node/1725032 + // - https://github.com/alohaeditor/Aloha-Editor/issues/693. + // Also unbind to make sure this doesn't break anything when using + // this version of edit.js with a fixed version of Aloha Editor. + $editable + .unbind('click.edit') + .bind('click.edit', function() { + Drupal.edit.editables.startEdit($(this)); return false; + }); + } + else { + $editable + .removeAttr('contenteditable') + .unbind('keypress.edit'); + } + + Drupal.edit.editables._unpadEditable($editable, $field); + + $editable + .removeData(['edit-content-original', 'edit-content-changed', 'edit-content-original-transformed']) + .unbind('blur.edit keyup.edit paste.edit edit-content-changed.edit'); + + // Not only clean up the changes to $editable, but also clean up the + // backstage area, where we hid the form that we used to send the changes. + $('#edit_backstage form').remove(); + }, + + _padEditable: function($editable, $field) { + // 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 ($editable[0].style.width === "") { + $editable + .data('edit-width-empty', true) + .addClass('edit-animate-disable-width') + .css('width', $editable.width()); + } + // 2) Add padding; use animations. + var posProp = Drupal.edit.util.getPositionProperties($editable); + var $toolbar = Drupal.edit.toolbar.get($editable); + setTimeout(function() { + // Re-enable width animations (padding changes affect width too!). + $editable.removeClass('edit-animate-disable-width'); + + // The toolbar must move to the top and the left. + var $hf = $toolbar.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 ($field.hasClass('edit-type-direct-with-wysiwyg')) { + $hf.css({ width: $editable.width() + 10 }); + } + + // Pad the editable. + $editable + .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); + }, + + _unpadEditable: function($editable, $field) { + // 1) Set the empty width again. + if ($editable.data('edit-width-empty') === true) { + console.log('restoring width'); + $editable + .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 = Drupal.edit.util.getPositionProperties($editable); + var $toolbar = Drupal.edit.toolbar.get($editable); + setTimeout(function() { + // Re-enable width animations (padding changes affect width too!). + $editable.removeClass('edit-animate-disable-width'); + + // Move the toolbar back to its original position. + var $hf = $toolbar.find('.edit-toolbar-heightfaker'); + $hf.css({ bottom: '1px', left: '' }); + // When using a WYSIWYG editor, restore the width of the toolbar. + if ($field.hasClass('edit-type-direct-with-wysiwyg')) { + $hf.css({ width: '' }); + } + + // Unpad the editable. + $editable + .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); + }, + + // Creates a form container; when the $editable is inline, it will inherit CSS + // properties from the toolbar container, so the toolbar must already exist. + _updateFormEditable: function($editable) { + if (Drupal.edit.form.create($editable)) { + $editable + .addClass('edit-belowoverlay') + .removeClass('edit-highlighted edit-editable'); + + Drupal.edit.form.get($editable) + .find('.edit-form') + .addClass('edit-editable edit-highlighted edit-editing') + .css('background-color', $editable.data('edit-background-color')); + } + }, + + _restoreFormEditable: function($editable) { + // No need to do anything here; all of the field HTML will be overwritten + // with the freshly rendered version from the server anyway! + }, + + _loadForm: function($editable, $field) { + var edit_id = Drupal.edit.getID($field); + var element_settings = { + url : Drupal.edit.util.calcFormURLForField(edit_id), + event : 'edit-internal.edit', + $field : $field, + $editable: $editable, + submit : { nocssjs : ($field.hasClass('edit-type-direct')) }, + progress : { type : null }, // No progress indicator. + }; + if (Drupal.ajax.hasOwnProperty(edit_id)) { + delete Drupal.ajax[edit_id]; + $editable.unbind('edit-internal.edit'); + } + Drupal.ajax[edit_id] = new Drupal.ajax(edit_id, $editable, element_settings); + $editable.trigger('edit-internal.edit'); + }, + + _buttonFieldSaveToBlue: function(e, $editable, $field) { + Drupal.edit.toolbar.get($editable) + .find('a.save').addClass('blue-button').removeClass('gray-button'); + }, + + _buttonFieldSaveClicked: function(e, $editable, $field) { + // type = form + if ($field.hasClass('edit-type-form')) { + Drupal.edit.form.get($field).find('form') + .find('.edit-form-submit').trigger('click.edit').end(); + } + // type = direct + else if ($field.hasClass('edit-type-direct')) { + $editable.blur(); + + var wysiwyg = $field.hasClass('edit-type-direct-with-wysiwyg') + && $editable.hasClass('edit-wysiwyg-attached'); + + // When using WYSIWYG editing, first detach the WYSIWYG editor to ensure + // the content has been cleaned up before saving it. (Otherwise, + // annotations and infrastructure created by the WYSIWYG editor could also + // get saved). + if (wysiwyg) { + $editable.removeClass('edit-wysiwyg-attached'); + Drupal.edit.wysiwyg[Drupal.settings.edit.wysiwyg].detach($editable); + } + + // We trim the title because otherwise whitespace in the raw HTML ends + // up in the title as well. + // TRICKY: Drupal core does not trim the title, so in theory this is + // out of line with Drupal core's behavior. + var value = (wysiwyg) + ? $.trim($editable.html()) + : $.trim($editable.text()); + var selector = (wysiwyg) + ? 'textarea' + : ':input[type!="hidden"][type!="submit"]'; + $('#edit_backstage form') + .find(selector).val(value).end() + .find('.edit-form-submit').trigger('click.edit'); + } + return false; + }, + + _buttonFieldCloseClicked: function(e, $editable, $field) { + // Content not changed: stop editing field. + if (!$editable.data('edit-content-changed')) { + // Restore to original content. When dealing with processed text, it's + // possible that one or more transformation filters are used. Then, the + // "real" original content (i.e. the transformed one) is stored separately + // from the "original content" that we use to detect changes. + if (typeof $editable.data('edit-content-original-transformed') !== 'undefined') { + $editable.html($editable.data('edit-content-original-transformed')); + } + + Drupal.edit.editables.stopEdit($editable); + } + // Content changed: show modal. + else { + Drupal.edit.modal.create( + Drupal.t('You have unsaved changes'), + Drupal.theme('editButtons', { 'buttons' : [ + { url: '#', classes: 'gray-button discard', label: Drupal.t('Discard changes') }, + { url: '#', classes: 'blue-button save', label: Drupal.t('Save') } + ]}), + $editable + ); + setTimeout(Drupal.edit.modal.show, 0); + }; + return false; + } +}; + +})(jQuery); diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js new file mode 100644 index 0000000..5b2b655 --- /dev/null +++ b/core/modules/edit/js/theme.js @@ -0,0 +1,154 @@ +(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: + * - None. + * @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 (optional) + * - label: the label of the button (optional) + * - title: the title of the button (optional) + * - hasButtonRole: whether this button should have its "role" attribute set + * to "button" + * @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('hasButtonRole')) { + button.hasButtonRole = true; + } + + html += ''; + html += '
'; + html += '
'; + html += settings.loadingMsg; + html += '
'; + html += '
'; + html += '
'; + return html; +}; + +})(jQuery); diff --git a/core/modules/edit/js/ui-editables.js b/core/modules/edit/js/ui-editables.js new file mode 100644 index 0000000..874a224 --- /dev/null +++ b/core/modules/edit/js/ui-editables.js @@ -0,0 +1,179 @@ +(function($) { + +/** + * @file ui-editables.js + * + * UI components for editables: toolbar, form. + */ + +Drupal.edit = Drupal.edit || {}; + + +Drupal.edit.toolbar = { + create: function($editable) { + if (Drupal.edit.toolbar.get($editable).length > 0) { + return false; + } + else { + // Render toolbar. + var $toolbar = $(Drupal.theme('editToolbarContainer', { + id: this._id($editable) + })); + + // Insert in DOM. + if ($editable.css('display') == 'inline') { + $toolbar.prependTo($editable.offsetParent()); + + var pos = $editable.position(); + Drupal.edit.toolbar.get($editable) + .css('left', pos.left).css('top', pos.top); + } + else { + $toolbar.insertBefore($editable); + } + + // Animate the toolbar into visibility. + setTimeout(function() { + $toolbar.removeClass('edit-animate-invisible'); + }, 0); + + // Remove any and all existing toolbars, except for any that are for a + // currently being edited field. + $('.edit-toolbar-container:not(:has(.edit-editing))') + .trigger('edit-toolbar-remove.edit'); + + // Event bindings. + $toolbar + .bind('mouseenter.edit', function(e) { + // Prevent triggering the entity's mouse enter event. + e.stopPropagation(); + }) + .bind('mouseleave.edit', function(e) { + var el = $editable[0]; + if (e.relatedTarget != el && !jQuery.contains(el, e.relatedTarget)) { + console.log('triggering mouseleave on ', $editable); + $editable.trigger('mouseleave.edit'); + } + // Prevent triggering the entity's mouse leave event. + e.stopPropagation(); + }) + // Immediate removal whenever requested. + // (This is necessary when showing many toolbars in rapid succession: we + // don't want all of them to show up!) + .bind('edit-toolbar-remove.edit', function(e) { + $toolbar.remove(); + }) + .delegate('.edit-toolbar, .edit-toolgroup', 'click.edit mousedown.edit', function(e) { + if (!$(e.target).is(':input')) { + return false; + } + }); + + return true; + } + }, + + get: function($editable) { + return ($editable.length == 0) + ? $([]) + : $('#' + this._id($editable)); + }, + + remove: function($editable) { + var $toolbar = Drupal.edit.toolbar.get($editable); + + // Remove after animation. + $toolbar + .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.const.transitionEnd, function(e) { + $toolbar.remove(); + }); + }, + + // Animate into view. + show: function($editable, toolgroup) { + this._find($editable, toolgroup).removeClass('edit-animate-invisible'); + }, + + addClass: function($editable, toolgroup, classes) { + this._find($editable, toolgroup).addClass(classes); + }, + + removeClass: function($editable, toolgroup, classes) { + this._find($editable, toolgroup).removeClass(classes); + }, + + _find: function($editable, toolgroup) { + return Drupal.edit.toolbar.get($editable) + .find('.edit-toolbar .edit-toolgroup.' + toolgroup); + }, + + _id: function($editable) { + var edit_id = Drupal.edit.getID(Drupal.edit.findFieldForEditable($editable)); + return 'edit-toolbar-for-' + edit_id.split(':').join('_'); + } +}; + + +Drupal.edit.form = { + create: function($editable) { + if (Drupal.edit.form.get($editable).length > 0) { + return false; + } + else { + // Indicate in the 'info' toolgroup that the form is loading. Animated. + setTimeout(function() { + Drupal.edit.toolbar.addClass($editable, 'info', 'loading'); + }, 0); + + // Render form container. + var $form = $(Drupal.theme('editFormContainer', { + id: this._id($editable), + loadingMsg: Drupal.t('Loading…')} + )); + + // Insert in DOM. + if ($editable.css('display') == 'inline') { + $form.prependTo($editable.offsetParent()); + + var pos = $editable.position(); + $form.css('left', pos.left).css('top', pos.top); + // Reset the toolbar's positioning because it'll be moved inside the + // form container. + Drupal.edit.toolbar.get($editable).css('left', '').css('top', ''); + } + else { + $form.insertBefore($editable); + } + + // Move toolbar inside .edit-form-container, to let it snap to the width + // of the form instead of the field formatter. + Drupal.edit.toolbar.get($editable).detach().prependTo('.edit-form') + + return true; + } + }, + + get: function($editable) { + return ($editable.length == 0) + ? $([]) + : $('#' + this._id($editable)); + }, + + remove: function($editable) { + Drupal.edit.form.get($editable).remove(); + }, + + _id: function($editable) { + var edit_id = ($editable.hasClass('edit-entity')) + ? Drupal.edit.getID($editable) + : Drupal.edit.getID(Drupal.edit.findFieldForEditable($editable)); + return 'edit-form-for-' + edit_id.split(':').join('_'); + } +}; + +})(jQuery); diff --git a/core/modules/edit/js/ui.js b/core/modules/edit/js/ui.js new file mode 100644 index 0000000..f881527 --- /dev/null +++ b/core/modules/edit/js/ui.js @@ -0,0 +1,82 @@ +(function($) { + +/** + * @file ui.js + * + * "Global" UI components: toggle, modal. + */ + +Drupal.edit = Drupal.edit || {}; + + +Drupal.edit.toggle = { + render: function() { + // TODO: fancy, "physical toggle" to switch from view to edit mode and back. + } +}; + + +Drupal.edit.modal = { + create: function(message, actions, $editable) { + // The modal should be the only interaction element now. + $editable + .add(Drupal.edit.toolbar.get($editable)) + .addClass('edit-belowoverlay'); + + $(Drupal.theme('editModal', {})) + .appendTo('body') + .find('.main p').text(message).end() + .find('.actions').append($(actions)) + .delegate('a.discard', 'click.edit', function() { + // Restore to original content. When dealing with processed text, it's + // possible that one or more transformation filters are used. Then, the + // "real" original content (i.e. the transformed one) is stored separately + // from the "original content" that we use to detect changes. + if (typeof $editable.data('edit-content-original-transformed') !== 'undefined') { + $editable.html($editable.data('edit-content-original-transformed')); + } + else { + $editable.html($editable.data('edit-content-original')); + } + + $editable.data('edit-content-changed', false); + + Drupal.edit.modal.remove(); + Drupal.edit.toolbar.get($editable).find('a.close').trigger('click.edit'); + return false; + }) + .delegate('a.save', 'click.edit', function() { + Drupal.edit.modal.remove(); + Drupal.edit.toolbar.get($editable).find('a.save').trigger('click.edit'); + return false; + }); + }, + + get: function() { + return $('#edit_modal'); + }, + + remove: function() { + var $modal = Drupal.edit.modal.get(); + + // Remove after animation. + $modal + .addClass('edit-animate-invisible') + .bind(Drupal.edit.const.transitionEnd, function(e) { + $modal.remove(); + + // The modal's HTML was removed, hence no need to undelegate it. + }); + + // Make the other interaction elements available again. + $('.edit-belowoverlay').removeClass('edit-belowoverlay'); + }, + + // Animate into view. + show: function() { + Drupal.edit.modal.get() + .removeClass('edit-animate-invisible'); + } +}; + +})(jQuery); diff --git a/core/modules/edit/js/util.js b/core/modules/edit/js/util.js new file mode 100644 index 0000000..577448a --- /dev/null +++ b/core/modules/edit/js/util.js @@ -0,0 +1,98 @@ +(function($) { + +/** + * @file util.js + * + * Utilities for Edit module. + */ + +Drupal.edit = Drupal.edit || {}; +Drupal.edit.util = Drupal.edit.util || {}; + +Drupal.edit.util.calcFormURLForField = function(id) { + var parts = id.split(':'); + var urlFormat = decodeURIComponent(Drupal.settings.edit.fieldFormURL); + return Drupal.formatString(urlFormat, { + '!entity_type': parts[0], + '!id' : parts[1], + '!field_name' : parts[2], + '!langcode' : parts[3], + '!view_mode' : parts[4] + }); +}; + +Drupal.edit.util.calcRerenderProcessedTextURL = function(id) { + var parts = id.split(':'); + var urlFormat = decodeURIComponent(Drupal.settings.edit.rerenderProcessedTextURL); + return Drupal.formatString(urlFormat, { + '!entity_type': parts[0], + '!id' : parts[1], + '!field_name' : parts[2], + '!langcode' : parts[3], + '!view_mode' : parts[4] + }); +} + +/** + * Get the background color of an element (or the inherited one). + */ +Drupal.edit.util.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 Drupal.edit.util.getBgColor($e.parent()); + } + return c; +}; + +/** + * Ignore hovering to/from the given closest element, but as soon as a hover + * occurs to/from *another* element, then call the given callback. + */ +Drupal.edit.util.ignoreHoveringVia = function(e, closest, callback) { + if ($(e.relatedTarget).closest(closest).length > 0) { + e.stopPropagation(); + } + else { + callback(); + } +}; + +/** + * If no position properties defined, replace value with zero. + */ +Drupal.edit.util.replaceBlankPosition = function(pos) { + if (pos == 'auto' || pos == NaN) { + pos = '0px'; + } + return pos; +}; + +/** + * Get the top and left properties of an element and convert extraneous + * values and information into numbers ready for subtraction. + */ +Drupal.edit.util.getPositionProperties = function($e) { + var p, + r = {}, + props = [ + 'top', 'left', 'bottom', 'right', + 'padding-top', 'padding-left', 'padding-right', 'padding-bottom', + 'margin-bottom' + ]; + + for (var i = 0; i < props.length; i++) { + p = props[i]; + r[p] = parseFloat(this.replaceBlankPosition($e.css(p))); + } + return r; +}; + +})(jQuery); 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" * } * ) */