diff --git a/css/custom-metadata-manager.css b/css/custom-metadata-manager.css index 7215e16..faaa401 100644 --- a/css/custom-metadata-manager.css +++ b/css/custom-metadata-manager.css @@ -171,8 +171,31 @@ /* multifields */ .custom-metadata-multifield { - padding-top: 5px; + padding-top: 15px; } + +.custom-metadata-multifield-label-wrapper { + padding-bottom: 5px; +} + +.custom-metadata-multifield-label-wrapper h2 { + float: left; + margin-top: 3px; + line-height: 1; +} + +.custom-metadata-multifield-label-wrapper p.description { + float: left; + margin-left: 10px; + margin-bottom: 0; +} + +.custom-metadata-multifield-label-wrapper a.custom-metadata-multifield-add { + float: right; + margin-top: -3px; +} + + #poststuff .custom-metadata-multifield h2 { margin: 0; } @@ -182,11 +205,13 @@ margin: 2px; position: relative; } + .custom-metadata-multifield-grouping .custom-metadata-field { float: left; width: auto; - margin-left: 20px; + margin-right: 20px; } + .custom-metadata-multifield-grouping .custom-metadata-field:first-child { margin-left: 0; } @@ -197,36 +222,48 @@ width: 150px; } -.custom-metadata-multifield-clone, -.custom-metadata-multifield-delete { - position: absolute; - display: block; - top: 50%; - right: 25px; - padding: 0 3px 4px; - font-size: 35px; - line-height: 20px; - text-decoration: none; - border: 1px solid; - border-radius: 50%; -} -.custom-metadata-multifield-clone { - margin-top: -17px; - color: green; - border-color: green; -} -.custom-metadata-multifield-delete ~ .custom-metadata-multifield-clone { - margin-top: -35px; +.custom-metadata-multifield-grouping .sort-handle { + float: left; + width: 30px; + width: 5%; + margin-right: 1%; + height: 23px; + background: url(images/move-drag-icon.png) no-repeat; + background-size: 30px 23px; + margin-top: 31px; + cursor: move; +} + +.custom-metadata-multifield-grouping .custom-metadata-multifield-fields { + float: left; + width: 84%; +} + +.custom-metadata-multifield-grouping .custom-metadata-multifield-actions { + float: right; + width: 10%; + padding-top: 30px; + text-align: right; } + +.custom-metadata-multifield-grouping .custom-metadata-multifield-actions a { + margin-bottom: 5px; +} + .custom-metadata-multifield-delete { - margin-top: 17px; - padding: 1px 6px 4px; - color: red; - border-color: red; + display: inline-block; + color: #f00; + border-bottom-color: #f00; + margin-right: 3px; } -.custom-metadata-multifield-clone:hover, -.custom-metadata-multifield-clone:active, + .custom-metadata-multifield-delete:hover, .custom-metadata-multifield-delete:active { - border-color: #d54e21; -} \ No newline at end of file + color: #fff; + background-color: #f00; + border-bottom-color: #f00; +} + +.custom-metadata-field.sortable_select .select2-container { + width: 65%; +} diff --git a/css/images/move-drag-icon.png b/css/images/move-drag-icon.png new file mode 100644 index 0000000..9a90b9a Binary files /dev/null and b/css/images/move-drag-icon.png differ diff --git a/custom_metadata.php b/custom_metadata.php index dad4b6a..85763ed 100644 --- a/custom_metadata.php +++ b/custom_metadata.php @@ -51,7 +51,7 @@ class custom_metadata_manager { var $_column_types = array( 'posts', 'pages', 'users', 'comments' ); // field types - var $_field_types = array( 'text', 'textarea', 'password', 'number', 'email', 'telephone', 'checkbox', 'radio', 'select', 'multi_select', 'upload', 'wysiwyg', 'datepicker', 'datetimepicker', 'timepicker', 'taxonomy_select', 'taxonomy_radio', 'taxonomy_checkbox', 'link' ); + var $_field_types = array( 'text', 'textarea', 'password', 'number', 'email', 'telephone', 'checkbox', 'radio', 'select', 'multi_select', 'upload', 'wysiwyg', 'datepicker', 'datetimepicker', 'timepicker', 'taxonomy_select', 'taxonomy_radio', 'taxonomy_checkbox', 'link', 'sortable_select' ); // field types that are cloneable var $_cloneable_field_types = array( 'text', 'textarea', 'upload', 'password', 'number', 'email', 'tel' ); @@ -67,7 +67,7 @@ class custom_metadata_manager { // field types that support being part of a multifield group // @todo: workarounds needed for other field types - var $_field_types_that_support_multifield = array( 'text', 'textarea', 'password', 'number', 'email', 'tel', 'select' ); + var $_field_types_that_support_multifield = array( 'text', 'textarea', 'password', 'number', 'email', 'tel', 'select', 'checkbox', 'datepicker', 'datetimepicker', 'timepicker', 'link', 'upload' ); // taxonomy types var $_taxonomy_fields = array( 'taxonomy_select', 'taxonomy_radio', 'taxonomy_checkbox', 'taxonomy_multi_select' ); @@ -684,8 +684,14 @@ function save_metadata_multifield( $slug, $multifield, $object_type, $object_id foreach ( $fields as $field_slug => $field ) { if ( ! empty( $grouping[$field_slug] ) ) { $grouping_values[$field_slug] = $this->_sanitize_field_value( $field_slug, $field, $object_type, $object_id, $grouping[$field_slug] ); + if ( $field->field_type == 'upload' ) { + $grouping_values[$field_slug . '_attachment_id'] = $this->_sanitize_field_value( $field_slug . '_attachment_id', $field, $object_type, $object_id, $grouping[$field_slug . '_attachment_id'] ); + } } else { $grouping_values[$field_slug] = ''; + if ( $field->field_type == 'upload' ) { + $grouping_values[$field_slug . '_attachment_id'] = ''; + } } } $multifield_value[] = $grouping_values; @@ -712,15 +718,15 @@ function save_metadata_field( $field_slug, $field, $object_type, $object_id ) { $value = $this->_sanitize_field_value( $field_slug, $field, $object_type, $object_id, $_POST[$field_slug] ); $this->_save_field_value( $field_slug, $field, $object_type, $object_id, $value ); - // save the attachment ID of the upload field as well - if ( $field->field_type == 'upload' && isset( $_POST[$field_slug . '_attachment_id'] ) ) + if ( $field->field_type == 'upload' && isset( $_POST[$field_slug . '_attachment_id'] ) ) { $this->_save_field_value( $field_slug . '_attachment_id', $field, $object_type, $object_id, absint( $_POST[$field_slug . '_attachment_id'] ) ); + } } else { $this->_delete_field_value( $field_slug, $field, $object_type, $object_id ); // delete the attachment ID of the upload field as well - if ( $field->field_type == 'upload' && isset( $_POST[$field_slug . '_attachment_id'] ) ) + if ( $field->field_type == 'upload' ) $this->_delete_field_value( $field_slug . '_attachment_id', $field, $object_type, $object_id ); } } @@ -1086,6 +1092,7 @@ function _metadata_column_content( $field_slug, $field, $object_type, $object_id function _display_metadata_multifield( $slug, $multifield, $object_type, $object_id ) { echo '
'; + echo '
'; if ( ! empty( $multifield->label ) ) { printf( '

