diff --git a/assets/js/src/document-library-grid.js b/assets/js/src/document-library-grid.js new file mode 100644 index 0000000..b52d9ec --- /dev/null +++ b/assets/js/src/document-library-grid.js @@ -0,0 +1,201 @@ +/** + * Document Library Lite – grid layout. + */ +(function ($) { + $(document).ready(function () { + const params = window.document_library_grid_params || {}; + + /** + * Tiny debounce helper. + */ + function debounce(fn, wait) { + let timer; + return function () { + const context = this; + const args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(context, args); + }, wait); + }; + } + + /** + * Build the request params for a container, honouring the active filter. + * + * The active category filter and the free-text search are mutually exclusive: when a category is + * active (set by clicking a category link), the search box only displays its name and is not sent + * as a keyword. + * + * @param {jQuery} $container + * @param {number} page + * @return {Object} + */ + function requestParams($container, page) { + const category = $container.attr("data-active-category") || ""; + const search = category + ? "" + : $container.find(".dlp-grid-search-input").val() || ""; + const length = $container.find(".dlp-grid-length-select").val() || ""; + + return { + category: category, + search_query: search, + length: length, + page_number: page, + }; + } + + /** + * Load a page of grid cards over AJAX and swap the relevant parts into the container. + * + * @param {jQuery} $container The .dlp-grid-container element. + * @param {number} page The 1-based page number to load. + */ + function loadPage($container, page) { + if (!params.ajax_url) { + return; + } + + $container.addClass("dlp-grid-loading"); + + $.ajax({ + url: params.ajax_url, + type: "POST", + data: $.extend( + { + action: params.ajax_action, + grid_id: $container.attr("id"), + _ajax_nonce: params.ajax_nonce, + }, + requestParams($container, page) + ), + }) + .done(function (response) { + if (!response) { + return; + } + + if (response.grid) { + $container + .find(".dlp-grid-documents") + .replaceWith(response.grid); + } + + if (response.totals) { + $container.find(".dlp-grid-totals").replaceWith(response.totals); + } + + if (response.pagination) { + $container + .find(".dlp-grid-pagination") + .replaceWith(response.pagination); + } + + // Scroll back to the top of the grid on page change. + const scrollOffset = $container.data("scroll-offset"); + if (scrollOffset !== false && typeof scrollOffset !== "undefined") { + const adminBar = $("#wpadminbar"); + let top = + $container.offset().top - (parseInt(scrollOffset, 10) || 0); + if (adminBar.length) { + top -= adminBar.outerHeight() || 32; + } + $("html, body").animate({ scrollTop: top }, 300); + } + + $container.trigger("grid-loaded"); + }) + .always(function () { + $container.removeClass("dlp-grid-loading"); + }); + } + + // Pagination clicks. + $(document).on("click", ".dlp-grid-paginate-button", function () { + const $button = $(this); + + if ($button.hasClass("disabled") || $button.hasClass("current")) { + return; + } + + const $container = $button.closest(".dlp-grid-container"); + loadPage($container, $button.data("page-number")); + }); + + // Free-text search (debounced). Typing clears any active category filter. + $(document).on( + "input", + ".dlp-grid-search-input", + debounce(function () { + const $container = $(this).closest(".dlp-grid-container"); + $container.attr("data-active-category", ""); + loadPage($container, 1); + }, 300) + ); + + // Page-length change → reload from page 1 with the new documents-per-page. + $(document).on("change", ".dlp-grid-length-select", function () { + const $container = $(this).closest(".dlp-grid-container"); + loadPage($container, 1); + }); + + // Category link click → filter the grid by that category (only when search_on_click is enabled, in + // which case the link carries the .dlp-grid-category-link class). The search box is left untouched. + $(document).on("click", ".dlp-grid-category-link", function (event) { + event.preventDefault(); + + const $link = $(this); + const $container = $link.closest(".dlp-grid-container"); + + $container.attr("data-active-category", $link.data("slug")); + loadPage($container, 1); + }); + + // Lightbox (PhotoSwipe) on the featured image. + $(document).on( + "click", + ".dlp-grid-card-featured-img a.dlw-lightbox", + function (event) { + event.preventDefault(); + event.stopPropagation(); + + const pswpElement = $(".pswp")[0]; + const $img = $(this).find("img"); + + if (!pswpElement || $img.length < 1) { + return; + } + + const items = [ + { + src: $img.attr("data-large_image"), + w: $img.attr("data-large_image_width"), + h: $img.attr("data-large_image_height"), + title: + $img.attr("data-caption") && $img.attr("data-caption").length + ? $img.attr("data-caption") + : $img.attr("title"), + }, + ]; + + const photoswipe = new PhotoSwipe( + pswpElement, + PhotoSwipeUI_Default, + items, + { + index: 0, + shareEl: false, + closeOnScroll: false, + history: false, + hideAnimationDuration: 0, + showAnimationDuration: 0, + } + ); + photoswipe.init(); + + return false; + } + ); + }); +})(jQuery); diff --git a/assets/scss/document-library-grid.scss b/assets/scss/document-library-grid.scss new file mode 100644 index 0000000..b8953b1 --- /dev/null +++ b/assets/scss/document-library-grid.scss @@ -0,0 +1,284 @@ +/** + * Document Library Lite – grid layout styles. + */ + +.dlp-grid-container { + margin-top: 1.2em; + margin-bottom: 2.2em; + + &.dlp-grid-loading { + opacity: 0.6; + pointer-events: none; + } + + .dlp-grid-header { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: space-between; + align-items: start; + margin-bottom: 1rem; + color: #333333; + } + + .dlp-grid-length-select { + height: auto; + line-height: 1.25; + margin-bottom: 0; + padding: .2em .5em; + width: auto; + border: 1px solid #aaa; + border-radius: 3px; + background-color: transparent; + } + + .dlp-grid-search-input { + // Matches the table layout's .dataTables_filter input styling. + box-sizing: border-box; + border: 1px solid #aaa; + border-radius: 3px; + background-color: transparent; + margin: 0 0 0 3px; + padding: 0.3em 0.5em; + width: auto; + height: auto; + line-height: 1.25; + font-size: inherit; + } + + .dlp-grid-documents { + display: grid; + gap: 1.5rem; + grid-template-columns: repeat(4, minmax(0, 1fr)); + + &.columns-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + + &.columns-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + &.columns-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + &.columns-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + &.columns-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } + + &.columns-6 { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } + } + + @media (max-width: 960px) { + .dlp-grid-documents { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + } + } + + @media (max-width: 600px) { + .dlp-grid-documents { + grid-template-columns: repeat(1, minmax(0, 1fr)) !important; + } + } +} + +.dlp-grid-empty { + grid-column: 1 / -1; + margin: 0; + padding: 1rem 0; +} + +.dlp-grid-card { + display: flex; + background: #fff; + border: 1px solid #e2e2e2; + border-radius: 6px; + overflow: hidden; + + .dlp-grid-card-inner { + display: flex; + flex-direction: column; + width: 100%; + } + + .dlp-grid-card-featured-img { + a { + display: block; + } + + img { + display: block; + width: 100%; + height: auto; + aspect-ratio: 4 / 3; + object-fit: cover; + } + } + + .dlp-grid-card-featured-icon { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + background: #f6f7f7; + + svg { + width: 48px; + height: 48px; + } + } + + .dlp-grid-card-content { + display: flex; + flex: 1; + flex-direction: column; + gap: 0.5rem; + padding: 1rem; + } + + .dlp-grid-card-info { + display:flex; + align-items: center; + flex-wrap: nowrap; + justify-content: flex-start; + } + + .dlp-grid-card-categories { + margin-left: auto; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.03em; + opacity: 0.75; + + .dlp-grid-category-link { + cursor: pointer; + } + } + + .dlp-grid-card-title { + font-weight: 700; + } + + .dlp-grid-card-excerpt { + flex: 1; + font-size: 0.9rem; + } + + .dlp-grid-card-link { + margin-top: auto; + padding-top: 0.5rem; + } +} + +.dlp-grid-footer { + margin-top: 10px; + + // Clearfix for the floated info (left) and pagination (right). + &::after { + content: ""; + display: block; + clear: both; + } +} + +// Info text — "Showing 1 to 10 of 12 entries" (left). +.dlp-grid-totals { + clear: both; + float: left; + padding-top: 4px; + color: #333; +} + +// Pagination (right). Styling ported from DataTables so it matches the table layout exactly. +.dlp-grid-pagination { + float: right; + text-align: right; + color: #333; + + .dlp-grid-paginate-button { + box-sizing: border-box; + display: inline-block; + min-width: 1.5em; + padding: 0.3em 0.9em; + margin-left: 2px; + text-align: center; + text-decoration: none !important; + cursor: pointer; + color: #333 !important; + border: 1px solid transparent; + border-radius: 2px; + box-shadow: none; + + &:hover { + color: #fff !important; + border: 1px solid #111; + background: linear-gradient(to bottom, #585858 0%, #111 100%); + } + + &:active { + outline: none; + background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%); + box-shadow: inset 0 0 3px #111; + } + + &.current, + &.current:hover { + color: #333 !important; + border: 1px solid #979797; + background: linear-gradient(to bottom, #fff 0%, #dcdcdc 100%); + } + + &.disabled, + &.disabled:hover, + &.disabled:active { + cursor: default; + color: #666 !important; + border: 1px solid transparent; + background: transparent; + box-shadow: none; + } + } +} + +.document-library-button { + display: inline-flex !important; + align-items: center; + justify-content: center; + word-break: keep-all; + font-size: inherit; + min-height: 2em; + line-height: 1.9em; + padding: 0px 18px !important; + + &:hover { + text-decoration: none; + } + + &::after { + display: none; + } +} + +.dll-button-icon { + min-width: 18px; + min-height: 18px; + width: 18px; + height: 18px; + line-height: 1 !important; +} + +.dll-button-icon-text { + margin-right: 10px; +} + +.dll-file-icon { + width: 48px; + height: 48px; +} diff --git a/src/Admin/Settings_Tab/Display.php b/src/Admin/Settings_Tab/Display.php index 604411e..94c6697 100644 --- a/src/Admin/Settings_Tab/Display.php +++ b/src/Admin/Settings_Tab/Display.php @@ -54,8 +54,8 @@ public function register_settings() { // Table section Settings_API_Helper::add_settings_section( 'dlp_table', self::MENU_SLUG, __( 'Table', 'document-library-lite' ), '__return_false', $this->get_table_settings() ); - // Grid section - Pro Only - Settings_API_Helper::add_settings_section( 'dlp_grid', self::MENU_SLUG, __( 'Grid', 'document-library-lite' ), [ $this, 'display_grid_section' ], [] ); + // Grid section + Settings_API_Helper::add_settings_section( 'dlp_grid', self::MENU_SLUG, __( 'Grid', 'document-library-lite' ), [ $this, 'display_grid_section' ], $this->get_grid_settings() ); // Sorting section Settings_API_Helper::add_settings_section( 'dlp_sort_by', self::MENU_SLUG, __( 'Sorting', 'document-library-lite' ), '__return_false', $this->get_sort_by_settings() ); @@ -100,12 +100,39 @@ public function display_pro_only_section() { */ public function display_grid_section() { printf( - '
' . - esc_html__( 'Display your documents in a responsive grid layout. Control the grid content, clickable fields, columns, and customize the appearance to match your site design.', 'document-library-lite' ) . - '
' . - '%s
', - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - Lib_Util::barn2_link( 'wordpress-plugins/document-library-pro/?utm_source=settings&utm_medium=settings&utm_campaign=settingsinline&utm_content=dlw-settings', __( 'Pro version only', 'document-library-lite' ), true ) + '%s
', + esc_html__( 'Display your documents in a responsive grid layout. Set the default layout to Grid to enable it.', 'document-library-lite' ) + ); + } + + /** + * Get the Grid settings. + * + * @return array + */ + private function get_grid_settings() { + return Options::mark_readonly_settings( + [ + [ + 'id' => Options::SHORTCODE_OPTION_KEY . '[grid_content]', + 'title' => __( 'Grid content', 'document-library-lite' ), + 'type' => 'text', + 'desc' => __( 'Choose which information to display in the grid of documents.', 'document-library-lite' ) . ' ' . Lib_Util::barn2_link( 'kb/document-library-wordpress-documentation/#document-tables', '', true ), + 'default' => 'image,title,content,link', + ], + [ + 'id' => Options::SHORTCODE_OPTION_KEY . '[grid_columns]', + 'title' => __( 'Number of columns', 'document-library-lite' ), + 'type' => 'number', + 'class' => 'small-text', + 'desc' => __( 'The number of columns to display in the grid layout.', 'document-library-lite' ), + 'default' => 4, + 'custom_attributes' => [ + 'min' => 1, + 'max' => 6, + ], + ], + ] ); } @@ -162,13 +189,6 @@ private function get_table_settings() { 'desc' => __( 'Enter the fields to include in your document tables.', 'document-library-lite' ) . ' ' . Lib_Util::barn2_link( 'kb/document-library-wordpress-documentation/#document-tables', '', true ), 'default' => 'id,title,content,image,date,doc_categories,link', ], - [ - 'id' => Options::SHORTCODE_OPTION_KEY . '[image_size]', - 'title' => __( 'Image size', 'document-library-lite' ), - 'type' => 'text', - 'desc' => __( 'Enter WxH in pixels (e.g. 80x80).', 'document-library-lite' ) . ' ' . Lib_Util::barn2_link( 'kb/document-library-image-options/#image-size', '', true ), - 'default' => '70x70', - ], ] ); } diff --git a/src/Admin/Settings_Tab/General.php b/src/Admin/Settings_Tab/General.php index 3671ea6..aad512e 100644 --- a/src/Admin/Settings_Tab/General.php +++ b/src/Admin/Settings_Tab/General.php @@ -60,6 +60,9 @@ public function register_settings() { // Frontend Submission - Pro Only Settings_API_Helper::add_settings_section( 'dlp_frontend_submission', self::MENU_SLUG, __( 'Front end document submission', 'document-library-lite' ), [ $this, 'display_frontend_submission_section' ], [] ); + + // Lead Capture - Pro Only + Settings_API_Helper::add_settings_section( 'dlp_lead_capture', self::MENU_SLUG, __( 'Lead capture', 'document-library-lite' ), [ $this, 'display_lead_capture_section' ], [] ); } /** @@ -131,6 +134,22 @@ public function display_frontend_submission_section() { ); } + /** + * Output the Lead Capture section description. + */ + public function display_lead_capture_section() { + printf( + '' . + esc_html__( 'Require users to enter their email address before they can access document links.', 'document-library-lite' ) . + ' %s
' . + '%s
', + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + Lib_Util::barn2_link( 'kb/lead-capture/', __( 'Read more', 'document-library-lite' ), true ), + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + Lib_Util::barn2_link( 'wordpress-plugins/document-library-pro/?utm_source=settings&utm_medium=settings&utm_campaign=settingsinline&utm_content=dlw-settings', __( 'Pro version only', 'document-library-lite' ), true ) + ); + } + /** * Get the General settings. * diff --git a/src/Admin/Wizard/Steps/Layout.php b/src/Admin/Wizard/Steps/Layout.php index c6b1969..6736fcc 100644 --- a/src/Admin/Wizard/Steps/Layout.php +++ b/src/Admin/Wizard/Steps/Layout.php @@ -42,12 +42,6 @@ public function init() { public function setup_fields() { $fields = [ - 'columns' => [ - 'label' => __( 'Columns', 'document-library-lite' ), - 'description' => __( 'Enter the fields to include in your document tables.', 'document-library-lite' ) . ' ' . Lib_Util::barn2_link( 'kb/document-library-wordpress-documentation/#document-tables-tab', esc_html__( 'Read more', 'document-library-lite' ), true ), - 'type' => 'text', - 'value' => $this->values['columns'], - ], 'layout' => [ 'label' => __( 'Default layout', 'document-library-lite' ), 'type' => 'radio', @@ -59,10 +53,45 @@ public function setup_fields() { [ 'value' => 'grid', 'label' => __( 'Grid', 'document-library-lite' ), - ] + ], + ], + 'value' => $this->values['layout'], + ], + 'columns' => [ + 'label' => __( 'Columns', 'document-library-lite' ), + 'description' => __( 'Enter the fields to include in your document tables.', 'document-library-lite' ) . ' ' . Lib_Util::barn2_link( 'kb/document-library-wordpress-documentation/#document-tables', esc_html__( 'Read more', 'document-library-lite' ), true ), + 'type' => 'text', + 'value' => $this->values['columns'], + 'conditions' => [ + 'layout' => [ + 'op' => 'eq', + 'value' => 'table', + ], + ], + ], + 'grid_content' => [ + 'label' => __( 'Grid content', 'document-library-lite' ), + 'description' => __( 'Choose which information to display in the grid of documents.', 'document-library-lite' ) . ' ' . Lib_Util::barn2_link( 'kb/document-library-wordpress-documentation/#document-grids', '', true ), + 'type' => 'text', + 'value' => $this->values['grid_content'], + 'conditions' => [ + 'layout' => [ + 'op' => 'eq', + 'value' => 'grid', + ], + ], + ], + 'grid_columns' => [ + 'label' => __( 'Number of columns', 'document-library-lite' ), + 'type' => 'number', + 'description' => __( 'The number of columns to display in the grid layout.', 'document-library-lite' ), + 'value' => $this->values['grid_columns'], + 'conditions' => [ + 'layout' => [ + 'op' => 'eq', + 'value' => 'grid', + ], ], - 'value' => 'table', - 'premium' => true, ], 'folders' => [ 'title' => __( 'Folders', 'document-library-lite' ), @@ -72,9 +101,7 @@ public function setup_fields() { 'premium' => true, ], ]; - return $fields; - } /** @@ -82,11 +109,17 @@ public function setup_fields() { */ public function submit( array $values ) { - $columns = isset( $values['columns'] ) && ! empty( trim( $values['columns'] ) ) ? $values['columns'] : $this->values['columns']; + $layout = isset( $values['layout'] ) ? $values['layout'] : $this->values['layout']; + $columns = isset( $values['columns'] ) && ! empty( trim( $values['columns'] ) ) ? $values['columns'] : $this->values['columns']; + $grid_content = isset( $values['grid_content'] ) && ! empty( trim( $values['grid_content'] ) ) ? $values['grid_content'] : $this->values['grid_content']; + $grid_columns = isset( $values['grid_columns'] ) && is_numeric( $values['grid_columns'] ) ? $values['grid_columns'] : $this->values['grid_columns']; Options::update_shortcode_option( [ - 'columns' => $columns, + 'layout' => $layout, + 'columns' => $columns, + 'grid_content' => $grid_content, + 'grid_columns' => $grid_columns, ] ); diff --git a/src/Document_Library_Shortcode.php b/src/Document_Library_Shortcode.php index 805fd10..e8ed597 100644 --- a/src/Document_Library_Shortcode.php +++ b/src/Document_Library_Shortcode.php @@ -32,6 +32,13 @@ class Document_Library_Shortcode implements Registerable, Standard_Service { */ private static $script_params = []; + /** + * Tracks whether the global grid AJAX params have been localized. + * + * @var bool + */ + private static $grid_params_printed = false; + /** * {@inheritdoc} */ @@ -55,6 +62,11 @@ public function do_shortcode( $atts, $content = '' ) { // Store the configuration securely and get a unique ID $table_id = Config_Builder::store( $atts ); + // Render a grid instead of a table when the layout is set to 'grid'. + if ( isset( $atts['layout'] ) && $atts['layout'] === 'grid' ) { + return $this->do_grid_shortcode( $atts, $table_id ); + } + $table = new Simple_Document_Library( $atts, $table_id ); // Determine sort order - if not set or empty, use automatic based on sort_by @@ -102,6 +114,44 @@ public function do_shortcode( $atts, $content = '' ) { return ob_get_clean(); } + /** + * Handles rendering the document library as a grid. + * + * @param array $atts The parsed shortcode attributes. + * @param string $table_id The stored configuration ID. + * @return string The grid HTML output. + */ + private function do_grid_shortcode( $atts, $table_id ) { + $grid = new Simple_Document_Grid( $atts, $table_id ); + + if ( apply_filters( 'document_library_grid_load_scripts', true ) ) { + wp_enqueue_style( 'document-library-grid' ); + wp_enqueue_script( 'document-library-grid' ); + + // Localize the shared grid AJAX params once per page. + if ( ! self::$grid_params_printed ) { + wp_localize_script( + 'document-library-grid', + 'document_library_grid_params', + apply_filters( + 'document_library_grid_script_params', + [ + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'ajax_nonce' => wp_create_nonce( 'dll_load_grid' ), + 'ajax_action' => 'dll_load_grid', + ] + ) + ); + + self::$grid_params_printed = true; + } + } + + Frontend_Scripts::load_photoswipe_resources( $grid->args['lightbox'] ); + + return $grid->get_container(); + } + /** * Print script params in the footer before scripts are printed. */ diff --git a/src/Document_Query.php b/src/Document_Query.php new file mode 100644 index 0000000..f0c13c6 --- /dev/null +++ b/src/Document_Query.php @@ -0,0 +1,175 @@ + + * @license GPL-3.0 + * @copyright Barn2 Media Ltd + */ +trait Document_Query { + + /** + * Generate an inner array for the 'tax_query' arg in WP_Query. + * + * @param string $terms The list of terms as a string + * @param string $taxonomy The taxonomy name + * @param string $operator The SQL operator: IN, NOT IN, AND, etc + * @param string $field Add tax query by `term_id` or `slug`. Leave empty to auto-detect correct type + * @return array A tax query sub-array + */ + private function tax_query_item( $terms, $taxonomy, $operator = 'IN', $field = '' ) { + $and_relation = 'AND' === $operator; + + // comma-delimited list = OR, plus-delimited = AND + if ( ! is_array( $terms ) ) { + if ( false !== strpos( $terms, '+' ) ) { + $terms = explode( '+', $terms ); + $and_relation = true; + } else { + $terms = explode( ',', $terms ); + } + } + + // Do we have slugs or IDs? + if ( ! $field ) { + $using_term_ids = count( $terms ) === count( array_filter( $terms, 'is_numeric' ) ); + $field = $using_term_ids && ( ! isset( $this->args['numeric_terms'] ) || ! $this->args['numeric_terms'] ) ? 'term_id' : 'slug'; + } + + // Strange bug when using operator => 'AND' in individual tax queries - + // We need to separate these out into separate 'IN' arrays joined by and outer relation => 'AND' + if ( $and_relation && count( $terms ) > 1 ) { + $result = [ 'relation' => 'AND' ]; + + foreach ( $terms as $term ) { + $result[] = [ + 'taxonomy' => $taxonomy, + 'terms' => $term, + 'operator' => 'IN', + 'field' => $field, + ]; + } + + return $result; + } else { + return [ + 'taxonomy' => $taxonomy, + 'terms' => $terms, + 'operator' => $operator, + 'field' => $field, + ]; + } + } + + /** + * Run the posts query. + * + * @param array $query_args + * @return \WP_Post[] + */ + public function run_table_query( $query_args ) { + do_action( 'document_library_before_posts_query', $this ); + + $query = get_posts( $query_args ); + + do_action( 'document_library_after_posts_query', $this ); + + return $query; + } + + /** + * Build the posts query args, applying lazy-load / post-limit constraints. + * + * @param array $query_args + * @return array + */ + public function build_table_query( $query_args ) { + if ( $this->args['lazy_load'] ) { + // Ensure rows per page doesn't exceed post limit + $query_args['posts_per_page'] = $this->check_within_post_limit( $this->args['rows_per_page'] ); + $query_args['offset'] = $this->args['offset']; + } else { + $query_args['posts_per_page'] = $this->args['post_limit']; + } + return apply_filters( 'document_library_table_query_args', $query_args, $this ); + } + + /** + * Constrain a count to the configured post limit. + * + * @param int $count + * @return int + */ + private function check_within_post_limit( $count ) { + return is_int( $this->args['post_limit'] ) && $this->args['post_limit'] > 0 ? min( $this->args['post_limit'], $count ) : $count; + } + + /** + * Get the total number of posts matching the query. + * + * @return int + */ + public function get_total_posts() { + if ( is_numeric( $this->total_posts ) ) { + return $this->total_posts; + } + + $total = 0; + + $total_query = new \WP_Query( $this->build_post_totals_query( $this->post_args ) ); + $total = $total_query->post_count; + + $this->total_posts = $this->check_within_post_limit( $total ); + + return $this->total_posts; + } + + /** + * Build the query args used to count the total posts. + * + * @param array $args + * @return array + */ + private function build_post_totals_query( $args ) { + $query_args = $this->build_table_query( $args ); + $query_args['offset'] = 0; + $query_args['posts_per_page'] = -1; + $query_args['fields'] = 'ids'; + + return apply_filters( 'document_library_query_args', $query_args, $this ); + } + + /** + * Build the base post query args from the current settings. + */ + public function set_post_args() { + // Start building the args needed for our posts query + $this->post_args = [ + 'post_type' => Post_Type::POST_TYPE_SLUG, + // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page + 'posts_per_page' => apply_filters( 'document_library_table_post_limit', 1000 ), + 'post_status' => $this->args['status'], + 'order' => strtoupper( $this->args['sort_order'] ), + 'orderby' => $this->args['sort_by'], + 'suppress_filters' => false, // Ensure WPML filters run on this query + ]; + + // Add our doc_category if we have one. + if ( isset( $this->args['doc_category'] ) && strlen( $this->args['doc_category'] ) > 0 ) { + $this->post_args = array_merge( + $this->post_args, + [ 'tax_query' => [ $this->tax_query_item( $this->args['doc_category'], 'doc_categories' ) ] ] + ); + } + + if ( isset( $this->args['search_value'] ) && strlen( $this->args['search_value'] ) > 0 ) { + $this->post_args['s'] = $this->args['search_value']; + } + } +} diff --git a/src/Frontend_Scripts.php b/src/Frontend_Scripts.php index 464c3ab..daa1fe8 100644 --- a/src/Frontend_Scripts.php +++ b/src/Frontend_Scripts.php @@ -48,6 +48,7 @@ public function register_styles() { wp_register_style( 'jquery-datatables-dlw', plugins_url( 'assets/js/datatables/datatables.min.css', $this->plugin->get_file() ), [], self::DATATABLES_VERSION ); wp_register_style( 'document-library', plugins_url( 'assets/css/document-library-main.css', $this->plugin->get_file() ), [ 'jquery-datatables-dlw' ], $this->plugin->get_version() ); + wp_register_style( 'document-library-grid', plugins_url( 'assets/css/document-library-grid.css', $this->plugin->get_file() ), [], $this->plugin->get_version() ); wp_register_style( 'photoswipe', plugins_url( 'assets/js/photoswipe/photoswipe.min.css', $this->plugin->get_file() ), [], self::PHOTOSWIPE_VERSION ); wp_register_style( 'photoswipe-default-skin', plugins_url( 'assets/js/photoswipe/default-skin/default-skin.min.css', $this->plugin->get_file() ), [ 'photoswipe' ], self::PHOTOSWIPE_VERSION ); } @@ -60,6 +61,7 @@ public function register_scripts() { wp_register_script( 'jquery-datatables-dlw', plugins_url( "assets/js/datatables/datatables{$suffix}.js", $this->plugin->get_file() ), [ 'jquery' ], self::DATATABLES_VERSION, true ); wp_register_script( 'document-library', plugins_url( 'assets/js/document-library-main.js', $this->plugin->get_file() ), [ 'jquery', 'jquery-datatables-dlw' ], $this->plugin->get_version(), true ); + wp_register_script( 'document-library-grid', plugins_url( 'assets/js/document-library-grid.js', $this->plugin->get_file() ), [ 'jquery' ], $this->plugin->get_version(), true ); wp_register_script( 'photoswipe', plugins_url( 'assets/js/photoswipe/photoswipe.min.js', $this->plugin->get_file() ), [], self::PHOTOSWIPE_VERSION, true ); wp_register_script( 'photoswipe-ui-default', plugins_url( 'assets/js/photoswipe/photoswipe-ui-default.min.js', $this->plugin->get_file() ), [ 'photoswipe' ], self::PHOTOSWIPE_VERSION, true ); } diff --git a/src/Simple_Document_Grid.php b/src/Simple_Document_Grid.php new file mode 100644 index 0000000..7568fad --- /dev/null +++ b/src/Simple_Document_Grid.php @@ -0,0 +1,564 @@ + + * @license GPL-3.0 + * @copyright Barn2 Media Ltd + */ +class Simple_Document_Grid { + + use Document_Query; + + public $args = []; + public $post_args = []; + private $total_posts = null; + private $grid_id = null; + + /** + * The configured documents-per-page, captured before get_page_posts() mutates $args['rows_per_page']. + * + * @var int + */ + private $base_per_page = self::DEFAULT_PAGE_LENGTH; + + /** + * The fields that may be displayed on a grid card. 'id' and 'date' are intentionally excluded. + */ + const ALLOWED_FIELDS = [ 'title', 'content', 'image', 'doc_categories', 'link' ]; + const ALLOWED_FIELDS_DEFAULT = 'image,title,content,link'; + + /** + * The selectable page-length options, and the default when the configured value isn't one of them. + */ + const PAGE_LENGTHS = [ 10, 25, 50, 100 ]; + const DEFAULT_PAGE_LENGTH = 10; + + /** + * Constructor. + * + * @param array $args The shortcode/settings arguments. + * @param string $grid_id A unique ID for this grid (the stored config ID). + */ + public function __construct( $args, $grid_id = null ) { + $this->args = $this->validate_options( $args ); + $this->grid_id = $grid_id ? $grid_id : 'document-library-grid'; + + // Capture the configured page length now: get_page_posts() later overwrites $args['rows_per_page']. + $this->base_per_page = isset( $this->args['rows_per_page'] ) ? (int) $this->args['rows_per_page'] : self::DEFAULT_PAGE_LENGTH; + + $this->set_post_args(); + } + + /** + * Validate the incoming options. + * + * @param array $args + * @return array + */ + public function validate_options( $args ) { + $boolean_options = [ 'lightbox', 'link_icon', 'link_target', 'text_links_new_tab' ]; + + foreach ( $boolean_options as $option ) { + $args[ $option ] = isset( $args[ $option ] ) ? filter_var( $args[ $option ], FILTER_VALIDATE_BOOLEAN ) : false; + } + + $valid_post_statuses = [ 'publish', 'pending', 'draft', 'future', 'any' ]; + $args['status'] = isset( $args['status'] ) && in_array( $args['status'], $valid_post_statuses, true ) ? $args['status'] : 'publish'; + + $args['grid_columns'] = isset( $args['grid_columns'] ) ? max( 1, min( 6, (int) $args['grid_columns'] ) ) : 4; + + // Defaults to true (it is a general shortcode default) rather than the generic "missing => false". + $args['search_on_click'] = isset( $args['search_on_click'] ) ? filter_var( $args['search_on_click'], FILTER_VALIDATE_BOOLEAN ) : true; + + if ( empty( $args['grid_content'] ) ) { + $args['grid_content'] = self::ALLOWED_FIELDS_DEFAULT; + } + + return $args; + } + + /** + * Parse the grid_content setting into a validated list of fields to render on each card. + * + * Mirrors Simple_Document_Library::get_columns(): comma-separated, trimmed, lowercased, then + * intersected with the allowed grid fields. Falls back to the default set when empty/invalid. + * + * @return string[] + */ + public function get_grid_fields() { + $raw = isset( $this->args['grid_content'] ) ? $this->args['grid_content'] : self::ALLOWED_FIELDS_DEFAULT; + + $fields = array_filter( array_map( 'trim', explode( ',', strtolower( $raw ) ) ) ); + $fields = array_values( array_intersect( $fields, self::ALLOWED_FIELDS ) ); + + if ( empty( $fields ) ) { + $fields = explode( ',', self::ALLOWED_FIELDS_DEFAULT ); + } + + return $fields; + } + + /** + * Number of documents shown per grid page. + * + * @return int + */ + public function get_per_page() { + $lengths = $this->get_page_lengths(); + $requested = isset( $this->args['requested_length'] ) ? (int) $this->args['requested_length'] : $this->base_per_page; + + // A valid numeric selection always wins (lets a rows_per_page = -1 site still pick 10/25/50/100). + if ( $requested > 0 && in_array( $requested, $lengths, true ) ) { + return $requested; + } + + // "All" — show every document on one page (only when the site is configured with rows_per_page = -1; + // the AJAX handler never sets a negative requested_length). + if ( -1 === $requested || -1 === $this->base_per_page ) { + return -1; + } + + return ( $this->base_per_page > 0 && in_array( $this->base_per_page, $lengths, true ) ) + ? $this->base_per_page + : self::DEFAULT_PAGE_LENGTH; + } + + /** + * The selectable page-length options: the defaults plus the configured value when it isn't one of them, + * inserted in ascending order (e.g. rows_per_page=8 => [8,10,25,50,100]). + * + * @return int[] + */ + public function get_page_lengths() { + $lengths = self::PAGE_LENGTHS; + + if ( $this->base_per_page > 0 && ! in_array( $this->base_per_page, $lengths, true ) ) { + $lengths[] = $this->base_per_page; + sort( $lengths, SORT_NUMERIC ); + } + + return $lengths; + } + + /** + * Query the documents for a given page. + * + * @param int $page The 1-based page number. + * @return \WP_Post[] + */ + public function get_page_posts( $page = 1 ) { + $page = max( 1, (int) $page ); + $per_page = $this->get_per_page(); + + // Force offset-based paging for the grid regardless of the table lazy-load setting. + $this->args['lazy_load'] = true; + + if ( $per_page < 1 ) { + // "All" — fetch every document on a single page. + $this->args['rows_per_page'] = -1; + $this->args['offset'] = 0; + } else { + $this->args['rows_per_page'] = $per_page; + $this->args['offset'] = ( $page - 1 ) * $per_page; + } + + $this->set_post_args(); + + return $this->run_table_query( $this->build_table_query( $this->post_args ) ); + } + + /** + * Render the cards for a given page (the inner '.dlp-grid-documents' markup). + * + * @param int $page + * @return string + */ + public function get_grid_html( $page = 1 ) { + $posts = $this->get_page_posts( $page ); + $grid_class = sprintf( 'grid-columns columns-%d', (int) $this->args['grid_columns'] ); + $fields = $this->get_grid_fields(); + + $cards = ''; + + if ( ! empty( $posts ) && is_array( $posts ) ) { + foreach ( $posts as $_post ) { + setup_postdata( $_post ); + $cards .= $this->get_card( $_post, $fields ); + } + wp_reset_postdata(); + } else { + $cards = sprintf( '%s
', esc_html__( 'No matching documents', 'document-library-lite' ) ); + } + + return sprintf( '