%s

', esc_html( $multifield->label ) ); } @@ -1094,6 +1101,13 @@ function _display_metadata_multifield( $slug, $multifield, $object_type, $object printf( '

%s

', esc_html( $multifield->description ) ); } + printf( '', __( 'Add new' ), __( 'Add new' ) ); + + echo '
'; + + echo '
'; + + $fields = $this->get_fields_in_multifield( $multifield->group, $slug, $object_type ); // validate/weed out the fields that can't be part of mulitified @@ -1110,20 +1124,37 @@ function _display_metadata_multifield( $slug, $multifield, $object_type, $object foreach ( $_values as $grouping_of_values ) { $grouping_count++; $grouping_id = $slug . '-' . $grouping_count; + printf( '
', esc_attr( $grouping_id ) ); + echo '
'; + echo ''; + echo ''; + echo '
'; echo '
'; } @@ -1143,7 +1174,7 @@ function _display_metadata_field( $field_slug, $field, $object_type, $object_id, $callback = $field->display_callback; if ( $callback && is_callable( $callback ) ) { - call_user_func( $callback, $field_slug, $field, $object_type, $object_id, $value ); + call_user_func( $callback, $field_slug, $field, $object_type, $object_id, $field_id, $value ); return; } @@ -1182,6 +1213,7 @@ function _display_metadata_field( $field_slug, $field, $object_type, $object_id, $container_class = sanitize_html_class( $field_slug ); $container_class .= ( $cloneable ) ? ' cloneable' : ''; foreach ( $value as $v ) : + $container_id = $field_slug . '-' . $count; printf( '
', esc_attr( $container_class ), esc_attr( $container_id ) ); @@ -1249,12 +1281,18 @@ function _display_metadata_field( $field_slug, $field, $object_type, $object_id, wp_editor( $v, $field_id, $wysiwyg_args ); break; case 'upload' : - $_attachment_id = $this->get_metadata_field_value( $field_slug . '_attachment_id', $field, $object_type, $object_id ); - $attachment_id = array_shift( array_values( $_attachment_id ) ); // get the first value in the array - printf( '', esc_attr( $field_id ), esc_attr( $v ), $readonly_str, $placeholder_str ); + if( isset($field->multifield) && $field->multifield != '' ) { + printf( '', esc_attr( $field_id ), esc_attr( $v['url'] ), $readonly_str, $placeholder_str ); + printf( '', esc_attr( str_replace('image]', 'image_attachment_id]', $field_id) ), esc_attr( $v['id'] ) ); + } else { + $_attachment_id = $this->get_metadata_field_value( $field_slug . '_attachment_id', $field, $object_type, $object_id ); + $_attachment_id_values = array_values( $_attachment_id ); + $attachment_id = array_shift( $_attachment_id_values ); // get the first value in the array + printf( '', esc_attr( $field_id ), esc_attr( $v ), $readonly_str, $placeholder_str ); + printf( '', esc_attr( $field_id . '_attachment_id' ), esc_attr( $attachment_id ) ); + } printf( '', esc_attr( $field->upload_modal_title ), esc_attr( $field->upload_modal_button_text ), esc_attr( $field->upload_modal_title ) ); printf( '', $field->upload_clear_button_text ); - printf( '', esc_attr( $field_id . '_attachment_id' ), esc_attr( $attachment_id ) ); break; case 'taxonomy_select' : $terms = get_terms( $field->taxonomy, array( 'hide_empty' => false ) ); @@ -1282,6 +1320,12 @@ function _display_metadata_field( $field_slug, $field, $object_type, $object_id, echo ''; } break; + case 'sortable_select' : + $container_class = sanitize_html_class( $field_slug ); + printf( '
', esc_attr( $container_class ), esc_attr( $field_id ) ); + printf( '', esc_attr( $field_slug ), esc_attr( $field_id ), esc_attr( $v ), esc_attr( json_encode($field->values) ) ); + echo '
'; + break; endswitch; if ( $cloneable && $count > 1 ) diff --git a/js/custom-metadata-manager.js b/js/custom-metadata-manager.js index f1b17b3..3375f89 100644 --- a/js/custom-metadata-manager.js +++ b/js/custom-metadata-manager.js @@ -4,12 +4,86 @@ var $custom_metadata_field = $( '.custom-metadata-field' ), $custom_metadata_multifield = $( '.custom-metadata-multifield' ); + var initMultiselectSortable = function() { + $('.select2-sortable').each(function() { + var select = $(this); + var values = $.makeArray(jQuery.parseJSON($(this).attr('data-possible-values'))); + $(this).select2({ + data: values, + multiple: true + }); + + $(this).select2("container").find("ul.select2-choices").sortable({ + containment: 'parent', + start: function() { select.select2('onSortStart'); }, + update: function() { select.select2('onSortEnd'); } + }); + }); + }; + + initMultiselectSortable(); + var incrementor = function(v) { return v.replace( /[0-9]+(?!.*[0-9])/ , function( match ) { return parseInt(match, 10) + 1; }); }; + var destroy_date_pickers = function( ) { + var $datepickers = $( '.custom-metadata-field.datepicker input' ), + $datetimepickers = $( '.custom-metadata-field.datetimepicker input' ), + $timepickers = $( '.custom-metadata-field.timepicker input' ); + + $datepickers.each(function() { + var $this = $(this); + if( $this.hasClass('hasDatepicker') ) { + $this.datepicker( 'destroy' ); + $this.removeClass('hasDatepicker'); + } + }); + + $datetimepickers.each(function() { + var $this = $(this); + if( $this.hasClass('hasDatepicker') ) { + $this.datetimepicker( 'destroy' ); + $this.removeClass('hasDatepicker'); + } + }); + + $timepickers.each(function() { + var $this = $(this); + if( $this.hasClass('hasDatepicker') ) { + $this.timepicker( 'destroy' ); + $this.removeClass('hasDatepicker'); + } + }); + }; + + var bind_date_pickers = function() { + + var $datepickers = $( '.custom-metadata-field.datepicker input' ), + $datetimepickers = $( '.custom-metadata-field.datetimepicker input' ), + $timepickers = $( '.custom-metadata-field.timepicker input' ); + + // init the datepicker fields + $datepickers.datepicker({ + changeMonth: true, + changeYear: true + }); + + // init the datetimepicker fields + $datetimepickers.datetimepicker({ + changeMonth: true, + changeYear: true + }); + + // init the timepicker fields + $timepickers.timepicker({ + changeMonth: true, + changeYear: true + }); + }; + // duplicating fields $custom_metadata_field.on( 'click.custom_metadata', '.add-multiple', function(e){ e.preventDefault(); @@ -34,90 +108,27 @@ }); }); - // cloning multifields - $custom_metadata_multifield.on( 'click.custom_metadata', '.custom-metadata-multifield-clone', function(e){ - e.preventDefault(); - var $this = $( this ), - $parent = $this.parent().parent(), - $slug = $parent.attr( 'data-slug' ), - $last = $this.parent(), - $clone = $last.clone(); - - $clone.find( ':input:not(:button)' ).val(''); - $clone.insertAfter( $last ).hide(); - - $groupings = $parent.find('.custom-metadata-multifield-grouping'); - - $.each( $groupings, function( i, grouping ){ - - var $grouping = $( grouping ), - $fields = $grouping.find('.custom-metadata-field'), - num = i + 1; - - $grouping.attr( 'id', $slug + '-' + num ); - - $.each( $fields, function( j, field ){ - var $field = $( field ), - $field_slug = $field.attr( 'data-slug' ), - $label = $field.find( 'label' ), - $div = $field.find( 'div' ), - $field_inputs = $field.find( ':input:not(:button)' ), - $field_id = $field_slug + '-' + num; - - $label.attr( 'for', $field_id ); - $div.attr( 'id', $field_id + '-1' ).attr( 'class', $field_id ); - - $.each( $field_inputs, function( k, field_input ){ - - var $field_input = $( field_input ); - - if ( ! _.isEmpty( $field_input.attr( 'id' ) ) ) { - $field_input.attr( 'id', $field_id ); - } - - if ( ! _.isEmpty( $field_input.attr( 'name' ) ) ) { - $field_input.attr( 'name', $slug + '[' + i + '][' + $field_slug + ']'); - } - }); - - }); - - }); - - $clone.fadeIn(); - - }); - - // deleting multifields - $custom_metadata_multifield.on( 'click.custom_metadata', '.custom-metadata-multifield-delete', function(e){ - e.preventDefault(); - var $this = $( this ); - $this.parent().fadeOut('normal', function(){ - $(this).remove(); - }); - }); - // init upload fields var custom_metadata_file_frame; $custom_metadata_field.on( 'click.custom_metadata', '.custom-metadata-upload-button', function(e) { e.preventDefault(); var $this = $(this), - $this_field = $this.parent(); - - // if the media frame already exists, reopen it. - if ( custom_metadata_file_frame ) { - custom_metadata_file_frame.open(); - return; + $this_field = $this.parent(); + + // if the media frame doesn't exist yet, create it + if ( ! custom_metadata_file_frame ) { + custom_metadata_file_frame = wp.media.frames.file_frame = wp.media({ + title: $this.data( 'uploader-title' ), + button: { + text: $this.data( 'uploader-button-text' ) + }, + multiple: false + }); } - custom_metadata_file_frame = wp.media.frames.file_frame = wp.media({ - title: $this.data( 'uploader-title' ), - button: { - text: $this.data( 'uploader-button-text' ) - }, - multiple: false - }); + // unbind prior events first + custom_metadata_file_frame.off( 'select' ); custom_metadata_file_frame.on( 'select', function() { attachment = custom_metadata_file_frame.state().get( 'selection' ).first().toJSON(); @@ -138,6 +149,7 @@ // init link fields var custom_metadata_link_selector_is_open = false; var custom_metadata_link_selector_target = null; + $custom_metadata_field.on( 'click.custom_metadata', '.custom-metadata-link-button', function(e){ e.preventDefault(); custom_metadata_link_selector_is_open = true; @@ -173,27 +185,131 @@ custom_metadata_link_selector_is_open = false; }); - // init the datepicker fields - $( '.custom-metadata-field.datepicker' ).find( 'input' ).datepicker({ - changeMonth: true, - changeYear: true + // First initialization of datepickers + bind_date_pickers(); + + // select2 + $custom_metadata_field.find( '.custom-metadata-select2' ).each(function(index) { + $(this).select2({ placeholder : $(this).attr('data-placeholder'), allowClear : true }); }); - // init the datetimepicker fields - $( '.custom-metadata-field.datetimepicker' ).find( 'input' ).datetimepicker({ - changeMonth: true, - changeYear: true + + /* MULTIFIELDS */ + + // reset field names, container ids, etc + var multifield_after_change = function( $container ) { + var $slug = $container.attr( 'data-slug' ), + $groupings = $container.find('.custom-metadata-multifield-grouping'); + + $groupings = $container.find('.custom-metadata-multifield-grouping'); + + $.each( $groupings, function( i, grouping ){ + + var $grouping = $( grouping ), + $fields = $grouping.find('.custom-metadata-field'), + num = i + 1; + + $grouping.attr( 'id', $slug + '-' + num ); + + $.each( $fields, function( j, field ){ + var $field = $( field ), + $field_slug = $field.attr( 'data-slug' ), + $label = $field.find( 'label' ), + $div = $field.find( 'div' ), + $field_inputs = $field.find( ':input:not(:button)' ), + $field_id = $field_slug + '-' + num; + + $label.attr( 'for', $field_id ); + $div.attr( 'id', $field_id + '-1' ).attr( 'class', $field_id ); + + $.each( $field_inputs, function( k, field_input ){ + + var $field_input = $( field_input ); + + if ( ! _.isEmpty( $field_input.attr( 'id' ) ) ) { + $field_input.attr( 'id', $field_id ); + } + + if ( ! _.isEmpty( $field_input.attr( 'name' ) ) ) { + $field_input.attr( 'name', $slug + '[' + i + '][' + $field_slug + ']'); + } + }); + + }); + + }); + }; + + // Adds sortable functionality to multifields + $('.custom-metadata-multifield').sortable({ + items: '.custom-metadata-multifield-grouping', + handle: '.sort-handle' }); - // init the timepicker fields - $( '.custom-metadata-field.timepicker' ).find( 'input' ).timepicker({ - changeMonth: true, - changeYear: true + $('.custom-metadata-multifield').sortable().bind('sortupdate', function(e) { + multifield_after_change( $(this) ); }); - // select2 - $custom_metadata_field.find( '.custom-metadata-select2' ).each(function(index) { - $(this).select2(); + // adding multifields + $custom_metadata_multifield.on( 'click.custom_metadata', '.custom-metadata-multifield-add', function(e){ + e.preventDefault(); + + destroy_date_pickers(); + + var $this = $( this ), + $container = $this.parents('.custom-metadata-multifield'), + $elements = $container.find('.custom-metadata-multifield-grouping'), + $element_to_clone = $elements.first(), + $clone = $element_to_clone.clone(true); + + $clone.find( ':input:not(:button)' ).val(''); + $clone.insertAfter( $elements.last() ).hide(); + + multifield_after_change( $container ); + bind_date_pickers(); + + $clone.fadeIn(); + + }); + + // cloning multifields + $custom_metadata_multifield.on( 'click.custom_metadata', '.custom-metadata-multifield-clone', function(e){ + + e.preventDefault(); + + destroy_date_pickers(); + + var $this = $( this ), + $element_to_clone = $this.parents('.custom-metadata-multifield-grouping'), + $container = $this.parents('.custom-metadata-multifield'), + $clone = $element_to_clone.clone(true); + + $clone.insertAfter( $element_to_clone ).hide(); + + multifield_after_change( $container ); + bind_date_pickers(); + + $clone.fadeIn(); + + }); + + // deleting multifields + $custom_metadata_multifield.on( 'click.custom_metadata', '.custom-metadata-multifield-delete', function(e){ + e.preventDefault(); + destroy_date_pickers(); + var $this = $( this ), + $element_to_delete = $this.parents('.custom-metadata-multifield-grouping'), + $container = $this.parents('.custom-metadata-multifield'); + + if( $container.find('.custom-metadata-multifield-grouping').length === 1 ) { + $element_to_delete.find( ':input:not(:button)' ).val(''); + } else { + $element_to_delete.fadeOut('normal', function(){ + $(this).remove(); + multifield_after_change( $container ); + }); + } + bind_date_pickers(); }); }); diff --git a/js/jquery.sortable.js b/js/jquery.sortable.js new file mode 100644 index 0000000..350d172 --- /dev/null +++ b/js/jquery.sortable.js @@ -0,0 +1,85 @@ +/* + * HTML5 Sortable jQuery Plugin + * http://farhadi.ir/projects/html5sortable + * + * Copyright 2012, Ali Farhadi + * Released under the MIT license. + */ +(function($) { +var dragging, placeholders = $(); +$.fn.sortable = function(options) { + var method = String(options); + options = $.extend({ + connectWith: false + }, options); + return this.each(function() { + if (/^enable|disable|destroy$/.test(method)) { + var items = $(this).children($(this).data('items')).attr('draggable', method == 'enable'); + if (method == 'destroy') { + items.add(this).removeData('connectWith items') + .off('dragstart.h5s dragend.h5s selectstart.h5s dragover.h5s dragenter.h5s drop.h5s'); + } + return; + } + var isHandle, index, items = $(this).children(options.items); + var placeholder = $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="sortable-placeholder">'); + items.find(options.handle).mousedown(function() { + isHandle = true; + }).mouseup(function() { + isHandle = false; + }); + $(this).data('items', options.items) + placeholders = placeholders.add(placeholder); + if (options.connectWith) { + $(options.connectWith).add(this).data('connectWith', options.connectWith); + } + items.attr('draggable', 'true').on('dragstart.h5s', function(e) { + if (options.handle && !isHandle) { + return false; + } + isHandle = false; + var dt = e.originalEvent.dataTransfer; + dt.effectAllowed = 'move'; + dt.setData('Text', 'dummy'); + index = (dragging = $(this)).addClass('sortable-dragging').index(); + }).on('dragend.h5s', function() { + if (!dragging) { + return; + } + dragging.removeClass('sortable-dragging').show(); + placeholders.detach(); + if (index != dragging.index()) { + dragging.parent().trigger('sortupdate', {item: dragging}); + } + dragging = null; + }).not('a[href], img').on('selectstart.h5s', function() { + this.dragDrop && this.dragDrop(); + return false; + }).end().add([this, placeholder]).on('dragover.h5s dragenter.h5s drop.h5s', function(e) { + if (!items.is(dragging) && options.connectWith !== $(dragging).parent().data('connectWith')) { + return true; + } + if (e.type == 'drop') { + e.stopPropagation(); + placeholders.filter(':visible').after(dragging); + dragging.trigger('dragend.h5s'); + return false; + } + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = 'move'; + if (items.is(this)) { + if (options.forcePlaceholderSize) { + placeholder.height(dragging.outerHeight()); + } + dragging.hide(); + $(this)[placeholder.index() < $(this).index() ? 'after' : 'before'](placeholder); + placeholders.not(placeholder).detach(); + } else if (!placeholders.is(this) && !$(this).children(options.items).length) { + placeholders.detach(); + $(this).append(placeholder); + } + return false; + }); + }); +}; +})(jQuery);