Plugin Directory

Changeset 3730982


Ignore:
Timestamp:
10/06/2026 02:12:29 PM (3 weeks ago)
Author:
farndesign
Message:

Update to version 4.2.0 from GitHub

Location:
easynewsletter
Files:
22 added
38 edited
1 copied

Legend:

Unmodified
Added
Removed
  • easynewsletter/tags/4.2.0/README.txt

    r3710879 r3730982  
    44Requires at least: 6.0
    55Tested up to: 7.0
    6 Stable tag: 4.1.4
     6Stable tag: 4.2.0
    77Requires PHP: 8.0
    88License: GPLv3 or later
    … …  
    7272== Changelog ==
    7373
     74= 4.2.0 =
     75Fix - Automatic image sizing in responsive emails.
     76New - Added fluid block layouts and signup forms.
     77New - Introduced theme-based newsletter styling pipeline.
     78New - Added inline registration form layout.
     79New - Configurable signup checkbox text.
     80
    7481= 4.1.4 =
    7582New - Added newsletter email rendering pipeline.
  • easynewsletter/tags/4.2.0/easynewsletter.php

    r3710879 r3730982  
    55Plugin URI: https://www.farnlabs.de/
    66Description: Provides a Newsletter functionality with complete WordPress integration.
    7 Version: 4.1.4
     7Version: 4.2.0
    88Author: FARN – digital brand design
    99Author URI: https://www.farn.de/
  • easynewsletter/tags/4.2.0/src/core/databaseConnector.php

    r3710879 r3730982  
    3131                'intervalInSeconds' => '300',
    3232                'signupFormFields' => '', // will be set as a serialized array('en_eMailAddress') in the constructor.
     33                'signupCheckboxText' => '', // Empty uses the translated default checkbox label.
    3334                'activeNewsletter' => '',
    3435                'activeNewsletterID' => '',
    … …  
    4243                'subscriberCategory' => '', // will be set as a serialized array('default') in the constructor.
    4344                'subscriberRole' => '', // will be set as a serialized array('default') in the constructor.
     45        'newsletterUseThemeStyles' => 'false', // Upgrades always opt out.
     46        'newsletterCSSFile' => '',
     47        'newsletterExcludedStyles' => '[]',
    4448                'newsletterCSS' => '',
    4549        'newsletterContentWidth' => '800',
     50        'newsletterWideWidth' => '1100',
     51        'newsletterSideSpacing' => '0', // Preserve existing installations.
    4652                'addedUserRoleKey' => 'en_user_subscriber',
    4753                "registrationPageID" => "",
    … …  
    125131                $results = array_column($results,0);
    126132
     133        $freshInstallation = empty($results);
    127134                foreach ($this->defaultSettings as $key => $value) {
     135            if ($key === 'newsletterUseThemeStyles' && $freshInstallation) { $value = 'true'; }
     136            if ($key === 'newsletterSideSpacing' && $freshInstallation) { $value = '20'; }
    128137                        if(!in_array($key, $results)){
    129138                                $this->wpdb->insert($this->tableName,
  • easynewsletter/tags/4.2.0/src/core/generateEmailHtml.php

    r3710879 r3730982  
    33
    44require_once __DIR__ . '/resources/vendor/cssin/CSSIN.php';
     5require_once __DIR__ . '/newsletterThemeStyles.php';
     6require_once __DIR__ . '/newsletterStyleInliner.php';
     7require_once __DIR__ . '/newsletterBlockLayout.php';
    58
    69use CSSIN;
    … …  
    1013class generateEmailHtml {
    1114        public static function generate($postID) : string{
     15        $extendedStyles = newsletterThemeStyles::extended();
     16        $baseCSS = file_get_contents(__DIR__ . '/resources/newsletterBaseStyle.css');
     17        if ($extendedStyles) {
     18            // Let the theme's inherited font size reach the email wrapper.
     19            $baseCSS = str_replace(['font-size: 16px!important;', 'line-height: 1.3;'], '', $baseCSS);
     20        }
     21        $additionalCSS = $extendedStyles ? newsletterThemeStyles::css((int)$postID) : '';
    1222        $contentWidth = self::contentWidth(databaseConnector::instance()->getSettingFromDB("newsletterContentWidth"));
    1323                $enl_mailcontent_head = '<!DOCTYPE html><html lang="de">
    … …  
    2030                        <style type="text/css">
    2131                                '.file_get_contents( __DIR__ . "/resources/newsletterBlocklibraryStyle.css" ).'
    22                                 '.file_get_contents( __DIR__ . "/resources/newsletterBaseStyle.css" ).'
     32                                '.$baseCSS.'
     33                '.$additionalCSS.'
    2334                                '.databaseConnector::instance()->getSettingFromDB("newsletterCSS").'
    2435                        </style>
    … …  
    6576                        $enl_full_mailcontent = $enl_mailcontent_head . $enl_mailcontent_body . $enl_mailcontent_foot;
    6677
     78            if ($extendedStyles) {
     79                // Normalize before variable resolution too: its legacy parser
     80                // must see ordinary root declarations outside conditional rules.
     81                $enl_full_mailcontent = preg_replace_callback('~(<style\b[^>]*>)(.*?)(</style>)~is', static function ($match) {
     82                    return $match[1] . newsletterStyleInliner::emailCSS($match[2]) . $match[3];
     83                }, $enl_full_mailcontent);
     84            }
     85
    6786                        /* Use CSSIN to inline all styles */
    6887                        $cssin                        = new CSSIN();
    69                         $enl_full_mailcontent_inlined = $cssin->inlineCSS( self::prepare($enl_full_mailcontent) );
     88                        $enl_full_mailcontent_inlined = $extendedStyles
     89                ? newsletterStyleInliner::inline(self::prepare($enl_full_mailcontent, false), $contentWidth)
     90                : $cssin->inlineCSS(self::prepare($enl_full_mailcontent));
    7091
    7192                        //return $enl_full_mailcontent;
    72                         return self::transform($enl_full_mailcontent_inlined, $contentWidth);
     93                        return self::transform($enl_full_mailcontent_inlined, $contentWidth, $extendedStyles, (int)(databaseConnector::instance()->getSettingFromDB('newsletterWideWidth') ?: 1100), self::sideSpacing(databaseConnector::instance()->getSettingFromDB('newsletterSideSpacing')));
    7394                } catch (\Throwable $e) {
    7495            Log::log("An error occurred while processing the mail content: " . $e->getMessage());
    … …  
    7798        }
    7899
     100    public static function sideSpacing($value): int {
     101        $valid = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 200]]);
     102        return $valid === false ? 0 : $valid;
     103    }
     104
    79105    private int $layoutWidth = 800;
     106    private float $rootFont = 16;
    80107    private ?\DOMElement $layoutRoot = null;
    81108    public static function contentWidth($value): int {
    … …  
    99126    private static function length(string $value, float $reference, float $font = 16): ?float {
    100127        $value = self::plain($value);
     128        if (preg_match('/^(calc|clamp|min|max)\(/i', $value)) {
     129            return newsletterCSSLength::pixels($value, $reference, $font);
     130        }
    101131        if (!preg_match('/^(\d+(?:\.\d+)?)(px|pt|%|em|rem)?$/i', $value, $match)) { return null; }
    102132        $number = (float)$match[1];
    … …  
    203233
    204234    /** Remember author intent before global image CSS is inlined. */
    205     public static function prepare(string $html): string {
     235    public static function prepare(string $html, bool $convertColumns = true): string {
    206236        if (trim($html) === '') { return $html; }
    207237        $dom = self::load($html);
    208238        self::resolveDocumentVariables($dom);
     239        foreach ((new \DOMXPath($dom))->query('//div[contains(concat(" ",normalize-space(@class)," ")," wp-block-column ")]') as $column) {
     240            $author = self::styles($column->getAttribute('style'));
     241            if (isset($author['flex-basis'])) { $column->setAttribute('data-en-author-basis', $author['flex-basis']); }
     242        }
     243        foreach ((new \DOMXPath($dom))->query('//div[contains(concat(" ",normalize-space(@class)," ")," wp-block-group ")]') as $group) {
     244            $padding = array_filter(self::styles($group->getAttribute('style')), static function ($name) { return str_starts_with($name, 'padding'); }, ARRAY_FILTER_USE_KEY);
     245            if ($padding) { $group->setAttribute('data-en-author-padding', json_encode($padding)); }
     246        }
     247        if ($convertColumns) { self::prepareColumns($dom); }
     248        foreach ($dom->getElementsByTagName('img') as $image) {
     249            $styles = self::styles($image->getAttribute('style'));
     250            $intent = [];
     251            foreach (['width', 'height', 'aspect-ratio', 'object-fit', 'object-position'] as $key) {
     252                if (isset($styles[$key])) { $intent[$key] = $styles[$key]; }
     253            }
     254            // Older Gutenberg versions put intentional dimensions in attributes.
     255            $parent = $image->parentNode;
     256            while ($parent instanceof \DOMElement && in_array($parent->tagName, ['a', 'figure', 'div'], true)) {
     257                if (preg_match('/\bis-resized\b/', $parent->getAttribute('class'))) {
     258                    foreach (['width', 'height'] as $key) {
     259                        if (!isset($intent[$key]) && preg_match('/^[1-9]\d*$/', $image->getAttribute($key))) {
     260                            $intent[$key] = $image->getAttribute($key) . 'px';
     261                        }
     262                    }
     263                    break;
     264                }
     265                $parent = $parent->parentNode;
     266            }
     267            $image->setAttribute('data-en-image-intent', json_encode($intent));
     268        }
     269        return $dom->saveHTML();
     270    }
     271
     272    private static function prepareColumns(\DOMDocument $dom): void {
    209273        // Work from inside out so nested column groups keep their own row/gap.
    210274        $groups = (new \DOMXPath($dom))->query('//div[contains(concat(" ", normalize-space(@class), " "), " wp-block-columns ")]');
    211275        foreach (array_reverse(iterator_to_array($groups)) as $group) {
    212276            $groupStyles = self::styles($group->getAttribute('style'));
    213             $gap = preg_split('/\s+/', self::plain($groupStyles['gap'] ?? '40px'));
     277            $gap = array_values(array_filter(newsletterStyleInliner::split(self::plain($groupStyles['gap'] ?? '40px'), ' '), 'strlen'));
    214278            $columnGap = $groupStyles['column-gap'] ?? ($gap[1] ?? $gap[0]);
    215279            $rowGap = $groupStyles['row-gap'] ?? $gap[0];
    … …  
    239303                foreach ($column->attributes as $attribute) { $cell->setAttribute($attribute->name, $attribute->value); }
    240304                $css = self::styles($cell->getAttribute('style'));
    241                 $cell->setAttribute('data-en-column-basis', $css['flex-basis'] ?? $css['width'] ?? '');
     305                $cell->setAttribute('data-en-column-basis', $cell->getAttribute('data-en-author-basis') ?: ($css['flex-basis'] ?? $css['width'] ?? ''));
     306                $cell->setAttribute('data-en-column-grow', $cell->hasAttribute('data-en-author-basis') ? '0' : ($css['flex-grow'] ?? '1'));
     307                $cell->removeAttribute('data-en-author-basis');
    242308                unset($css['flex-basis']);
     309                $alignment = $groupStyles['align-items'] ?? 'top';
     310                foreach (['top', 'center', 'bottom'] as $position) {
     311                    if (preg_match('/\bis-vertically-aligned-' . $position . '\b/', $group->getAttribute('class'))) { $alignment = $position; }
     312                }
     313                // A per-column choice takes precedence over the group choice.
     314                $alignment = $css['align-self'] ?? $alignment;
     315                foreach (['top', 'center', 'bottom'] as $position) {
     316                    if (preg_match('/\bis-vertically-aligned-' . $position . '\b/', $column->getAttribute('class'))) { $alignment = $position; }
     317                }
     318                $alignment = match ($alignment) { 'center' => 'middle', 'bottom', 'flex-end', 'end' => 'bottom', default => 'top' };
     319                $css['vertical-align'] = $alignment;
     320                $cell->setAttribute('valign', $alignment);
    243321                $cell->setAttribute('style', \CSSIN::arrayToStyle($css));
    244322                while ($column->firstChild) { $cell->appendChild($column->firstChild); }
    … …  
    247325            $group->parentNode->replaceChild($table, $group);
    248326        }
    249         foreach ($dom->getElementsByTagName('img') as $image) {
    250             $styles = self::styles($image->getAttribute('style'));
    251             $intent = [];
    252             foreach (['width', 'height', 'aspect-ratio', 'object-fit', 'object-position'] as $key) {
    253                 if (isset($styles[$key])) { $intent[$key] = $styles[$key]; }
    254             }
    255             // Older Gutenberg versions put intentional dimensions in attributes.
    256             $parent = $image->parentNode;
    257             while ($parent instanceof \DOMElement && in_array($parent->tagName, ['a', 'figure', 'div'], true)) {
    258                 if (preg_match('/\bis-resized\b/', $parent->getAttribute('class'))) {
    259                     foreach (['width', 'height'] as $key) {
    260                         if (!isset($intent[$key]) && preg_match('/^[1-9]\d*$/', $image->getAttribute($key))) {
    261                             $intent[$key] = $image->getAttribute($key) . 'px';
    262                         }
    263                     }
    264                     break;
    265                 }
    266                 $parent = $parent->parentNode;
    267             }
    268             $image->setAttribute('data-en-image-intent', json_encode($intent));
    269         }
    270         return $dom->saveHTML();
    271     }
    272 
    273     public static function transform(string $html, int $width): string {
     327    }
     328
     329    public static function transform(string $html, int $width, bool $convertColumns = false, ?int $wideWidth = null, int $sideSpacing = 0): string {
    274330        if (trim($html) === '') { return $html; }
    275331        $dom = self::load($html);
     332        $fluidSections = $wideWidth !== null && newsletterBlockLayout::apply($dom, self::contentWidth($width), $wideWidth >= 200 && $wideWidth <= 2000 ? $wideWidth : 1100, self::sideSpacing($sideSpacing));
     333        if ($convertColumns) { self::prepareColumns($dom); }
    276334        self::wrapColumnPadding($dom);
    277335        self::centerImages($dom);
    … …  
    282340        }
    283341        $instance = new self();
    284         $instance->layoutWidth = self::contentWidth($width);
     342        $instance->layoutWidth = $fluidSections ? max(self::contentWidth($width), $wideWidth >= 200 && $wideWidth <= 2000 ? $wideWidth : 1100) : self::contentWidth($width);
     343        $rootStyles = self::styles($dom->documentElement->getAttribute('style'));
     344        $instance->rootFont = self::length($rootStyles['font-size'] ?? '', 16, 16) ?? 16;
    285345        $instance->layoutRoot = (new \DOMXPath($dom))->query('//table[contains(concat(" ", normalize-space(@class), " "), " enl-wrapper ")]')->item(0);
    286         if ($instance->layoutRoot) {
     346        if ($fluidSections && $instance->layoutRoot) {
     347            $instance->layoutWidth = (int)$instance->layoutRoot->getAttribute('data-en-reference-width');
     348            $instance->layoutRoot->removeAttribute('data-en-reference-width');
     349        }
     350        if ($instance->layoutRoot && !$fluidSections) {
    287351            $rootStyles = self::styles($instance->layoutRoot->getAttribute('style'));
    288352            $instance->layoutWidth = (int)round(min($instance->layoutWidth,
    … …  
    298362        }
    299363        $instance->walk($dom->documentElement, $instance->layoutWidth);
    300         $instance->outlookFrame();
     364        if (!$fluidSections) { $instance->outlookFrame(); }
     365        foreach ((new \DOMXPath($dom))->query('//*[@data-en-author-padding or @data-en-fluid-sections]') as $node) {
     366            $node->removeAttribute('data-en-author-padding'); $node->removeAttribute('data-en-fluid-sections');
     367        }
    301368        return $dom->saveHTML();
    302369    }
    … …  
    340407
    341408    private static function paddingPixels(array $styles, float $reference, float $font): string {
    342         $parts = preg_split('/\s+/', self::plain($styles['padding'] ?? '0'));
     409        $parts = array_values(array_filter(newsletterStyleInliner::split(self::plain($styles['padding'] ?? '0'), ' '), 'strlen'));
    343410        $values = [$parts[0], $parts[1] ?? $parts[0], $parts[2] ?? $parts[0], $parts[3] ?? $parts[1] ?? $parts[0]];
    344411        foreach (['top', 'right', 'bottom', 'left'] as $index => $side) {
    … …  
    443510        }
    444511        $styles = self::styles($node->getAttribute('style'));
     512        foreach ($styles as $name => &$value) {
     513            if (!str_starts_with($name, '--') && !in_array($name, ['font-family', 'content'], true)) {
     514                $value = newsletterCSSLength::resolveRem($value, $node->tagName === 'html' && $name === 'font-size' ? 16 : $this->rootFont);
     515            }
     516        }
     517        unset($value);
    445518        if ($node->tagName === 'table' && preg_match('/\bwp-block-columns\b/', $node->getAttribute('class'))) {
    446519            $styles['display'] = 'table';
    … …  
    448521        }
    449522        $font = self::length($styles['font-size'] ?? '', $font, $font) ?? $font;
     523        foreach ($styles as $property => $value) {
     524            if (!preg_match('/^(?:padding|margin)(?:-(?:top|right|bottom|left))?$|^(?:font-size|width|height|min-width|max-width|min-height|max-height)$/', $property)
     525                || !preg_match('/(?:calc|clamp|min|max)\(/i', $value)) { continue; }
     526            $parts = array_values(array_filter(newsletterStyleInliner::split(self::plain($value), ' '), 'strlen'));
     527            foreach ($parts as &$part) {
     528                $pixels = newsletterCSSLength::pixels($part, $available, $font, $this->layoutWidth);
     529                if ($pixels !== null) { $part = round($pixels, 3) . 'px'; }
     530            }
     531            unset($part);
     532            $styles[$property] = implode(' ', $parts);
     533        }
    450534        if (in_array($node->tagName, ['td', 'th'], true)) {
    451535            $paddingStyles = $styles;
     536            if ($node->hasAttribute('data-en-outlook-padding')) {
     537                $paddingStyles = json_decode($node->getAttribute('data-en-outlook-padding'), true) ?: [];
     538                $node->removeAttribute('data-en-outlook-padding');
     539            }
    452540            if (strpos(' ' . $node->getAttribute('class') . ' ', ' en-button-cell ') !== false) {
    453541                $link = $node->getElementsByTagName('a')->item(0);
    … …  
    486574        $width = $allocated ?? min($available, self::length($styles['width'] ?? $node->getAttribute('width'), $available, $font) ?? $available);
    487575        $width = min($width, self::length($styles['max-width'] ?? '', $available, $font) ?? $width);
    488         $padding = preg_split('/\s+/', self::plain($styles['padding'] ?? '0'));
     576        $padding = array_values(array_filter(newsletterStyleInliner::split(self::plain($styles['padding'] ?? '0'), ' '), 'strlen'));
    489577        $left = $styles['padding-left'] ?? ($padding[3] ?? $padding[1] ?? $padding[0]);
    490578        $right = $styles['padding-right'] ?? ($padding[1] ?? $padding[0]);
    … …  
    545633        $gap = $count > 1 ? min($gap, max(0, ($width - $count) / ($count - 1))) : 0;
    546634        $contentWidth = $width - $gap * ($count - 1);
    547         $weights = []; $used = 0; $auto = 0;
     635        $weights = []; $growth = []; $used = 0;
    548636        foreach ($columns as $column) {
    549637            $basis = self::length($column->getAttribute('data-en-column-basis'), $contentWidth, $font);
    550             $weights[] = $basis;
    551             if ($basis === null) { $auto++; } else { $used += $basis; }
    552         }
    553         foreach ($weights as &$weight) { $weight = $weight ?? max(1, $contentWidth - $used) / max(1, $auto); }
    554         unset($weight);
     638            $weights[] = $basis ?? 0;
     639            $growth[] = max(0, (float)$column->getAttribute('data-en-column-grow'));
     640            $used += $basis ?? 0;
     641        }
     642        $free = max(0, $contentWidth - $used);
     643        $totalGrowth = array_sum($growth);
     644        if ($totalGrowth > 0) {
     645            foreach ($weights as $index => &$weight) { $weight += $free * $growth[$index] / $totalGrowth; }
     646            unset($weight);
     647        }
    555648        $sum = array_sum($weights);
    556649        if ($sum <= 0) { $weights = array_fill(0, $count, 1); $sum = $count; }
    … …  
    568661            $cellWidth = $contentWidth * $fraction;
    569662            $cell->removeAttribute('data-en-column-basis');
     663            $cell->removeAttribute('data-en-column-grow');
    570664            // Numeric desktop fallback plus proportional widths for narrow clients.
    571665            // Gutters scale with the row when columns stay side by side.
    … …  
    624718        // Keep tighter author caps (e.g. width:100%; max-width:200px).
    625719        $styles['max-width'] = $styles['max-width'] ?? '100%';
     720        if (!$fixed && strpos($widthCSS, '%') === false) {
     721            // Automatic images must shrink through width itself: Thunderbird can
     722            // override max-width with its message stylesheet. Keep the computed
     723            // desktop size as a cap so small intrinsic images are not enlarged.
     724            $cap = self::plain($styles['max-width']);
     725            $styles['width'] = preg_match('/^(\d+(?:\.\d+)?)%$/', $cap, $match)
     726                ? min(100, (float)$match[1]) . '%' : '100%';
     727            $styles['max-width'] = $width . 'px';
     728        }
    626729        $styles['height'] = 'auto';
    627730        unset($styles['object-fit'], $styles['object-position'], $styles['aspect-ratio']);
  • easynewsletter/tags/4.2.0/src/core/menuPage.php

    r3696327 r3730982  
    5454                        'easyNewsletter',
    5555                        [$this, "menuPageContent"]);
     56        add_submenu_page(
     57            'easyNewsletter',
     58            __('Overview', 'easynewsletter'),
     59            __('Overview', 'easynewsletter'),
     60            'manage_options',
     61            'easyNewsletter',
     62            [$this, 'menuPageContent'],
     63            0
     64        );
    5665        }
    5766
    … …  
    118127        public function settingsPageContent(): void{
    119128                //add settingsPage.js script to this site
    120                 wp_enqueue_script( "en_settingsPageJS", plugins_url( 'resources/settingsPage.js', __FILE__ ) );
     129                wp_enqueue_script( "en_settingsPageJS", plugins_url( 'resources/settingsPage.js', __FILE__ ), ['jquery'], filemtime(__DIR__ . '/resources/settingsPage.js'), true );
    121130
    122131                include ('settingsPageContent.php');
    … …  
    135144                // styles for menu icon
    136145                wp_enqueue_style("en_menuIconCSS", plugins_url('resources/menuIcon.css', __FILE__ ));
    137                 wp_enqueue_style("en_admin", plugins_url('resources/admin.css', __FILE__ ));
     146                wp_enqueue_style("en_admin", plugins_url('resources/admin.css', __FILE__), [], filemtime(__DIR__ . '/resources/admin.css'));
    138147        }
    139148
  • easynewsletter/tags/4.2.0/src/core/newsletterPageTemplate.php

    r3710879 r3730982  
    4747        </nav>
    4848    </header>
    49     <iframe title="<?php echo $outlook ? 'Outlook-Simulation' : 'Standard-Vorschau'; ?>" sandbox="allow-popups allow-popups-to-escape-sandbox" srcdoc="<?php echo esc_attr($html); ?>"></iframe>
     49    <iframe title="<?php echo $outlook ? 'Outlook-Simulation' : 'Standard-Vorschau'; ?>" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" srcdoc="<?php echo esc_attr($html); ?>"></iframe>
    5050</body>
    5151</html>
  • easynewsletter/tags/4.2.0/src/core/newsletterPostType.php

    r3710879 r3730982  
    2828         */
    2929        public function init(): void {
     30        newsletterThemeStyles::init();
    3031                add_action('init', array($this, 'createNewsletterPostType'));
    3132                add_filter( 'manage_en_newsletters_posts_columns', array( $this, "addBackendNewsletterColumns" ) );
    … …  
    3738                add_action( 'admin_enqueue_scripts', array($this, 'enqueue_script'), 11 );
    3839        add_filter('block_editor_settings_all', [$this, 'newsletterEditorStyles'], 20, 2);
     40        add_filter('post_row_actions', [$this, 'newsletterRowActions'], PHP_INT_MAX, 2);
     41        add_filter('page_row_actions', [$this, 'newsletterRowActions'], PHP_INT_MAX, 2);
     42        foreach (['manage_en_newsletters_posts_columns', 'manage_edit-en_newsletters_columns', 'manage_edit-en_newsletters_sortable_columns'] as $hook) {
     43            add_filter($hook, [$this, 'removeNewsletterSEOColumns'], PHP_INT_MAX);
     44        }
    3945
    4046                add_action( 'wp_ajax_en_sendNewsletterTestMail', array( $this,'en_sendNewsletterTestMail'));
    … …  
    6167                add_action('admin_enqueue_scripts', [$this, "add_script_to_overview"]);
    6268                add_action( 'wp_ajax_en_copyNewsletter', array( $this,'en_copyNewsletter'));
    63                 add_action( 'wp_ajax_nopriv_en_copyNewsletter', array( $this,'en_copyNewsletter'));
    6469
    6570                metaBoxes::addAllMetaBoxes();
    … …  
    8792                                'labels'       => array(
    8893                                        'name'          => __( 'Newsletter' ),
    89                                         'singular_name' => __( 'Newsletter' )
     94                                        'singular_name' => __( 'Newsletter' ),
     95                    'add_new' => __('Neuer Newsletter', 'easynewsletter'),
     96                    'add_new_item' => __('Neuer Newsletter', 'easynewsletter')
    9097                                ),
    9198                                'public'       => true,
    … …  
    268275                if( 'en_newsletters' == $post_type ) {
    269276            $resources_url = EASY_NEWSLETTER_URL . 'src/core/resources/';
    270                         wp_enqueue_script( 'newsletters-admin-edit-script', $resources_url . 'newslettersPostTypeAdmin.js', [ 'jquery', 'wp-i18n']);
     277                        wp_enqueue_script( 'newsletters-admin-edit-script', $resources_url . 'newslettersPostTypeAdmin.js', [ 'jquery', 'wp-i18n'], filemtime(__DIR__ . '/resources/newslettersPostTypeAdmin.js'), true);
    271278            wp_set_script_translations("newsletters-admin-edit-script",'easynewsletter',plugin_dir_path(__FILE__) . 'resources/language');
    272279
    … …  
    294301            return $settings;
    295302        }
     303        $db = databaseConnector::instance();
     304        $contentWidth = generateEmailHtml::contentWidth($db->getSettingFromDB('newsletterContentWidth'));
     305        $wideWidth = filter_var($db->getSettingFromDB('newsletterWideWidth'), FILTER_VALIDATE_INT,
     306            ['options' => ['min_range' => 200, 'max_range' => 2000]]) ?: 1100;
     307        $wideWidth = max($contentWidth, $wideWidth);
     308        // Gutenberg uses these settings for inherited Group layouts and controls.
     309        $settings['__experimentalFeatures']['layout']['contentSize'] = $contentWidth . 'px';
     310        $settings['__experimentalFeatures']['layout']['wideSize'] = $wideWidth . 'px';
    296311        $settings['styles'][] = ['css' => file_get_contents(__DIR__ . '/resources/newsletterBaseStyle.css')];
     312        if (newsletterThemeStyles::enabled()) {
     313            // Theme fonts may be enqueued only on the frontend. Include their
     314            // absolute @font-face sources in the editor canvas as well.
     315            $themeCSS = newsletterStyleInliner::emailCSS(newsletterThemeStyles::css((int)$context->post->ID));
     316            [, $fonts] = \CSSIN::splitFontFace($themeCSS);
     317            if ($fonts !== '') { $settings['styles'][] = ['css' => $fonts]; }
     318        }
     319        // Override theme-generated canvas widths without touching other post
     320        // types, nested columns or explicit per-group content-size choices.
     321        $settings['styles'][] = ['css' => "
     322            .editor-styles-wrapper {
     323                --wp--style--global--content-size: {$contentWidth}px !important;
     324                --wp--style--global--wide-size: {$wideWidth}px !important;
     325            }
     326            .editor-styles-wrapper .is-root-container > .wp-block:not([data-align=wide]):not([data-align=full]):not(.alignwide):not(.alignfull),
     327            .editor-styles-wrapper .editor-post-title {
     328                max-width: {$contentWidth}px;
     329                margin-left: auto;
     330                margin-right: auto;
     331            }
     332            .editor-styles-wrapper .is-root-container > .wp-block[data-align=wide],
     333            .editor-styles-wrapper .is-root-container > .wp-block.alignwide {
     334                max-width: {$wideWidth}px;
     335                margin-left: auto;
     336                margin-right: auto;
     337            }
     338            .editor-styles-wrapper .is-root-container > .wp-block[data-align=full],
     339            .editor-styles-wrapper .is-root-container > .wp-block.alignfull {
     340                max-width: none;
     341            }
     342        "];
    297343        $customCSS = databaseConnector::instance()->getSettingFromDB('newsletterCSS');
    298344        if (is_string($customCSS) && trim($customCSS) !== '') {
    … …  
    305351        function remove_editor_styles_for_post_type(): void {
    306352                global $post_type;
    307                 if( 'en_newsletters' == $post_type ) {
     353                if( 'en_newsletters' == $post_type && !newsletterThemeStyles::enabled() ) {
    308354                remove_theme_support('editor-styles');
    309355            }
    … …  
    419465
    420466                        $postID = sanitize_key($_POST["post_id"]);
    421                         $subject = get_post_meta($postID,"en_subject", true);
     467                        $subject = "[Test] " . get_post_meta($postID,"en_subject", true);
    422468                        $userID = get_current_user_id();
    423469
    … …  
    508554                $posttype = $_GET["post_type"] ?? null;
    509555                if ('edit.php' === $hook && $posttype == "en_newsletters") {
    510                         wp_enqueue_script( 'newsletters-overview-script', EASY_NEWSLETTER_URL . 'src/core/resources/newslettersPostTypeOverview.js');
    511                 }
    512         }
    513 
    514         public function en_copyNewsletter(): void {
    515                 check_ajax_referer( 'secure_nonce_name', 'security' );
    516                 try {
    517                         $post = get_post($_POST["post_id"]);
    518 
    519                         //make a duplicate from the post
    520                         $new_post_id = wp_insert_post( array(
    521                         'post_title'            => $post->post_title . ' (Copy)',
    522                         'post_content'          => $post->post_content,
    523                         'post_status'           => $post->post_status,
    524                         'post_type'             => $post->post_type,
    525                         'post_author'           => $post->post_author,
    526                         'post_parent'           => $post->post_parent,
    527                         'post_excerpt'          => $post->post_excerpt,
    528                         'post_date'             => $post->post_date,
    529                         'post_date_gmt'         => $post->post_date_gmt,
    530                         'menu_order'            => $post->menu_order,
    531                         'comment_status'        => $post->comment_status,
    532                         'ping_status'           => $post->ping_status,
    533                         'post_password'         => $post->post_password,
    534                         'to_ping'               => $post->to_ping,
    535                         'pinged'                => $post->pinged,
    536                         'post_content_filtered' => $post->post_content_filtered,
    537                         'post_category'         => $post->post_category,
    538                         'tags_input'            => $post->tags_input,
    539                         'tax_input'             => $post->tax_input,
    540                         'meta_input'            => $post->meta_input,
    541                 ) );
    542 
    543                         //Copy custom Meta keys to the new post
    544                         $custom_keys = get_post_custom_keys($_POST["post_id"]);
    545                         foreach ( $custom_keys as $key ) {
    546                         $value = get_post_meta( $_POST["post_id"], $key, true );
    547                         update_post_meta( $new_post_id, $key, $value );
    548                 }
    549 
    550 
    551                 } catch (\Error | Exception $e){
    552             Log::logError($e->getMessage());
    553                 }
    554         }
     556                        wp_enqueue_script( 'newsletters-overview-script', EASY_NEWSLETTER_URL . 'src/core/resources/newslettersPostTypeOverview.js', ['jquery'], filemtime(__DIR__ . '/resources/newslettersPostTypeOverview.js'));
     557                }
     558        }
     559
     560    /** Registered only on newsletter column hooks, including Screen Options. */
     561    public function removeNewsletterSEOColumns(array $columns): array {
     562        foreach (array_keys($columns) as $key) {
     563            if ($key === 'tsf-seo-bar-wrap' || str_starts_with($key, 'wpseo-')) {
     564                unset($columns[$key]);
     565            }
     566        }
     567        return $columns;
     568    }
     569
     570    /** Keep one predictable copy workflow in the newsletter list only. */
     571    public function newsletterRowActions(array $actions, $post): array {
     572        if (($post->post_type ?? '') !== 'en_newsletters') { return $actions; }
     573        // WordPress uses these action keys as the surrounding span classes.
     574        // Duplicate Page; Copy & Delete Everything (copy-delete-posts);
     575        // Yoast Duplicate Post; Duplicate Page and Post; WP Duplicate Page; Post Duplicator.
     576        foreach (['duplicate', 'cdp_copy', 'clone', 'edit_as_new_draft', 'rewrite', 'dpp', 'njt_duplicate_page', 'duplicate_post', 'w3tc_flush_post'] as $key) {
     577            unset($actions[$key]);
     578        }
     579        return $actions;
     580    }
     581
     582    public function en_copyNewsletter(): void {
     583        check_ajax_referer('secure_nonce_name', 'security');
     584        $postID = absint($_POST['post_id'] ?? 0);
     585        $post = get_post($postID);
     586        $type = get_post_type_object('en_newsletters');
     587        if (!$post || $post->post_type !== 'en_newsletters'
     588            || !current_user_can('edit_post', $postID)
     589            || !$type || !current_user_can($type->cap->create_posts)) {
     590            wp_send_json_error(['message' => __('Newsletter kann nicht kopiert werden.', 'easynewsletter')], 403);
     591            return;
     592        }
     593        $newID = self::duplicateNewsletter($post);
     594        if (is_wp_error($newID)) {
     595            wp_send_json_error(['message' => $newID->get_error_message()], 500);
     596            return;
     597        }
     598        wp_send_json_success(['post_id' => $newID]);
     599    }
     600
     601    /** Copy editorial content, never the original publication or delivery state. */
     602    public static function duplicateNewsletter(\WP_Post $post) {
     603        $newID = wp_insert_post(wp_slash([
     604            'post_title' => $post->post_title . ' (Copy)',
     605            'post_content' => $post->post_content,
     606            'post_excerpt' => $post->post_excerpt,
     607            'post_status' => 'draft',
     608            'post_type' => 'en_newsletters',
     609            'post_author' => get_current_user_id(),
     610            'post_parent' => $post->post_parent,
     611            'post_date' => current_time('mysql'),
     612            // A floating draft date lets WordPress use publication time later,
     613            // unless the editor explicitly chooses a publication date.
     614            'post_date_gmt' => '0000-00-00 00:00:00',
     615            'comment_status' => $post->comment_status,
     616            'ping_status' => $post->ping_status,
     617            'post_password' => $post->post_password,
     618        ]), true);
     619        if (is_wp_error($newID)) { return $newID; }
     620
     621        $excluded = ['_edit_lock', '_edit_last', '_wp_old_slug', '_wp_old_date',
     622            '_wp_trash_meta_status', '_wp_trash_meta_time', '_wp_desired_post_slug',
     623            'en_scheduleLock', 'lockStatus', 'en_alreadySend', 'en_archiveContent'];
     624        foreach (get_post_meta($post->ID) as $key => $values) {
     625            if (in_array($key, $excluded, true)) { continue; }
     626            foreach ($values as $value) {
     627                add_post_meta($newID, $key, wp_slash(maybe_unserialize($value)));
     628            }
     629        }
     630        foreach (get_object_taxonomies($post->post_type) as $taxonomy) {
     631            $terms = wp_get_object_terms($post->ID, $taxonomy, ['fields' => 'ids']);
     632            if (!is_wp_error($terms)) { wp_set_object_terms($newID, $terms, $taxonomy); }
     633        }
     634        return $newID;
     635    }
    555636}
  • easynewsletter/tags/4.2.0/src/core/outlookPreview.php

    r3710879 r3730982  
    33
    44require_once __DIR__ . '/resources/vendor/cssin/CSSIN.php';
     5require_once __DIR__ . '/newsletterCSSLength.php';
    56
    67/** Preview-only approximation of Classic Outlook for Windows, after CSSIN. */
    … …  
    3233            $node->parentNode->removeChild($node);
    3334        }
     35        $rootFont = 16.0;
    3436        foreach ($xpath->query('//*') as $node) {
    3537            $parser = new \csstidy();
    … …  
    4244                }
    4345            }
     46            if (strtolower($node->tagName) === 'html' && isset($styles['font-size'])) {
     47                $rootFont = newsletterCSSLength::pixels($styles['font-size'], 16, 16) ?? 16;
     48            }
     49            foreach ($styles as $name => &$value) {
     50                if (!str_starts_with($name, '--') && !in_array($name, ['font-family', 'content'], true)) {
     51                    $value = newsletterCSSLength::resolveRem($value, $node->tagName === 'html' && $name === 'font-size' ? 16 : $rootFont);
     52                }
     53            }
     54            unset($value);
    4455            unset($styles['max-width']);
    4556            if (isset($styles['mso-padding-alt'])) {
    … …  
    5364                $plainProperty = preg_replace('/^-(webkit|moz|ms|o)-/', '', strtolower($property));
    5465                if (preg_match('/^border-.*radius(?:-.*)?$/', $plainProperty)
    55                     || in_array($plainProperty, ['box-shadow', 'text-shadow', 'gap', 'row-gap', 'column-gap', 'order'], true)
     66                    || in_array($plainProperty, ['box-shadow', 'text-shadow', 'gap', 'row-gap', 'column-gap', 'order', 'transform', 'transform-origin', 'transform-style', 'rotate', 'translate', 'scale', 'perspective', 'perspective-origin', 'backface-visibility'], true)
    5667                    || preg_match('/^(flex|grid|align|justify|place)(-|$)/', $plainProperty)) {
    5768                    unset($styles[$property]);
  • easynewsletter/tags/4.2.0/src/core/registration/registration.php

    r3710879 r3730982  
    5151         */
    5252        public function init(): void {
     53        add_action('init', [registrationBlock::class, 'register']);
    5354                add_shortcode('easyNewsletter', array($this, 'registrationFormFilter'));
    5455                add_shortcode('easyNewsletterUnsubscribeForm', array($this, 'easyNewsletterUnsubscribeForm'));
     56        add_action('wp_enqueue_scripts', static function () {
     57            wp_enqueue_style('en-registration-form', plugins_url('../resources/registrationForm.css', __FILE__), array(), '1.0.0');
     58        });
    5559        }
    5660
    … …  
    188192        public function registrationFormFilter($attributes): string|bool {
    189193
     194        return $this->renderForm($attributes);
     195    }
     196
     197    public function renderForm($attributes, bool $preview = false): string {
     198        static $instance = 0;
     199        $formInstance = ++$instance;
     200        $idSuffix = $formInstance === 1 ? '' : '_' . $formInstance;
     201        if (!$preview) {
     202            wp_enqueue_script('en-signup-form', plugins_url('../resources/signupForm.js', __FILE__), [], (string) filemtime(__DIR__ . '/../resources/signupForm.js'), true);
     203        }
    190204        $attributes = shortcode_atts(array(
    191205            'jump' => false,
    192206            'category' => 'default',
    193207            'redirect' => false,
     208            'layout' => '',
    194209            'registration_success_page_id' => 0
    195210        ), $attributes);
    … …  
    197212        $availableSubscriberCategories = databaseConnector::instance()->getSettingFromDB("subscriberCategory");
    198213        $availableSubscriberCategories = unserialize($availableSubscriberCategories);
    199         if (!in_array($attributes['category'], $availableSubscriberCategories)){
     214        if (!in_array($attributes['category'], (array) $availableSubscriberCategories, true)){
    200215            $attributes['category'] = 'default';
    201216        }
    … …  
    212227                $token = sanitize_text_field($_GET['token'] ?? "");
    213228
    214         if ($attributes['jump'] == "true" || $shouldRedirect){
     229        if (!$preview && (wp_validate_boolean($attributes['jump']) || $shouldRedirect)){
    215230            wp_enqueue_script(
    216231                'custom-registration-script', // Handle
    … …  
    223238       
    224239                ob_start();
    225         if (empty($_GET) ||
     240        if ($preview || empty($_GET) ||
    226241                    !empty($confirmed) &&
    227242                    !empty($unsubscribed) &&
  • easynewsletter/tags/4.2.0/src/core/registration/registrationForm.php

    r3710879 r3730982  
    66use Farn\EasyNewsletter\core\subscriberHandler;
    77
    8 if (!empty(sanitize_text_field( $_REQUEST['submit'] ?? ""))){
     8if (!$preview && !empty($_POST['submit']) && (isset($_POST['en_form_instance']) ? (string) $_POST['en_form_instance'] === (string) $formInstance : $formInstance === 1)){
    99
    1010    $registrationSuccessPageContent = get_the_content("", false, $registrationSuccessPageID);
    … …  
    1414        : '';
    1515
    16     if ( ($_POST['exitInput'] != "") || ($_POST['human_check'] != date('Y-m-d')) ){
     16    if ( (($_POST['exitInput'] ?? '') != "") || (($_POST['human_check'] ?? '') != date('Y-m-d')) ){
    1717        echo '<div id="en_registrationJumpContainer"' . $registrationSuccessRedirectAttribute . '>';
    1818        echo $registrationSuccessPageContent;
    … …  
    2323    $metaFields = array();
    2424
    25     foreach ($_REQUEST as $field => $input){
     25    foreach ($_POST as $field => $input){
    2626        if ($field == "en_subscriberCategory"){
    27             $metaFields[$field] = [sanitize_text_field($input)];
    28         } else if ($field != 'submit'){
     27            $metaFields[$field] = [$attributes['category']];
     28        } else if ($field != 'submit' && $field != 'en_form_instance' && is_scalar($input)){
    2929            $metaFields[$field] = sanitize_text_field($input);
    3030        }
    … …  
    5252ob_end_clean();
    5353
    54 $content = str_replace("{{registrationForm}}", $formContent , $content);
     54// A block-level form cannot live inside the paragraph used by older templates.
     55// Remove only the paragraph wrapping our placeholder, preserving surrounding copy.
     56$content = preg_replace('/<p\b[^>]*>\s*\{\{registrationForm\}\}\s*<\/p>/i', '{{registrationForm}}', $content);
     57// Avoid automatic paragraph insertion at template-only whitespace between tags.
     58$formContent = preg_replace('/>\s+</', '><', trim($formContent));
     59$content = str_replace("{{registrationForm}}", $formContent, $content);
    5560
    5661//No escape because we want to get the raw content
  • easynewsletter/tags/4.2.0/src/core/registration/registrationFormContent.php

    r3495745 r3730982  
    11<?php
    2 
    32namespace Farn\EasyNewsletter\core\registration;
    4 
    53use Farn\EasyNewsletter\core\databaseConnector;
    64
     5$signupCheckboxText = wp_kses_post(databaseConnector::instance()->getSettingFromDB('signupCheckboxText'));
     6if (trim($signupCheckboxText) === '') {
     7    $signupCheckboxText = esc_html__('I am a human being', 'easynewsletter');
     8}
     9$inlineLayout = ($attributes['layout'] ?? '') === 'inline';
     10$submitButton = '<input type="submit" class="button button-primary" name="submit" id="submit' . esc_attr($idSuffix) . '" value="' . esc_attr__('Subscribe', 'easynewsletter') . '">';
     11$inlineSubmitRendered = false;
     12$signupFormFields = (array) maybe_unserialize(databaseConnector::instance()->getSettingFromDB('signupFormFields'));
    713?>
    8 
    914<div class="en_signup_wrapper">
    10         <form method="post" name="en_signup_form" id="en_signup_form" class="en_signup_form">
    11                 <?php
    12                 // get values from database/settings-page
    13 
    14 
    15         $category = isset($attributes) ? $attributes[ 'category'] : "default";
    16 
    17                 $signupFormFields = databaseConnector::instance()->getSettingFromDB('signupFormFields');
    18                 $signupFormFields = unserialize($signupFormFields);
    19                 if($signupFormFields !== false){
    20                         foreach ($signupFormFields as $signupFormField){
    21                                 echo match ($signupFormField){
    22                                         "en_eMailAddress" => "<label for='" . esc_attr($signupFormField) . "'>" . __('E-Mail','easynewsletter'),
    23                                         "en_salutation" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Salutation','easynewsletter'),
    24                                         "en_gender" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Gender','easynewsletter'),
    25                                         "en_firstName" => "<label for='" . esc_attr($signupFormField) . "'>" . __('First Name', 'easynewsletter'),
    26                                         "en_lastName" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Last Name','easynewsletter'),
    27                                         "en_telephoneNumber" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Telephone Number', 'easynewsletter'),
    28                                         "en_eMailAddressValidation" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Validate E-Mail', 'easynewsletter'),
    29                     "en_securityQuestion" => "",
    30                                         default => "<label for='" . esc_attr($signupFormField) . "'>".$signupFormField
    31                                 };
    32 
    33                                 echo match ( $signupFormField ) {
    34                                         'en_eMailAddress', "en_eMailAddressValidation"=> "<input type='email' name='" . esc_attr($signupFormField) . "' id='" . esc_attr($signupFormField) . "' value='' required class='en_inputCheck' oninput='en_validationCheck()'>",
    35                                         'telephoneNumber'=> "<input type=' tel' name='" . esc_attr($signupFormField) . "' id='" . esc_attr($signupFormField) . "' value='' required class='en_inputCheck'>",
    36                                         "en_securityQuestion" => "",
    37                                         default => "<input type='text' name='" . esc_attr($signupFormField) . "' id='" . esc_attr($signupFormField) . "' value='' required>",
    38                                 };
    39                                 echo "</label>";
    40                         }
    41                         echo "<input style='display: none' name='exitInput' id='exitInput'>";
    42             echo "<input type='hidden' name='en_subscriberCategory' value='". $category ."'>";
    43         }
    44                 ?>
    45         <br>
    46                 <div id="en_human_check_div"><input type="checkbox" name="human_check" id="human_check" value=""><label for="human_check"><?php _e("I am a human being", 'easynewsletter')?></label></div>
    47                 <input type="submit" class="button button-primary" name="submit" id="submit" value="<?php esc_html_e ("Subscribe", 'easynewsletter')?>">
    48 
    49 
    50                 <script>
    51                     document.getElementById('human_check').addEventListener('click', function() {
    52                         if (this.checked) {
    53                             const currentDate = new Date().toISOString().slice(0, 10);
    54                             this.value = currentDate;
    55                         } else {
    56                             this.value = '';
    57                         }
    58                     });
    59                 </script>
    60 
    61 
    62     </form>
     15<form method="post" name="en_signup_form<?php echo esc_attr($idSuffix); ?>" id="en_signup_form<?php echo esc_attr($idSuffix); ?>" class="en_signup_form">
     16<?php if ($preview) { echo '<fieldset disabled style="border:0;padding:0;margin:0;min-width:0">'; } ?>
     17<?php foreach ($signupFormFields as $field):
     18    if (!is_string($field) || $field === 'en_securityQuestion') { continue; }
     19    $inlineEmail = $inlineLayout && $field === 'en_eMailAddress';
     20    if ($inlineEmail) { echo '<div class="en_signup_inline">'; }
     21    $type = in_array($field, ['en_eMailAddress', 'en_eMailAddressValidation'], true) ? 'email' : ($field === 'en_telephoneNumber' ? 'tel' : 'text');
     22    echo '<label for="' . esc_attr($field . $idSuffix) . '">' . esc_html(registrationBlock::fieldLabel($field));
     23    echo '<input type="' . $type . '" name="' . esc_attr($field) . '" id="' . esc_attr($field . $idSuffix) . '" value="" required class="en_inputCheck"></label>';
     24    if ($inlineEmail) { echo $submitButton . '</div>'; $inlineSubmitRendered = true; }
     25endforeach; ?>
     26<input style="display:none" name="exitInput" id="exitInput<?php echo esc_attr($idSuffix); ?>" tabindex="-1" autocomplete="off">
     27<input type="hidden" name="en_subscriberCategory" value="<?php echo esc_attr($attributes['category']); ?>">
     28<input type="hidden" name="en_form_instance" value="<?php echo esc_attr($formInstance); ?>">
     29<?php if (!$inlineLayout) { echo '<br>'; } ?>
     30<div class="en_human_check_div" id="en_human_check_div<?php echo esc_attr($idSuffix); ?>"><input type="checkbox" name="human_check" id="human_check<?php echo esc_attr($idSuffix); ?>" value="" required><label for="human_check<?php echo esc_attr($idSuffix); ?>"><?php echo $signupCheckboxText; ?></label></div>
     31<?php if (!$inlineSubmitRendered) { echo $submitButton; } ?>
     32<?php if ($preview) { echo '</fieldset>'; } ?>
     33</form>
    6334</div>
    64 
    65 <?php
    66 if (in_array("en_eMailAddressValidation", $signupFormFields)) {
    67         wp_add_inline_script("en_registrationValidation", "
    68         const submitButton = document.getElementById('submit')
    69         let emailElement = document.getElementById('en_eMailAddress')
    70         let emailValidationElement = document.getElementById('en_eMailAddressValidation')
    71        
    72         submitButton.setAttribute('disabled', 'disabled')
    73        
    74         function en_validationCheck(){
    75             if (emailElement.value === emailValidationElement.value){
    76                 submitButton.removeAttribute('disabled')
    77                 emailValidationElement.style.color = 'black'
    78             } else {
    79                 submitButton.setAttribute('disabled', 'disabled')
    80                 emailValidationElement.style.color = 'red'
    81             }
    82         }");
    83 }
  • easynewsletter/tags/4.2.0/src/core/resources/admin.css

    r3495745 r3730982  
    77    float: right;
    88}
     9/* Settings subsections: plain links beneath the main plugin tabs. */
     10.en-settings-navigation {
     11    display: flex;
     12    gap: 12px;
     13    margin-bottom: 25px;
     14}
     15.en-settings-navigation a { text-decoration: none; }
     16.en-settings-navigation a[aria-current="page"] { color: #1d2327; font-weight: 600; }
     17.en-settings-navigation a:focus-visible { outline: 2px solid #2271b1; outline-offset: 4px; }
     18#easyNewsletterSettings [data-en-settings-panel][hidden] { display: none; }
  • easynewsletter/tags/4.2.0/src/core/resources/newslettersPostTypeAdmin.js

    r3696327 r3730982  
    33const { ajaxUrl, nonce, sendingInProgress } = easyNewsletterData;
    44
    5 if (typeof __ === "undefined") {
     5jQuery(document).ready(function ($) {
    66  const { __ } = wp.i18n;
    7 }
    8 
    9 jQuery(document).ready(function ($) {
    10   // add send-buttons and basic popup to html
    11   setTimeout(function () {
    12     // wrapper
    13     $(".editor-header").after("<div class='en_sendButtonsWrapper'></div>");
    14     // sendNewsletter-button or stopSendingNewsletter-button depending on sending state
    15     if (sendingInProgress === true || sendingInProgress === "true") {
    16       // Newsletter is currently being sent
    17       $(".en_sendButtonsWrapper").append('<button class="components-button editor-post-switch-to-draft is-tertiary en_stopSendingNewsletter dashicons-before dashicons-no">' + wp.i18n.__("Stop Sending", "easynewsletter") + "</button>");
    18     } else {
    19       $(".en_sendButtonsWrapper").append("<button class='components-button editor-post-switch-to-draft is-tertiary en_wantToSendNewsletter dashicons-before dashicons-email-alt'>" + __("Send Newsletter Now", "easynewsletter") + "</button>");
    20     }
    21     // sendTestMail-button
    22     $(".en_sendButtonsWrapper").append("<button class='components-button editor-post-switch-to-draft is-tertiary en_TestmailButton dashicons-before dashicons-admin-tools'>" + __("Send Test Mail", "easynewsletter") + "</button>");
    23     // newsletterPopup
    24     $("body").append('<div class="en_newsletterAdminPopup hidden"><div class="en_newsletterAdminPopupBackground"></div><div class="en_newsletterAdminPopupContent"></div></div>');
    25   }, 2000);
     7  let toolbar = null;
     8  let framePending = false;
     9
     10  function mountToolbar() {
     11    const header = document.querySelector('.editor-header, .edit-post-header');
     12    if (!header) return;
     13    if (!toolbar) {
     14      toolbar = document.createElement('div');
     15      toolbar.className = 'en_sendButtonsWrapper';
     16      const sending = sendingInProgress === true || sendingInProgress === 'true';
     17      const send = document.createElement('button');
     18      send.type = 'button';
     19      send.className = 'components-button editor-post-switch-to-draft is-tertiary dashicons-before ' +
     20        (sending ? 'en_stopSendingNewsletter dashicons-no' : 'en_wantToSendNewsletter dashicons-email-alt');
     21      send.textContent = sending ? __('Stop Sending', 'easynewsletter') : __('Send Newsletter Now', 'easynewsletter');
     22      const test = document.createElement('button');
     23      test.type = 'button';
     24      test.className = 'components-button editor-post-switch-to-draft is-tertiary en_TestmailButton dashicons-before dashicons-admin-tools';
     25      test.textContent = __('Send Test Mail', 'easynewsletter');
     26      toolbar.append(send, test);
     27    }
     28    if (header.nextElementSibling !== toolbar) {
     29      header.after(toolbar);
     30      // Also fires after Gutenberg replaces the header. Reuse our node so
     31      // extension buttons and their current state survive a remount.
     32      $(document).trigger('en:toolbar-ready', [toolbar]);
     33    }
     34    if (!document.querySelector('.en_newsletterAdminPopup')) {
     35      $('body').append('<div class="en_newsletterAdminPopup hidden"><div class="en_newsletterAdminPopupBackground"></div><div class="en_newsletterAdminPopupContent"></div></div>');
     36    }
     37  }
     38  const toolbarObserver = new MutationObserver(function () {
     39    if (framePending) return;
     40    framePending = true;
     41    requestAnimationFrame(function () {
     42      framePending = false;
     43      mountToolbar();
     44    });
     45  });
     46  toolbarObserver.observe(document.body, { childList: true, subtree: true });
     47  mountToolbar();
     48  window.addEventListener('pagehide', () => toolbarObserver.disconnect(), { once: true });
    2649
    2750  // click on newsletter popup background - hide popup
  • easynewsletter/tags/4.2.0/src/core/resources/newslettersPostTypeOverview.js

    r3495745 r3730982  
    33        let id = $(element).parents(":eq(1)").attr("id");
    44        id = id.toString().replace("post-","");
    5         $(element).append("<span class='copy'> | <a href='#' class='en_copy' data-post-id='"+id+"'>Copy</a></span>");
     5        $(element).append("<span class='en_row_action_copy'> | <a href='#' class='en_copy' data-post-id='"+id+"'>Duplizieren</a></span>");
    66    })
    77
  • easynewsletter/tags/4.2.0/src/core/resources/settingsPage.js

    r3495745 r3730982  
    1 jQuery(document).ready(function ($) {
    2     $("#easyNewsletterSettings").on('keydown', function(event) {
    3         if (event.keyCode === 13){
     1jQuery(function ($) {
     2    const form = document.getElementById('easyNewsletterSettings');
     3    if (!form) return;
     4    const links = Array.from(document.querySelectorAll('[data-en-settings-link]'));
     5    const panels = Array.from(document.querySelectorAll('[data-en-settings-panel]'));
     6    const selection = form.elements.en_settings_section;
     7    const valid = new Set(links.map(link => link.dataset.enSettingsLink));
     8    function showSection(id) {
     9        if (!valid.has(id)) id = 'general';
     10        panels.forEach(panel => { panel.hidden = panel.dataset.enSettingsPanel !== id; });
     11        links.forEach(link => {
     12            if (link.dataset.enSettingsLink === id) link.setAttribute('aria-current', 'page');
     13            else link.removeAttribute('aria-current');
     14        });
     15        selection.value = id;
     16    }
     17    function fromHash() {
     18        const match = location.hash.match(/^#en-settings-(general|signup|design)$/);
     19        return match ? match[1] : selection.value;
     20    }
     21    links.forEach(link => link.addEventListener('click', event => {
     22        event.preventDefault();
     23        const id = link.dataset.enSettingsLink;
     24        showSection(id);
     25        history.replaceState(null, '', link.getAttribute('href'));
     26    }));
     27    window.addEventListener('hashchange', () => showSection(fromHash()));
     28    // Hidden fields stay enabled: switching sections never discards edits and
     29    // saving submits the complete settings form, including unchecked checkboxes.
     30    form.addEventListener('invalid', event => {
     31        const panel = event.target.closest('[data-en-settings-panel]');
     32        if (panel) showSection(panel.dataset.enSettingsPanel);
     33    }, true);
     34    showSection(fromHash());
     35    $(form).on('keydown', function (event) {
     36        if (event.key === 'Enter' && $(event.target).is('input:not([type=submit]):not([type=button])')) {
    437            event.preventDefault();
    5             $("#en_submitSaveButton").click();
     38            document.getElementById('en_submitSaveButton').click();
    639        }
    7     })
    8 })
     40    });
     41});
  • easynewsletter/tags/4.2.0/src/core/settingsPageContent.php

    r3710879 r3730982  
    1414    }
    1515
     16    $settingsValueMap['newsletterUseThemeStyles'] = isset($_POST['newsletterUseThemeStyles']) && $_POST['newsletterUseThemeStyles'] === 'true' ? 'true' : 'false';
     17    $excludedStyles = array_values(array_filter((array)($_POST['newsletterExcludedStyles'] ?? []), 'is_string'));
     18    $settingsValueMap['newsletterExcludedStyles'] = wp_json_encode(array_map('sanitize_text_field', wp_unslash($excludedStyles)));
     19    $settingsValueMap['newsletterCSSFile'] = esc_url_raw(wp_unslash((string)($_POST['newsletterCSSFile'] ?? '')), ['http', 'https']);
     20    // Root-relative paths are convenient for CSS kept with the theme.
     21    if (isset($_POST['newsletterCSSFile']) && str_starts_with(trim((string)$_POST['newsletterCSSFile']), '/')) {
     22        $settingsValueMap['newsletterCSSFile'] = esc_url_raw(newsletterThemeStyles::absoluteURL(trim(wp_unslash($_POST['newsletterCSSFile'])), home_url('/')));
     23    }
     24    update_option('en_style_revision', wp_generate_uuid4(), false);
     25    delete_option('en_style_file_warnings');
     26    unset($settingsValueMap['en_settings_section']);
    1627    unset($settingsValueMap['page']);
    1728    unset($settingsValueMap['submit']);
    … …  
    3950            }
    4051            continue;
     52        } else if ($setting == "signupCheckboxText"){
     53            $value = wp_kses_post(wp_unslash($value));
    4154        } else if ($setting == "newsletterContentWidth"){
    4255            $value = (string)generateEmailHtml::contentWidth($value);
     56        } else if ($setting == "newsletterSideSpacing"){
     57            $value = (string)generateEmailHtml::sideSpacing($value);
     58        } else if ($setting == "newsletterWideWidth"){
     59            $value = (string)(filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 200, 'max_range' => 2000]]) ?: 1100);
    4360        } else if ($setting == "newsletterCSS"){
    4461            // Prevent escaping quotes
    … …  
    157174    }
    158175    ?>
    159     <h1 class="wp-heading-inline"><?php esc_html_e("Easy Newsletter Settings", "easynewsletter");?></h1>
    160     <hr class="wp-header-end">
     176    <?php
     177    $settingsSections = [
     178        'general' => ['label' => __('Allgemein', 'easynewsletter'), 'title' => __('Allgemeine Einstellungen', 'easynewsletter')],
     179        'signup' => ['label' => __('Anmeldung', 'easynewsletter'), 'title' => __('Anmeldung', 'easynewsletter')],
     180        'design' => ['label' => __('Design', 'easynewsletter'), 'title' => __('Newsletter-Design', 'easynewsletter')],
     181    ];
     182    $settingSection = static function ($setting) {
     183        if (in_array($setting, ['newsletterCSS', 'newsletterContentWidth', 'newsletterWideWidth', 'newsletterSideSpacing', 'newsletterUseThemeStyles', 'newsletterCSSFile', 'newsletterExcludedStyles'], true)) { return 'design'; }
     184        if (in_array($setting, ['signupFormFields', 'signupCheckboxText', 'subscriberMode', 'subscriberCategory', 'subscriberRole', 'addedUserRoleKey', 'standardActivationPost', 'standardUnsubscribePost', 'standardWelcomePost', 'registrationPageID', 'registrationSuccessPageID', 'confirmationSuccessPageID', 'confirmationDeniedPageID', 'unsubscribedPageID', 'registrationFormPageID', 'unsubscribeFormPageID', 'unsubscribedConfirmedPageID'], true)) { return 'signup'; }
     185        return 'general';
     186    };
     187    $selectedSection = sanitize_key($_POST['en_settings_section'] ?? 'general');
     188    if (!isset($settingsSections[$selectedSection])) { $selectedSection = 'general'; }
     189    ?>
     190    <nav class="en-settings-navigation" aria-label="<?php esc_attr_e('Einstellungsbereiche', 'easynewsletter'); ?>">
     191        <?php foreach ($settingsSections as $sectionID => $section): ?>
     192            <a href="#en-settings-<?php echo esc_attr($sectionID); ?>" data-en-settings-link="<?php echo esc_attr($sectionID); ?>"><?php echo esc_html($section['label']); ?></a>
     193        <?php endforeach; ?>
     194    </nav>
    161195    <form method="post" name="easyNewsletterSettings" id="easyNewsletterSettings">
     196        <input type="hidden" name="en_settings_section" value="<?php echo esc_attr($selectedSection); ?>">
     197        <?php
     198        $allSettings = (array)databaseConnector::instance()->getAllSettings();
     199        $designSettings = [];
     200        foreach (['newsletterCSS', 'newsletterContentWidth', 'newsletterWideWidth', 'newsletterSideSpacing'] as $designKey) {
     201            if (array_key_exists($designKey, $allSettings)) {
     202                $designSettings[$designKey] = $allSettings[$designKey];
     203                unset($allSettings[$designKey]);
     204            }
     205        }
     206        $allSettings = array_merge($allSettings, $designSettings);
     207        ?>
     208        <?php foreach ($settingsSections as $sectionID => $section): ?>
     209        <section id="en-settings-<?php echo esc_attr($sectionID); ?>" data-en-settings-panel="<?php echo esc_attr($sectionID); ?>" aria-labelledby="en-settings-heading-<?php echo esc_attr($sectionID); ?>">
     210        <h1 id="en-settings-heading-<?php echo esc_attr($sectionID); ?>"><?php echo esc_html($section['title']); ?></h1>
    162211        <table class="form-table">
    163212            <tbody>
    164213            <?php
    165                 $allSettings = databaseConnector::instance()->getAllSettings();
    166214                foreach ($allSettings as $setting => $value){
     215                    if ($settingSection($setting) !== $sectionID) { continue; }
     216
     217                    if (in_array($setting, ['newsletterUseThemeStyles', 'newsletterCSSFile', 'newsletterExcludedStyles'], true)) { continue; }
     218                    if ($setting === 'newsletterCSS') {
     219                        require_once __DIR__ . '/newsletterStyleSettings.php';
     220                        newsletterStyleSettings::render();
     221                    }
    167222
    168223                    //TODO Maybe put them somewhere to be seen in a hidden settings menu or something.
    … …  
    195250                        'intervalInSeconds' => "<th scope='row'>".__("Interval in seconds ", 'easynewsletter')."</th>",
    196251                        'signupFormFields' => "<th scope='row'>".__("Signup form fields ", 'easynewsletter')."</th>",
     252                        'signupCheckboxText' => "<th scope='row'>".esc_html__("Text der Pflicht-Checkbox im Anmeldeformular", 'easynewsletter')."</th>",
    197253                        'activeNewsletter' => "<th scope='row'>".__("Currently active newsletter ", 'easynewsletter')."</th>",
    198254                        'activeNewsletterID' => "<th scope='row'>".__("Currently active newsletter ID ", 'easynewsletter')."</th>",
    … …  
    204260                        'subscriberCategory' => "<th scope='row'>".__("Available subscriber category ", 'easynewsletter')."</th>",
    205261                        'subscriberRole' => "<th scope='row'>".__("Available subscriber roles ", 'easynewsletter')."</th>",
    206                         'newsletterContentWidth' => "<th scope='row'>".__("Newsletter-Inhaltsbreite (px)", 'easynewsletter')."</th>",
    207                         'newsletterCSS' => "<th scope='row'>".__("Custom Newsletter CSS ", 'easynewsletter')."</th>",
     262                        'newsletterContentWidth' => "<th scope='row'>".__("Standard-Breite (px)", 'easynewsletter')."</th>",
     263                        'newsletterSideSpacing' => "<th scope='row'>".__("Seitlicher Inhaltsabstand (px)", 'easynewsletter')."</th>",
     264                        'newsletterWideWidth' => "<th scope='row'>".__("Weite Breite (px)", 'easynewsletter')."</th>",
     265                        'newsletterCSS' => "<th scope='row'>".__("Benutzerdefiniertes CSS", 'easynewsletter')."</th>",
    208266                        'addedUserRoleKey' => "<th scope='row'>".__("New user role key ", 'easynewsletter')."</th>",
    209267                        'registrationPageID' => "<th scope='row'>".__("Registration page id ", 'easynewsletter')."</th>",
    … …  
    219277                                'senderName' => "<input type='text' name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "' value='" .stripslashes(esc_attr($value)) . "'>",
    220278                        'maxEmailPerInterval', 'intervalInSeconds' => "<input type='number' name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "' value='" . esc_attr($value) . "'>",
    221                         'newsletterContentWidth' => "<input type='number' min='200' max='2000' step='1' name='newsletterContentWidth' id='newsletterContentWidth' value='" . esc_attr($value) . "'><p class='description'>" . esc_html__("Maximale Desktop-Inhaltsbreite und Grundlage für die Bildgrößenberechnung. Auf schmalen Bildschirmen bleibt der Newsletter flexibel. Standard: 800 px.", 'easynewsletter') . "</p>",
     279                        'newsletterContentWidth' => "<input type='number' min='200' max='2000' step='1' name='newsletterContentWidth' id='newsletterContentWidth' value='" . esc_attr($value) . "'>",
     280                        'newsletterSideSpacing' => "<input type='number' min='0' max='200' step='1' name='newsletterSideSpacing' id='newsletterSideSpacing' value='" . esc_attr($value) . "'>",
     281                        'newsletterWideWidth' => "<input type='number' min='200' max='2000' step='1' name='newsletterWideWidth' id='newsletterWideWidth' value='" . esc_attr($value) . "'>",
    222282                        'signupFormFields' => createCheckboxes($value),
     283                        'signupCheckboxText' => "<textarea name='signupCheckboxText' id='signupCheckboxText' rows='4' cols='50' aria-describedby='signupCheckboxTextDescription'>" . esc_textarea($value) . "</textarea><p class='description' id='signupCheckboxTextDescription'>" . esc_html__("Eigener Text, z. B. zur Bestätigung der Datenschutzhinweise. Sicheres HTML wie Links und Hervorhebungen ist erlaubt. Leer lassen für den Standardtext „Ich bin ein Mensch“. Die bestehende Spam-Prüfung bleibt aktiv.", 'easynewsletter') . "</p>",
    223284                        'subscriberMode' => generateModeSelection(),
    224285                        'sendingInProgress', 'activeNewsletter', 'activeNewsletterID' => "<input type='text' name='" . $setting . "' id='" . $setting . "' value='" . $value . "' disabled>",
    225286                                'subscriberCategory' => generateTargetGroupsCategory(),
    226287                                'subscriberRole' => generateTargetGroupsRoles(),
    227                                 'newsletterCSS' => "<textarea name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "'rows='4' cols='50' form='easyNewsletterSettings'>".$value."</textarea>",
     288                                'newsletterCSS' => "<textarea name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "'rows='4' cols='50' form='easyNewsletterSettings'>".esc_textarea($value)."</textarea>",
    228289                        default => "<input type='text' name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "' value='" . esc_attr($value) . "'>",
    229290                        };
    … …  
    232293            </tbody>
    233294        </table>
     295        </section>
     296        <?php endforeach; ?>
    234297        <?php echo "<input type='hidden' name='wp_en_nonce' value='".wp_create_nonce()."'>"?>
    235298        <input type="submit" id="en_submitSaveButton" class="button button-primary" name="submit" value="<?php _e("Save", 'easynewsletter')?>">
    236299
     300        <div data-en-settings-panel="general">
    237301        <h3><?php _e("Press this Button to stop the Newsletter sending Process", 'easynewsletter')?></h3>
    238302        <input type="submit" class="button button-primary" style="background-color: red; border-color: red" name="stopNewsletterSending" value="<?php _e("Stop Newsletter Sending Process", 'easynewsletter')?>">
     303        </div>
    239304    </form>
    240305    <br>
    241306</div>
    242 
  • easynewsletter/tags/4.2.0/vendor-prefixed/autoload.php

    r3710879 r3730982  
    2020require_once __DIR__ . '/composer/autoload_real.php';
    2121
    22 return ComposerAutoloaderInit9130e49187f279f2bf1d421ae64cfb1b::getLoader();
     22return ComposerAutoloaderInit2a106f58d8aeb22de715011744b3955b::getLoader();
  • easynewsletter/tags/4.2.0/vendor-prefixed/composer/autoload_real.php

    r3710879 r3730982  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInit9130e49187f279f2bf1d421ae64cfb1b
     5class ComposerAutoloaderInit2a106f58d8aeb22de715011744b3955b
    66{
    77    private static $loader;
    … …  
    2323        }
    2424
    25         spl_autoload_register(array('ComposerAutoloaderInit9130e49187f279f2bf1d421ae64cfb1b', 'loadClassLoader'), true, true);
     25        spl_autoload_register(array('ComposerAutoloaderInit2a106f58d8aeb22de715011744b3955b', 'loadClassLoader'), true, true);
    2626        self::$loader = $loader = new \Farn\EasyNewsletter\Composer\Autoload\ClassLoader(\dirname(__DIR__));
    27         spl_autoload_unregister(array('ComposerAutoloaderInit9130e49187f279f2bf1d421ae64cfb1b', 'loadClassLoader'));
     27        spl_autoload_unregister(array('ComposerAutoloaderInit2a106f58d8aeb22de715011744b3955b', 'loadClassLoader'));
    2828
    2929        require __DIR__ . '/autoload_static.php';
    30         call_user_func(\Farn\EasyNewsletter\Composer\Autoload\ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b::getInitializer($loader));
     30        call_user_func(\Farn\EasyNewsletter\Composer\Autoload\ComposerStaticInit2a106f58d8aeb22de715011744b3955b::getInitializer($loader));
    3131
    3232        $loader->setClassMapAuthoritative(true);
  • easynewsletter/tags/4.2.0/vendor-prefixed/composer/autoload_static.php

    r3710879 r3730982  
    55namespace Farn\EasyNewsletter\Composer\Autoload;
    66
    7 class ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b
     7class ComposerStaticInit2a106f58d8aeb22de715011744b3955b
    88{
    99    public static $prefixLengthsPsr4 = array (
    … …  
    3333    {
    3434        return \Closure::bind(function () use ($loader) {
    35             $loader->prefixLengthsPsr4 = ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b::$prefixLengthsPsr4;
    36             $loader->prefixDirsPsr4 = ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b::$prefixDirsPsr4;
    37             $loader->classMap = ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b::$classMap;
     35            $loader->prefixLengthsPsr4 = ComposerStaticInit2a106f58d8aeb22de715011744b3955b::$prefixLengthsPsr4;
     36            $loader->prefixDirsPsr4 = ComposerStaticInit2a106f58d8aeb22de715011744b3955b::$prefixDirsPsr4;
     37            $loader->classMap = ComposerStaticInit2a106f58d8aeb22de715011744b3955b::$classMap;
    3838
    3939        }, null, ClassLoader::class);
  • easynewsletter/trunk/README.txt

    r3710879 r3730982  
    44Requires at least: 6.0
    55Tested up to: 7.0
    6 Stable tag: 4.1.4
     6Stable tag: 4.2.0
    77Requires PHP: 8.0
    88License: GPLv3 or later
    … …  
    7272== Changelog ==
    7373
     74= 4.2.0 =
     75Fix - Automatic image sizing in responsive emails.
     76New - Added fluid block layouts and signup forms.
     77New - Introduced theme-based newsletter styling pipeline.
     78New - Added inline registration form layout.
     79New - Configurable signup checkbox text.
     80
    7481= 4.1.4 =
    7582New - Added newsletter email rendering pipeline.
  • easynewsletter/trunk/easynewsletter.php

    r3710879 r3730982  
    55Plugin URI: https://www.farnlabs.de/
    66Description: Provides a Newsletter functionality with complete WordPress integration.
    7 Version: 4.1.4
     7Version: 4.2.0
    88Author: FARN – digital brand design
    99Author URI: https://www.farn.de/
  • easynewsletter/trunk/src/core/databaseConnector.php

    r3710879 r3730982  
    3131                'intervalInSeconds' => '300',
    3232                'signupFormFields' => '', // will be set as a serialized array('en_eMailAddress') in the constructor.
     33                'signupCheckboxText' => '', // Empty uses the translated default checkbox label.
    3334                'activeNewsletter' => '',
    3435                'activeNewsletterID' => '',
    … …  
    4243                'subscriberCategory' => '', // will be set as a serialized array('default') in the constructor.
    4344                'subscriberRole' => '', // will be set as a serialized array('default') in the constructor.
     45        'newsletterUseThemeStyles' => 'false', // Upgrades always opt out.
     46        'newsletterCSSFile' => '',
     47        'newsletterExcludedStyles' => '[]',
    4448                'newsletterCSS' => '',
    4549        'newsletterContentWidth' => '800',
     50        'newsletterWideWidth' => '1100',
     51        'newsletterSideSpacing' => '0', // Preserve existing installations.
    4652                'addedUserRoleKey' => 'en_user_subscriber',
    4753                "registrationPageID" => "",
    … …  
    125131                $results = array_column($results,0);
    126132
     133        $freshInstallation = empty($results);
    127134                foreach ($this->defaultSettings as $key => $value) {
     135            if ($key === 'newsletterUseThemeStyles' && $freshInstallation) { $value = 'true'; }
     136            if ($key === 'newsletterSideSpacing' && $freshInstallation) { $value = '20'; }
    128137                        if(!in_array($key, $results)){
    129138                                $this->wpdb->insert($this->tableName,
  • easynewsletter/trunk/src/core/generateEmailHtml.php

    r3710879 r3730982  
    33
    44require_once __DIR__ . '/resources/vendor/cssin/CSSIN.php';
     5require_once __DIR__ . '/newsletterThemeStyles.php';
     6require_once __DIR__ . '/newsletterStyleInliner.php';
     7require_once __DIR__ . '/newsletterBlockLayout.php';
    58
    69use CSSIN;
    … …  
    1013class generateEmailHtml {
    1114        public static function generate($postID) : string{
     15        $extendedStyles = newsletterThemeStyles::extended();
     16        $baseCSS = file_get_contents(__DIR__ . '/resources/newsletterBaseStyle.css');
     17        if ($extendedStyles) {
     18            // Let the theme's inherited font size reach the email wrapper.
     19            $baseCSS = str_replace(['font-size: 16px!important;', 'line-height: 1.3;'], '', $baseCSS);
     20        }
     21        $additionalCSS = $extendedStyles ? newsletterThemeStyles::css((int)$postID) : '';
    1222        $contentWidth = self::contentWidth(databaseConnector::instance()->getSettingFromDB("newsletterContentWidth"));
    1323                $enl_mailcontent_head = '<!DOCTYPE html><html lang="de">
    … …  
    2030                        <style type="text/css">
    2131                                '.file_get_contents( __DIR__ . "/resources/newsletterBlocklibraryStyle.css" ).'
    22                                 '.file_get_contents( __DIR__ . "/resources/newsletterBaseStyle.css" ).'
     32                                '.$baseCSS.'
     33                '.$additionalCSS.'
    2334                                '.databaseConnector::instance()->getSettingFromDB("newsletterCSS").'
    2435                        </style>
    … …  
    6576                        $enl_full_mailcontent = $enl_mailcontent_head . $enl_mailcontent_body . $enl_mailcontent_foot;
    6677
     78            if ($extendedStyles) {
     79                // Normalize before variable resolution too: its legacy parser
     80                // must see ordinary root declarations outside conditional rules.
     81                $enl_full_mailcontent = preg_replace_callback('~(<style\b[^>]*>)(.*?)(</style>)~is', static function ($match) {
     82                    return $match[1] . newsletterStyleInliner::emailCSS($match[2]) . $match[3];
     83                }, $enl_full_mailcontent);
     84            }
     85
    6786                        /* Use CSSIN to inline all styles */
    6887                        $cssin                        = new CSSIN();
    69                         $enl_full_mailcontent_inlined = $cssin->inlineCSS( self::prepare($enl_full_mailcontent) );
     88                        $enl_full_mailcontent_inlined = $extendedStyles
     89                ? newsletterStyleInliner::inline(self::prepare($enl_full_mailcontent, false), $contentWidth)
     90                : $cssin->inlineCSS(self::prepare($enl_full_mailcontent));
    7091
    7192                        //return $enl_full_mailcontent;
    72                         return self::transform($enl_full_mailcontent_inlined, $contentWidth);
     93                        return self::transform($enl_full_mailcontent_inlined, $contentWidth, $extendedStyles, (int)(databaseConnector::instance()->getSettingFromDB('newsletterWideWidth') ?: 1100), self::sideSpacing(databaseConnector::instance()->getSettingFromDB('newsletterSideSpacing')));
    7394                } catch (\Throwable $e) {
    7495            Log::log("An error occurred while processing the mail content: " . $e->getMessage());
    … …  
    7798        }
    7899
     100    public static function sideSpacing($value): int {
     101        $valid = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 200]]);
     102        return $valid === false ? 0 : $valid;
     103    }
     104
    79105    private int $layoutWidth = 800;
     106    private float $rootFont = 16;
    80107    private ?\DOMElement $layoutRoot = null;
    81108    public static function contentWidth($value): int {
    … …  
    99126    private static function length(string $value, float $reference, float $font = 16): ?float {
    100127        $value = self::plain($value);
     128        if (preg_match('/^(calc|clamp|min|max)\(/i', $value)) {
     129            return newsletterCSSLength::pixels($value, $reference, $font);
     130        }
    101131        if (!preg_match('/^(\d+(?:\.\d+)?)(px|pt|%|em|rem)?$/i', $value, $match)) { return null; }
    102132        $number = (float)$match[1];
    … …  
    203233
    204234    /** Remember author intent before global image CSS is inlined. */
    205     public static function prepare(string $html): string {
     235    public static function prepare(string $html, bool $convertColumns = true): string {
    206236        if (trim($html) === '') { return $html; }
    207237        $dom = self::load($html);
    208238        self::resolveDocumentVariables($dom);
     239        foreach ((new \DOMXPath($dom))->query('//div[contains(concat(" ",normalize-space(@class)," ")," wp-block-column ")]') as $column) {
     240            $author = self::styles($column->getAttribute('style'));
     241            if (isset($author['flex-basis'])) { $column->setAttribute('data-en-author-basis', $author['flex-basis']); }
     242        }
     243        foreach ((new \DOMXPath($dom))->query('//div[contains(concat(" ",normalize-space(@class)," ")," wp-block-group ")]') as $group) {
     244            $padding = array_filter(self::styles($group->getAttribute('style')), static function ($name) { return str_starts_with($name, 'padding'); }, ARRAY_FILTER_USE_KEY);
     245            if ($padding) { $group->setAttribute('data-en-author-padding', json_encode($padding)); }
     246        }
     247        if ($convertColumns) { self::prepareColumns($dom); }
     248        foreach ($dom->getElementsByTagName('img') as $image) {
     249            $styles = self::styles($image->getAttribute('style'));
     250            $intent = [];
     251            foreach (['width', 'height', 'aspect-ratio', 'object-fit', 'object-position'] as $key) {
     252                if (isset($styles[$key])) { $intent[$key] = $styles[$key]; }
     253            }
     254            // Older Gutenberg versions put intentional dimensions in attributes.
     255            $parent = $image->parentNode;
     256            while ($parent instanceof \DOMElement && in_array($parent->tagName, ['a', 'figure', 'div'], true)) {
     257                if (preg_match('/\bis-resized\b/', $parent->getAttribute('class'))) {
     258                    foreach (['width', 'height'] as $key) {
     259                        if (!isset($intent[$key]) && preg_match('/^[1-9]\d*$/', $image->getAttribute($key))) {
     260                            $intent[$key] = $image->getAttribute($key) . 'px';
     261                        }
     262                    }
     263                    break;
     264                }
     265                $parent = $parent->parentNode;
     266            }
     267            $image->setAttribute('data-en-image-intent', json_encode($intent));
     268        }
     269        return $dom->saveHTML();
     270    }
     271
     272    private static function prepareColumns(\DOMDocument $dom): void {
    209273        // Work from inside out so nested column groups keep their own row/gap.
    210274        $groups = (new \DOMXPath($dom))->query('//div[contains(concat(" ", normalize-space(@class), " "), " wp-block-columns ")]');
    211275        foreach (array_reverse(iterator_to_array($groups)) as $group) {
    212276            $groupStyles = self::styles($group->getAttribute('style'));
    213             $gap = preg_split('/\s+/', self::plain($groupStyles['gap'] ?? '40px'));
     277            $gap = array_values(array_filter(newsletterStyleInliner::split(self::plain($groupStyles['gap'] ?? '40px'), ' '), 'strlen'));
    214278            $columnGap = $groupStyles['column-gap'] ?? ($gap[1] ?? $gap[0]);
    215279            $rowGap = $groupStyles['row-gap'] ?? $gap[0];
    … …  
    239303                foreach ($column->attributes as $attribute) { $cell->setAttribute($attribute->name, $attribute->value); }
    240304                $css = self::styles($cell->getAttribute('style'));
    241                 $cell->setAttribute('data-en-column-basis', $css['flex-basis'] ?? $css['width'] ?? '');
     305                $cell->setAttribute('data-en-column-basis', $cell->getAttribute('data-en-author-basis') ?: ($css['flex-basis'] ?? $css['width'] ?? ''));
     306                $cell->setAttribute('data-en-column-grow', $cell->hasAttribute('data-en-author-basis') ? '0' : ($css['flex-grow'] ?? '1'));
     307                $cell->removeAttribute('data-en-author-basis');
    242308                unset($css['flex-basis']);
     309                $alignment = $groupStyles['align-items'] ?? 'top';
     310                foreach (['top', 'center', 'bottom'] as $position) {
     311                    if (preg_match('/\bis-vertically-aligned-' . $position . '\b/', $group->getAttribute('class'))) { $alignment = $position; }
     312                }
     313                // A per-column choice takes precedence over the group choice.
     314                $alignment = $css['align-self'] ?? $alignment;
     315                foreach (['top', 'center', 'bottom'] as $position) {
     316                    if (preg_match('/\bis-vertically-aligned-' . $position . '\b/', $column->getAttribute('class'))) { $alignment = $position; }
     317                }
     318                $alignment = match ($alignment) { 'center' => 'middle', 'bottom', 'flex-end', 'end' => 'bottom', default => 'top' };
     319                $css['vertical-align'] = $alignment;
     320                $cell->setAttribute('valign', $alignment);
    243321                $cell->setAttribute('style', \CSSIN::arrayToStyle($css));
    244322                while ($column->firstChild) { $cell->appendChild($column->firstChild); }
    … …  
    247325            $group->parentNode->replaceChild($table, $group);
    248326        }
    249         foreach ($dom->getElementsByTagName('img') as $image) {
    250             $styles = self::styles($image->getAttribute('style'));
    251             $intent = [];
    252             foreach (['width', 'height', 'aspect-ratio', 'object-fit', 'object-position'] as $key) {
    253                 if (isset($styles[$key])) { $intent[$key] = $styles[$key]; }
    254             }
    255             // Older Gutenberg versions put intentional dimensions in attributes.
    256             $parent = $image->parentNode;
    257             while ($parent instanceof \DOMElement && in_array($parent->tagName, ['a', 'figure', 'div'], true)) {
    258                 if (preg_match('/\bis-resized\b/', $parent->getAttribute('class'))) {
    259                     foreach (['width', 'height'] as $key) {
    260                         if (!isset($intent[$key]) && preg_match('/^[1-9]\d*$/', $image->getAttribute($key))) {
    261                             $intent[$key] = $image->getAttribute($key) . 'px';
    262                         }
    263                     }
    264                     break;
    265                 }
    266                 $parent = $parent->parentNode;
    267             }
    268             $image->setAttribute('data-en-image-intent', json_encode($intent));
    269         }
    270         return $dom->saveHTML();
    271     }
    272 
    273     public static function transform(string $html, int $width): string {
     327    }
     328
     329    public static function transform(string $html, int $width, bool $convertColumns = false, ?int $wideWidth = null, int $sideSpacing = 0): string {
    274330        if (trim($html) === '') { return $html; }
    275331        $dom = self::load($html);
     332        $fluidSections = $wideWidth !== null && newsletterBlockLayout::apply($dom, self::contentWidth($width), $wideWidth >= 200 && $wideWidth <= 2000 ? $wideWidth : 1100, self::sideSpacing($sideSpacing));
     333        if ($convertColumns) { self::prepareColumns($dom); }
    276334        self::wrapColumnPadding($dom);
    277335        self::centerImages($dom);
    … …  
    282340        }
    283341        $instance = new self();
    284         $instance->layoutWidth = self::contentWidth($width);
     342        $instance->layoutWidth = $fluidSections ? max(self::contentWidth($width), $wideWidth >= 200 && $wideWidth <= 2000 ? $wideWidth : 1100) : self::contentWidth($width);
     343        $rootStyles = self::styles($dom->documentElement->getAttribute('style'));
     344        $instance->rootFont = self::length($rootStyles['font-size'] ?? '', 16, 16) ?? 16;
    285345        $instance->layoutRoot = (new \DOMXPath($dom))->query('//table[contains(concat(" ", normalize-space(@class), " "), " enl-wrapper ")]')->item(0);
    286         if ($instance->layoutRoot) {
     346        if ($fluidSections && $instance->layoutRoot) {
     347            $instance->layoutWidth = (int)$instance->layoutRoot->getAttribute('data-en-reference-width');
     348            $instance->layoutRoot->removeAttribute('data-en-reference-width');
     349        }
     350        if ($instance->layoutRoot && !$fluidSections) {
    287351            $rootStyles = self::styles($instance->layoutRoot->getAttribute('style'));
    288352            $instance->layoutWidth = (int)round(min($instance->layoutWidth,
    … …  
    298362        }
    299363        $instance->walk($dom->documentElement, $instance->layoutWidth);
    300         $instance->outlookFrame();
     364        if (!$fluidSections) { $instance->outlookFrame(); }
     365        foreach ((new \DOMXPath($dom))->query('//*[@data-en-author-padding or @data-en-fluid-sections]') as $node) {
     366            $node->removeAttribute('data-en-author-padding'); $node->removeAttribute('data-en-fluid-sections');
     367        }
    301368        return $dom->saveHTML();
    302369    }
    … …  
    340407
    341408    private static function paddingPixels(array $styles, float $reference, float $font): string {
    342         $parts = preg_split('/\s+/', self::plain($styles['padding'] ?? '0'));
     409        $parts = array_values(array_filter(newsletterStyleInliner::split(self::plain($styles['padding'] ?? '0'), ' '), 'strlen'));
    343410        $values = [$parts[0], $parts[1] ?? $parts[0], $parts[2] ?? $parts[0], $parts[3] ?? $parts[1] ?? $parts[0]];
    344411        foreach (['top', 'right', 'bottom', 'left'] as $index => $side) {
    … …  
    443510        }
    444511        $styles = self::styles($node->getAttribute('style'));
     512        foreach ($styles as $name => &$value) {
     513            if (!str_starts_with($name, '--') && !in_array($name, ['font-family', 'content'], true)) {
     514                $value = newsletterCSSLength::resolveRem($value, $node->tagName === 'html' && $name === 'font-size' ? 16 : $this->rootFont);
     515            }
     516        }
     517        unset($value);
    445518        if ($node->tagName === 'table' && preg_match('/\bwp-block-columns\b/', $node->getAttribute('class'))) {
    446519            $styles['display'] = 'table';
    … …  
    448521        }
    449522        $font = self::length($styles['font-size'] ?? '', $font, $font) ?? $font;
     523        foreach ($styles as $property => $value) {
     524            if (!preg_match('/^(?:padding|margin)(?:-(?:top|right|bottom|left))?$|^(?:font-size|width|height|min-width|max-width|min-height|max-height)$/', $property)
     525                || !preg_match('/(?:calc|clamp|min|max)\(/i', $value)) { continue; }
     526            $parts = array_values(array_filter(newsletterStyleInliner::split(self::plain($value), ' '), 'strlen'));
     527            foreach ($parts as &$part) {
     528                $pixels = newsletterCSSLength::pixels($part, $available, $font, $this->layoutWidth);
     529                if ($pixels !== null) { $part = round($pixels, 3) . 'px'; }
     530            }
     531            unset($part);
     532            $styles[$property] = implode(' ', $parts);
     533        }
    450534        if (in_array($node->tagName, ['td', 'th'], true)) {
    451535            $paddingStyles = $styles;
     536            if ($node->hasAttribute('data-en-outlook-padding')) {
     537                $paddingStyles = json_decode($node->getAttribute('data-en-outlook-padding'), true) ?: [];
     538                $node->removeAttribute('data-en-outlook-padding');
     539            }
    452540            if (strpos(' ' . $node->getAttribute('class') . ' ', ' en-button-cell ') !== false) {
    453541                $link = $node->getElementsByTagName('a')->item(0);
    … …  
    486574        $width = $allocated ?? min($available, self::length($styles['width'] ?? $node->getAttribute('width'), $available, $font) ?? $available);
    487575        $width = min($width, self::length($styles['max-width'] ?? '', $available, $font) ?? $width);
    488         $padding = preg_split('/\s+/', self::plain($styles['padding'] ?? '0'));
     576        $padding = array_values(array_filter(newsletterStyleInliner::split(self::plain($styles['padding'] ?? '0'), ' '), 'strlen'));
    489577        $left = $styles['padding-left'] ?? ($padding[3] ?? $padding[1] ?? $padding[0]);
    490578        $right = $styles['padding-right'] ?? ($padding[1] ?? $padding[0]);
    … …  
    545633        $gap = $count > 1 ? min($gap, max(0, ($width - $count) / ($count - 1))) : 0;
    546634        $contentWidth = $width - $gap * ($count - 1);
    547         $weights = []; $used = 0; $auto = 0;
     635        $weights = []; $growth = []; $used = 0;
    548636        foreach ($columns as $column) {
    549637            $basis = self::length($column->getAttribute('data-en-column-basis'), $contentWidth, $font);
    550             $weights[] = $basis;
    551             if ($basis === null) { $auto++; } else { $used += $basis; }
    552         }
    553         foreach ($weights as &$weight) { $weight = $weight ?? max(1, $contentWidth - $used) / max(1, $auto); }
    554         unset($weight);
     638            $weights[] = $basis ?? 0;
     639            $growth[] = max(0, (float)$column->getAttribute('data-en-column-grow'));
     640            $used += $basis ?? 0;
     641        }
     642        $free = max(0, $contentWidth - $used);
     643        $totalGrowth = array_sum($growth);
     644        if ($totalGrowth > 0) {
     645            foreach ($weights as $index => &$weight) { $weight += $free * $growth[$index] / $totalGrowth; }
     646            unset($weight);
     647        }
    555648        $sum = array_sum($weights);
    556649        if ($sum <= 0) { $weights = array_fill(0, $count, 1); $sum = $count; }
    … …  
    568661            $cellWidth = $contentWidth * $fraction;
    569662            $cell->removeAttribute('data-en-column-basis');
     663            $cell->removeAttribute('data-en-column-grow');
    570664            // Numeric desktop fallback plus proportional widths for narrow clients.
    571665            // Gutters scale with the row when columns stay side by side.
    … …  
    624718        // Keep tighter author caps (e.g. width:100%; max-width:200px).
    625719        $styles['max-width'] = $styles['max-width'] ?? '100%';
     720        if (!$fixed && strpos($widthCSS, '%') === false) {
     721            // Automatic images must shrink through width itself: Thunderbird can
     722            // override max-width with its message stylesheet. Keep the computed
     723            // desktop size as a cap so small intrinsic images are not enlarged.
     724            $cap = self::plain($styles['max-width']);
     725            $styles['width'] = preg_match('/^(\d+(?:\.\d+)?)%$/', $cap, $match)
     726                ? min(100, (float)$match[1]) . '%' : '100%';
     727            $styles['max-width'] = $width . 'px';
     728        }
    626729        $styles['height'] = 'auto';
    627730        unset($styles['object-fit'], $styles['object-position'], $styles['aspect-ratio']);
  • easynewsletter/trunk/src/core/menuPage.php

    r3696327 r3730982  
    5454                        'easyNewsletter',
    5555                        [$this, "menuPageContent"]);
     56        add_submenu_page(
     57            'easyNewsletter',
     58            __('Overview', 'easynewsletter'),
     59            __('Overview', 'easynewsletter'),
     60            'manage_options',
     61            'easyNewsletter',
     62            [$this, 'menuPageContent'],
     63            0
     64        );
    5665        }
    5766
    … …  
    118127        public function settingsPageContent(): void{
    119128                //add settingsPage.js script to this site
    120                 wp_enqueue_script( "en_settingsPageJS", plugins_url( 'resources/settingsPage.js', __FILE__ ) );
     129                wp_enqueue_script( "en_settingsPageJS", plugins_url( 'resources/settingsPage.js', __FILE__ ), ['jquery'], filemtime(__DIR__ . '/resources/settingsPage.js'), true );
    121130
    122131                include ('settingsPageContent.php');
    … …  
    135144                // styles for menu icon
    136145                wp_enqueue_style("en_menuIconCSS", plugins_url('resources/menuIcon.css', __FILE__ ));
    137                 wp_enqueue_style("en_admin", plugins_url('resources/admin.css', __FILE__ ));
     146                wp_enqueue_style("en_admin", plugins_url('resources/admin.css', __FILE__), [], filemtime(__DIR__ . '/resources/admin.css'));
    138147        }
    139148
  • easynewsletter/trunk/src/core/newsletterPageTemplate.php

    r3710879 r3730982  
    4747        </nav>
    4848    </header>
    49     <iframe title="<?php echo $outlook ? 'Outlook-Simulation' : 'Standard-Vorschau'; ?>" sandbox="allow-popups allow-popups-to-escape-sandbox" srcdoc="<?php echo esc_attr($html); ?>"></iframe>
     49    <iframe title="<?php echo $outlook ? 'Outlook-Simulation' : 'Standard-Vorschau'; ?>" sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox" srcdoc="<?php echo esc_attr($html); ?>"></iframe>
    5050</body>
    5151</html>
  • easynewsletter/trunk/src/core/newsletterPostType.php

    r3710879 r3730982  
    2828         */
    2929        public function init(): void {
     30        newsletterThemeStyles::init();
    3031                add_action('init', array($this, 'createNewsletterPostType'));
    3132                add_filter( 'manage_en_newsletters_posts_columns', array( $this, "addBackendNewsletterColumns" ) );
    … …  
    3738                add_action( 'admin_enqueue_scripts', array($this, 'enqueue_script'), 11 );
    3839        add_filter('block_editor_settings_all', [$this, 'newsletterEditorStyles'], 20, 2);
     40        add_filter('post_row_actions', [$this, 'newsletterRowActions'], PHP_INT_MAX, 2);
     41        add_filter('page_row_actions', [$this, 'newsletterRowActions'], PHP_INT_MAX, 2);
     42        foreach (['manage_en_newsletters_posts_columns', 'manage_edit-en_newsletters_columns', 'manage_edit-en_newsletters_sortable_columns'] as $hook) {
     43            add_filter($hook, [$this, 'removeNewsletterSEOColumns'], PHP_INT_MAX);
     44        }
    3945
    4046                add_action( 'wp_ajax_en_sendNewsletterTestMail', array( $this,'en_sendNewsletterTestMail'));
    … …  
    6167                add_action('admin_enqueue_scripts', [$this, "add_script_to_overview"]);
    6268                add_action( 'wp_ajax_en_copyNewsletter', array( $this,'en_copyNewsletter'));
    63                 add_action( 'wp_ajax_nopriv_en_copyNewsletter', array( $this,'en_copyNewsletter'));
    6469
    6570                metaBoxes::addAllMetaBoxes();
    … …  
    8792                                'labels'       => array(
    8893                                        'name'          => __( 'Newsletter' ),
    89                                         'singular_name' => __( 'Newsletter' )
     94                                        'singular_name' => __( 'Newsletter' ),
     95                    'add_new' => __('Neuer Newsletter', 'easynewsletter'),
     96                    'add_new_item' => __('Neuer Newsletter', 'easynewsletter')
    9097                                ),
    9198                                'public'       => true,
    … …  
    268275                if( 'en_newsletters' == $post_type ) {
    269276            $resources_url = EASY_NEWSLETTER_URL . 'src/core/resources/';
    270                         wp_enqueue_script( 'newsletters-admin-edit-script', $resources_url . 'newslettersPostTypeAdmin.js', [ 'jquery', 'wp-i18n']);
     277                        wp_enqueue_script( 'newsletters-admin-edit-script', $resources_url . 'newslettersPostTypeAdmin.js', [ 'jquery', 'wp-i18n'], filemtime(__DIR__ . '/resources/newslettersPostTypeAdmin.js'), true);
    271278            wp_set_script_translations("newsletters-admin-edit-script",'easynewsletter',plugin_dir_path(__FILE__) . 'resources/language');
    272279
    … …  
    294301            return $settings;
    295302        }
     303        $db = databaseConnector::instance();
     304        $contentWidth = generateEmailHtml::contentWidth($db->getSettingFromDB('newsletterContentWidth'));
     305        $wideWidth = filter_var($db->getSettingFromDB('newsletterWideWidth'), FILTER_VALIDATE_INT,
     306            ['options' => ['min_range' => 200, 'max_range' => 2000]]) ?: 1100;
     307        $wideWidth = max($contentWidth, $wideWidth);
     308        // Gutenberg uses these settings for inherited Group layouts and controls.
     309        $settings['__experimentalFeatures']['layout']['contentSize'] = $contentWidth . 'px';
     310        $settings['__experimentalFeatures']['layout']['wideSize'] = $wideWidth . 'px';
    296311        $settings['styles'][] = ['css' => file_get_contents(__DIR__ . '/resources/newsletterBaseStyle.css')];
     312        if (newsletterThemeStyles::enabled()) {
     313            // Theme fonts may be enqueued only on the frontend. Include their
     314            // absolute @font-face sources in the editor canvas as well.
     315            $themeCSS = newsletterStyleInliner::emailCSS(newsletterThemeStyles::css((int)$context->post->ID));
     316            [, $fonts] = \CSSIN::splitFontFace($themeCSS);
     317            if ($fonts !== '') { $settings['styles'][] = ['css' => $fonts]; }
     318        }
     319        // Override theme-generated canvas widths without touching other post
     320        // types, nested columns or explicit per-group content-size choices.
     321        $settings['styles'][] = ['css' => "
     322            .editor-styles-wrapper {
     323                --wp--style--global--content-size: {$contentWidth}px !important;
     324                --wp--style--global--wide-size: {$wideWidth}px !important;
     325            }
     326            .editor-styles-wrapper .is-root-container > .wp-block:not([data-align=wide]):not([data-align=full]):not(.alignwide):not(.alignfull),
     327            .editor-styles-wrapper .editor-post-title {
     328                max-width: {$contentWidth}px;
     329                margin-left: auto;
     330                margin-right: auto;
     331            }
     332            .editor-styles-wrapper .is-root-container > .wp-block[data-align=wide],
     333            .editor-styles-wrapper .is-root-container > .wp-block.alignwide {
     334                max-width: {$wideWidth}px;
     335                margin-left: auto;
     336                margin-right: auto;
     337            }
     338            .editor-styles-wrapper .is-root-container > .wp-block[data-align=full],
     339            .editor-styles-wrapper .is-root-container > .wp-block.alignfull {
     340                max-width: none;
     341            }
     342        "];
    297343        $customCSS = databaseConnector::instance()->getSettingFromDB('newsletterCSS');
    298344        if (is_string($customCSS) && trim($customCSS) !== '') {
    … …  
    305351        function remove_editor_styles_for_post_type(): void {
    306352                global $post_type;
    307                 if( 'en_newsletters' == $post_type ) {
     353                if( 'en_newsletters' == $post_type && !newsletterThemeStyles::enabled() ) {
    308354                remove_theme_support('editor-styles');
    309355            }
    … …  
    419465
    420466                        $postID = sanitize_key($_POST["post_id"]);
    421                         $subject = get_post_meta($postID,"en_subject", true);
     467                        $subject = "[Test] " . get_post_meta($postID,"en_subject", true);
    422468                        $userID = get_current_user_id();
    423469
    … …  
    508554                $posttype = $_GET["post_type"] ?? null;
    509555                if ('edit.php' === $hook && $posttype == "en_newsletters") {
    510                         wp_enqueue_script( 'newsletters-overview-script', EASY_NEWSLETTER_URL . 'src/core/resources/newslettersPostTypeOverview.js');
    511                 }
    512         }
    513 
    514         public function en_copyNewsletter(): void {
    515                 check_ajax_referer( 'secure_nonce_name', 'security' );
    516                 try {
    517                         $post = get_post($_POST["post_id"]);
    518 
    519                         //make a duplicate from the post
    520                         $new_post_id = wp_insert_post( array(
    521                         'post_title'            => $post->post_title . ' (Copy)',
    522                         'post_content'          => $post->post_content,
    523                         'post_status'           => $post->post_status,
    524                         'post_type'             => $post->post_type,
    525                         'post_author'           => $post->post_author,
    526                         'post_parent'           => $post->post_parent,
    527                         'post_excerpt'          => $post->post_excerpt,
    528                         'post_date'             => $post->post_date,
    529                         'post_date_gmt'         => $post->post_date_gmt,
    530                         'menu_order'            => $post->menu_order,
    531                         'comment_status'        => $post->comment_status,
    532                         'ping_status'           => $post->ping_status,
    533                         'post_password'         => $post->post_password,
    534                         'to_ping'               => $post->to_ping,
    535                         'pinged'                => $post->pinged,
    536                         'post_content_filtered' => $post->post_content_filtered,
    537                         'post_category'         => $post->post_category,
    538                         'tags_input'            => $post->tags_input,
    539                         'tax_input'             => $post->tax_input,
    540                         'meta_input'            => $post->meta_input,
    541                 ) );
    542 
    543                         //Copy custom Meta keys to the new post
    544                         $custom_keys = get_post_custom_keys($_POST["post_id"]);
    545                         foreach ( $custom_keys as $key ) {
    546                         $value = get_post_meta( $_POST["post_id"], $key, true );
    547                         update_post_meta( $new_post_id, $key, $value );
    548                 }
    549 
    550 
    551                 } catch (\Error | Exception $e){
    552             Log::logError($e->getMessage());
    553                 }
    554         }
     556                        wp_enqueue_script( 'newsletters-overview-script', EASY_NEWSLETTER_URL . 'src/core/resources/newslettersPostTypeOverview.js', ['jquery'], filemtime(__DIR__ . '/resources/newslettersPostTypeOverview.js'));
     557                }
     558        }
     559
     560    /** Registered only on newsletter column hooks, including Screen Options. */
     561    public function removeNewsletterSEOColumns(array $columns): array {
     562        foreach (array_keys($columns) as $key) {
     563            if ($key === 'tsf-seo-bar-wrap' || str_starts_with($key, 'wpseo-')) {
     564                unset($columns[$key]);
     565            }
     566        }
     567        return $columns;
     568    }
     569
     570    /** Keep one predictable copy workflow in the newsletter list only. */
     571    public function newsletterRowActions(array $actions, $post): array {
     572        if (($post->post_type ?? '') !== 'en_newsletters') { return $actions; }
     573        // WordPress uses these action keys as the surrounding span classes.
     574        // Duplicate Page; Copy & Delete Everything (copy-delete-posts);
     575        // Yoast Duplicate Post; Duplicate Page and Post; WP Duplicate Page; Post Duplicator.
     576        foreach (['duplicate', 'cdp_copy', 'clone', 'edit_as_new_draft', 'rewrite', 'dpp', 'njt_duplicate_page', 'duplicate_post', 'w3tc_flush_post'] as $key) {
     577            unset($actions[$key]);
     578        }
     579        return $actions;
     580    }
     581
     582    public function en_copyNewsletter(): void {
     583        check_ajax_referer('secure_nonce_name', 'security');
     584        $postID = absint($_POST['post_id'] ?? 0);
     585        $post = get_post($postID);
     586        $type = get_post_type_object('en_newsletters');
     587        if (!$post || $post->post_type !== 'en_newsletters'
     588            || !current_user_can('edit_post', $postID)
     589            || !$type || !current_user_can($type->cap->create_posts)) {
     590            wp_send_json_error(['message' => __('Newsletter kann nicht kopiert werden.', 'easynewsletter')], 403);
     591            return;
     592        }
     593        $newID = self::duplicateNewsletter($post);
     594        if (is_wp_error($newID)) {
     595            wp_send_json_error(['message' => $newID->get_error_message()], 500);
     596            return;
     597        }
     598        wp_send_json_success(['post_id' => $newID]);
     599    }
     600
     601    /** Copy editorial content, never the original publication or delivery state. */
     602    public static function duplicateNewsletter(\WP_Post $post) {
     603        $newID = wp_insert_post(wp_slash([
     604            'post_title' => $post->post_title . ' (Copy)',
     605            'post_content' => $post->post_content,
     606            'post_excerpt' => $post->post_excerpt,
     607            'post_status' => 'draft',
     608            'post_type' => 'en_newsletters',
     609            'post_author' => get_current_user_id(),
     610            'post_parent' => $post->post_parent,
     611            'post_date' => current_time('mysql'),
     612            // A floating draft date lets WordPress use publication time later,
     613            // unless the editor explicitly chooses a publication date.
     614            'post_date_gmt' => '0000-00-00 00:00:00',
     615            'comment_status' => $post->comment_status,
     616            'ping_status' => $post->ping_status,
     617            'post_password' => $post->post_password,
     618        ]), true);
     619        if (is_wp_error($newID)) { return $newID; }
     620
     621        $excluded = ['_edit_lock', '_edit_last', '_wp_old_slug', '_wp_old_date',
     622            '_wp_trash_meta_status', '_wp_trash_meta_time', '_wp_desired_post_slug',
     623            'en_scheduleLock', 'lockStatus', 'en_alreadySend', 'en_archiveContent'];
     624        foreach (get_post_meta($post->ID) as $key => $values) {
     625            if (in_array($key, $excluded, true)) { continue; }
     626            foreach ($values as $value) {
     627                add_post_meta($newID, $key, wp_slash(maybe_unserialize($value)));
     628            }
     629        }
     630        foreach (get_object_taxonomies($post->post_type) as $taxonomy) {
     631            $terms = wp_get_object_terms($post->ID, $taxonomy, ['fields' => 'ids']);
     632            if (!is_wp_error($terms)) { wp_set_object_terms($newID, $terms, $taxonomy); }
     633        }
     634        return $newID;
     635    }
    555636}
  • easynewsletter/trunk/src/core/outlookPreview.php

    r3710879 r3730982  
    33
    44require_once __DIR__ . '/resources/vendor/cssin/CSSIN.php';
     5require_once __DIR__ . '/newsletterCSSLength.php';
    56
    67/** Preview-only approximation of Classic Outlook for Windows, after CSSIN. */
    … …  
    3233            $node->parentNode->removeChild($node);
    3334        }
     35        $rootFont = 16.0;
    3436        foreach ($xpath->query('//*') as $node) {
    3537            $parser = new \csstidy();
    … …  
    4244                }
    4345            }
     46            if (strtolower($node->tagName) === 'html' && isset($styles['font-size'])) {
     47                $rootFont = newsletterCSSLength::pixels($styles['font-size'], 16, 16) ?? 16;
     48            }
     49            foreach ($styles as $name => &$value) {
     50                if (!str_starts_with($name, '--') && !in_array($name, ['font-family', 'content'], true)) {
     51                    $value = newsletterCSSLength::resolveRem($value, $node->tagName === 'html' && $name === 'font-size' ? 16 : $rootFont);
     52                }
     53            }
     54            unset($value);
    4455            unset($styles['max-width']);
    4556            if (isset($styles['mso-padding-alt'])) {
    … …  
    5364                $plainProperty = preg_replace('/^-(webkit|moz|ms|o)-/', '', strtolower($property));
    5465                if (preg_match('/^border-.*radius(?:-.*)?$/', $plainProperty)
    55                     || in_array($plainProperty, ['box-shadow', 'text-shadow', 'gap', 'row-gap', 'column-gap', 'order'], true)
     66                    || in_array($plainProperty, ['box-shadow', 'text-shadow', 'gap', 'row-gap', 'column-gap', 'order', 'transform', 'transform-origin', 'transform-style', 'rotate', 'translate', 'scale', 'perspective', 'perspective-origin', 'backface-visibility'], true)
    5667                    || preg_match('/^(flex|grid|align|justify|place)(-|$)/', $plainProperty)) {
    5768                    unset($styles[$property]);
  • easynewsletter/trunk/src/core/registration/registration.php

    r3710879 r3730982  
    5151         */
    5252        public function init(): void {
     53        add_action('init', [registrationBlock::class, 'register']);
    5354                add_shortcode('easyNewsletter', array($this, 'registrationFormFilter'));
    5455                add_shortcode('easyNewsletterUnsubscribeForm', array($this, 'easyNewsletterUnsubscribeForm'));
     56        add_action('wp_enqueue_scripts', static function () {
     57            wp_enqueue_style('en-registration-form', plugins_url('../resources/registrationForm.css', __FILE__), array(), '1.0.0');
     58        });
    5559        }
    5660
    … …  
    188192        public function registrationFormFilter($attributes): string|bool {
    189193
     194        return $this->renderForm($attributes);
     195    }
     196
     197    public function renderForm($attributes, bool $preview = false): string {
     198        static $instance = 0;
     199        $formInstance = ++$instance;
     200        $idSuffix = $formInstance === 1 ? '' : '_' . $formInstance;
     201        if (!$preview) {
     202            wp_enqueue_script('en-signup-form', plugins_url('../resources/signupForm.js', __FILE__), [], (string) filemtime(__DIR__ . '/../resources/signupForm.js'), true);
     203        }
    190204        $attributes = shortcode_atts(array(
    191205            'jump' => false,
    192206            'category' => 'default',
    193207            'redirect' => false,
     208            'layout' => '',
    194209            'registration_success_page_id' => 0
    195210        ), $attributes);
    … …  
    197212        $availableSubscriberCategories = databaseConnector::instance()->getSettingFromDB("subscriberCategory");
    198213        $availableSubscriberCategories = unserialize($availableSubscriberCategories);
    199         if (!in_array($attributes['category'], $availableSubscriberCategories)){
     214        if (!in_array($attributes['category'], (array) $availableSubscriberCategories, true)){
    200215            $attributes['category'] = 'default';
    201216        }
    … …  
    212227                $token = sanitize_text_field($_GET['token'] ?? "");
    213228
    214         if ($attributes['jump'] == "true" || $shouldRedirect){
     229        if (!$preview && (wp_validate_boolean($attributes['jump']) || $shouldRedirect)){
    215230            wp_enqueue_script(
    216231                'custom-registration-script', // Handle
    … …  
    223238       
    224239                ob_start();
    225         if (empty($_GET) ||
     240        if ($preview || empty($_GET) ||
    226241                    !empty($confirmed) &&
    227242                    !empty($unsubscribed) &&
  • easynewsletter/trunk/src/core/registration/registrationForm.php

    r3710879 r3730982  
    66use Farn\EasyNewsletter\core\subscriberHandler;
    77
    8 if (!empty(sanitize_text_field( $_REQUEST['submit'] ?? ""))){
     8if (!$preview && !empty($_POST['submit']) && (isset($_POST['en_form_instance']) ? (string) $_POST['en_form_instance'] === (string) $formInstance : $formInstance === 1)){
    99
    1010    $registrationSuccessPageContent = get_the_content("", false, $registrationSuccessPageID);
    … …  
    1414        : '';
    1515
    16     if ( ($_POST['exitInput'] != "") || ($_POST['human_check'] != date('Y-m-d')) ){
     16    if ( (($_POST['exitInput'] ?? '') != "") || (($_POST['human_check'] ?? '') != date('Y-m-d')) ){
    1717        echo '<div id="en_registrationJumpContainer"' . $registrationSuccessRedirectAttribute . '>';
    1818        echo $registrationSuccessPageContent;
    … …  
    2323    $metaFields = array();
    2424
    25     foreach ($_REQUEST as $field => $input){
     25    foreach ($_POST as $field => $input){
    2626        if ($field == "en_subscriberCategory"){
    27             $metaFields[$field] = [sanitize_text_field($input)];
    28         } else if ($field != 'submit'){
     27            $metaFields[$field] = [$attributes['category']];
     28        } else if ($field != 'submit' && $field != 'en_form_instance' && is_scalar($input)){
    2929            $metaFields[$field] = sanitize_text_field($input);
    3030        }
    … …  
    5252ob_end_clean();
    5353
    54 $content = str_replace("{{registrationForm}}", $formContent , $content);
     54// A block-level form cannot live inside the paragraph used by older templates.
     55// Remove only the paragraph wrapping our placeholder, preserving surrounding copy.
     56$content = preg_replace('/<p\b[^>]*>\s*\{\{registrationForm\}\}\s*<\/p>/i', '{{registrationForm}}', $content);
     57// Avoid automatic paragraph insertion at template-only whitespace between tags.
     58$formContent = preg_replace('/>\s+</', '><', trim($formContent));
     59$content = str_replace("{{registrationForm}}", $formContent, $content);
    5560
    5661//No escape because we want to get the raw content
  • easynewsletter/trunk/src/core/registration/registrationFormContent.php

    r3495745 r3730982  
    11<?php
    2 
    32namespace Farn\EasyNewsletter\core\registration;
    4 
    53use Farn\EasyNewsletter\core\databaseConnector;
    64
     5$signupCheckboxText = wp_kses_post(databaseConnector::instance()->getSettingFromDB('signupCheckboxText'));
     6if (trim($signupCheckboxText) === '') {
     7    $signupCheckboxText = esc_html__('I am a human being', 'easynewsletter');
     8}
     9$inlineLayout = ($attributes['layout'] ?? '') === 'inline';
     10$submitButton = '<input type="submit" class="button button-primary" name="submit" id="submit' . esc_attr($idSuffix) . '" value="' . esc_attr__('Subscribe', 'easynewsletter') . '">';
     11$inlineSubmitRendered = false;
     12$signupFormFields = (array) maybe_unserialize(databaseConnector::instance()->getSettingFromDB('signupFormFields'));
    713?>
    8 
    914<div class="en_signup_wrapper">
    10         <form method="post" name="en_signup_form" id="en_signup_form" class="en_signup_form">
    11                 <?php
    12                 // get values from database/settings-page
    13 
    14 
    15         $category = isset($attributes) ? $attributes[ 'category'] : "default";
    16 
    17                 $signupFormFields = databaseConnector::instance()->getSettingFromDB('signupFormFields');
    18                 $signupFormFields = unserialize($signupFormFields);
    19                 if($signupFormFields !== false){
    20                         foreach ($signupFormFields as $signupFormField){
    21                                 echo match ($signupFormField){
    22                                         "en_eMailAddress" => "<label for='" . esc_attr($signupFormField) . "'>" . __('E-Mail','easynewsletter'),
    23                                         "en_salutation" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Salutation','easynewsletter'),
    24                                         "en_gender" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Gender','easynewsletter'),
    25                                         "en_firstName" => "<label for='" . esc_attr($signupFormField) . "'>" . __('First Name', 'easynewsletter'),
    26                                         "en_lastName" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Last Name','easynewsletter'),
    27                                         "en_telephoneNumber" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Telephone Number', 'easynewsletter'),
    28                                         "en_eMailAddressValidation" => "<label for='" . esc_attr($signupFormField) . "'>" . __('Validate E-Mail', 'easynewsletter'),
    29                     "en_securityQuestion" => "",
    30                                         default => "<label for='" . esc_attr($signupFormField) . "'>".$signupFormField
    31                                 };
    32 
    33                                 echo match ( $signupFormField ) {
    34                                         'en_eMailAddress', "en_eMailAddressValidation"=> "<input type='email' name='" . esc_attr($signupFormField) . "' id='" . esc_attr($signupFormField) . "' value='' required class='en_inputCheck' oninput='en_validationCheck()'>",
    35                                         'telephoneNumber'=> "<input type=' tel' name='" . esc_attr($signupFormField) . "' id='" . esc_attr($signupFormField) . "' value='' required class='en_inputCheck'>",
    36                                         "en_securityQuestion" => "",
    37                                         default => "<input type='text' name='" . esc_attr($signupFormField) . "' id='" . esc_attr($signupFormField) . "' value='' required>",
    38                                 };
    39                                 echo "</label>";
    40                         }
    41                         echo "<input style='display: none' name='exitInput' id='exitInput'>";
    42             echo "<input type='hidden' name='en_subscriberCategory' value='". $category ."'>";
    43         }
    44                 ?>
    45         <br>
    46                 <div id="en_human_check_div"><input type="checkbox" name="human_check" id="human_check" value=""><label for="human_check"><?php _e("I am a human being", 'easynewsletter')?></label></div>
    47                 <input type="submit" class="button button-primary" name="submit" id="submit" value="<?php esc_html_e ("Subscribe", 'easynewsletter')?>">
    48 
    49 
    50                 <script>
    51                     document.getElementById('human_check').addEventListener('click', function() {
    52                         if (this.checked) {
    53                             const currentDate = new Date().toISOString().slice(0, 10);
    54                             this.value = currentDate;
    55                         } else {
    56                             this.value = '';
    57                         }
    58                     });
    59                 </script>
    60 
    61 
    62     </form>
     15<form method="post" name="en_signup_form<?php echo esc_attr($idSuffix); ?>" id="en_signup_form<?php echo esc_attr($idSuffix); ?>" class="en_signup_form">
     16<?php if ($preview) { echo '<fieldset disabled style="border:0;padding:0;margin:0;min-width:0">'; } ?>
     17<?php foreach ($signupFormFields as $field):
     18    if (!is_string($field) || $field === 'en_securityQuestion') { continue; }
     19    $inlineEmail = $inlineLayout && $field === 'en_eMailAddress';
     20    if ($inlineEmail) { echo '<div class="en_signup_inline">'; }
     21    $type = in_array($field, ['en_eMailAddress', 'en_eMailAddressValidation'], true) ? 'email' : ($field === 'en_telephoneNumber' ? 'tel' : 'text');
     22    echo '<label for="' . esc_attr($field . $idSuffix) . '">' . esc_html(registrationBlock::fieldLabel($field));
     23    echo '<input type="' . $type . '" name="' . esc_attr($field) . '" id="' . esc_attr($field . $idSuffix) . '" value="" required class="en_inputCheck"></label>';
     24    if ($inlineEmail) { echo $submitButton . '</div>'; $inlineSubmitRendered = true; }
     25endforeach; ?>
     26<input style="display:none" name="exitInput" id="exitInput<?php echo esc_attr($idSuffix); ?>" tabindex="-1" autocomplete="off">
     27<input type="hidden" name="en_subscriberCategory" value="<?php echo esc_attr($attributes['category']); ?>">
     28<input type="hidden" name="en_form_instance" value="<?php echo esc_attr($formInstance); ?>">
     29<?php if (!$inlineLayout) { echo '<br>'; } ?>
     30<div class="en_human_check_div" id="en_human_check_div<?php echo esc_attr($idSuffix); ?>"><input type="checkbox" name="human_check" id="human_check<?php echo esc_attr($idSuffix); ?>" value="" required><label for="human_check<?php echo esc_attr($idSuffix); ?>"><?php echo $signupCheckboxText; ?></label></div>
     31<?php if (!$inlineSubmitRendered) { echo $submitButton; } ?>
     32<?php if ($preview) { echo '</fieldset>'; } ?>
     33</form>
    6334</div>
    64 
    65 <?php
    66 if (in_array("en_eMailAddressValidation", $signupFormFields)) {
    67         wp_add_inline_script("en_registrationValidation", "
    68         const submitButton = document.getElementById('submit')
    69         let emailElement = document.getElementById('en_eMailAddress')
    70         let emailValidationElement = document.getElementById('en_eMailAddressValidation')
    71        
    72         submitButton.setAttribute('disabled', 'disabled')
    73        
    74         function en_validationCheck(){
    75             if (emailElement.value === emailValidationElement.value){
    76                 submitButton.removeAttribute('disabled')
    77                 emailValidationElement.style.color = 'black'
    78             } else {
    79                 submitButton.setAttribute('disabled', 'disabled')
    80                 emailValidationElement.style.color = 'red'
    81             }
    82         }");
    83 }
  • easynewsletter/trunk/src/core/resources/admin.css

    r3495745 r3730982  
    77    float: right;
    88}
     9/* Settings subsections: plain links beneath the main plugin tabs. */
     10.en-settings-navigation {
     11    display: flex;
     12    gap: 12px;
     13    margin-bottom: 25px;
     14}
     15.en-settings-navigation a { text-decoration: none; }
     16.en-settings-navigation a[aria-current="page"] { color: #1d2327; font-weight: 600; }
     17.en-settings-navigation a:focus-visible { outline: 2px solid #2271b1; outline-offset: 4px; }
     18#easyNewsletterSettings [data-en-settings-panel][hidden] { display: none; }
  • easynewsletter/trunk/src/core/resources/newslettersPostTypeAdmin.js

    r3696327 r3730982  
    33const { ajaxUrl, nonce, sendingInProgress } = easyNewsletterData;
    44
    5 if (typeof __ === "undefined") {
     5jQuery(document).ready(function ($) {
    66  const { __ } = wp.i18n;
    7 }
    8 
    9 jQuery(document).ready(function ($) {
    10   // add send-buttons and basic popup to html
    11   setTimeout(function () {
    12     // wrapper
    13     $(".editor-header").after("<div class='en_sendButtonsWrapper'></div>");
    14     // sendNewsletter-button or stopSendingNewsletter-button depending on sending state
    15     if (sendingInProgress === true || sendingInProgress === "true") {
    16       // Newsletter is currently being sent
    17       $(".en_sendButtonsWrapper").append('<button class="components-button editor-post-switch-to-draft is-tertiary en_stopSendingNewsletter dashicons-before dashicons-no">' + wp.i18n.__("Stop Sending", "easynewsletter") + "</button>");
    18     } else {
    19       $(".en_sendButtonsWrapper").append("<button class='components-button editor-post-switch-to-draft is-tertiary en_wantToSendNewsletter dashicons-before dashicons-email-alt'>" + __("Send Newsletter Now", "easynewsletter") + "</button>");
    20     }
    21     // sendTestMail-button
    22     $(".en_sendButtonsWrapper").append("<button class='components-button editor-post-switch-to-draft is-tertiary en_TestmailButton dashicons-before dashicons-admin-tools'>" + __("Send Test Mail", "easynewsletter") + "</button>");
    23     // newsletterPopup
    24     $("body").append('<div class="en_newsletterAdminPopup hidden"><div class="en_newsletterAdminPopupBackground"></div><div class="en_newsletterAdminPopupContent"></div></div>');
    25   }, 2000);
     7  let toolbar = null;
     8  let framePending = false;
     9
     10  function mountToolbar() {
     11    const header = document.querySelector('.editor-header, .edit-post-header');
     12    if (!header) return;
     13    if (!toolbar) {
     14      toolbar = document.createElement('div');
     15      toolbar.className = 'en_sendButtonsWrapper';
     16      const sending = sendingInProgress === true || sendingInProgress === 'true';
     17      const send = document.createElement('button');
     18      send.type = 'button';
     19      send.className = 'components-button editor-post-switch-to-draft is-tertiary dashicons-before ' +
     20        (sending ? 'en_stopSendingNewsletter dashicons-no' : 'en_wantToSendNewsletter dashicons-email-alt');
     21      send.textContent = sending ? __('Stop Sending', 'easynewsletter') : __('Send Newsletter Now', 'easynewsletter');
     22      const test = document.createElement('button');
     23      test.type = 'button';
     24      test.className = 'components-button editor-post-switch-to-draft is-tertiary en_TestmailButton dashicons-before dashicons-admin-tools';
     25      test.textContent = __('Send Test Mail', 'easynewsletter');
     26      toolbar.append(send, test);
     27    }
     28    if (header.nextElementSibling !== toolbar) {
     29      header.after(toolbar);
     30      // Also fires after Gutenberg replaces the header. Reuse our node so
     31      // extension buttons and their current state survive a remount.
     32      $(document).trigger('en:toolbar-ready', [toolbar]);
     33    }
     34    if (!document.querySelector('.en_newsletterAdminPopup')) {
     35      $('body').append('<div class="en_newsletterAdminPopup hidden"><div class="en_newsletterAdminPopupBackground"></div><div class="en_newsletterAdminPopupContent"></div></div>');
     36    }
     37  }
     38  const toolbarObserver = new MutationObserver(function () {
     39    if (framePending) return;
     40    framePending = true;
     41    requestAnimationFrame(function () {
     42      framePending = false;
     43      mountToolbar();
     44    });
     45  });
     46  toolbarObserver.observe(document.body, { childList: true, subtree: true });
     47  mountToolbar();
     48  window.addEventListener('pagehide', () => toolbarObserver.disconnect(), { once: true });
    2649
    2750  // click on newsletter popup background - hide popup
  • easynewsletter/trunk/src/core/resources/newslettersPostTypeOverview.js

    r3495745 r3730982  
    33        let id = $(element).parents(":eq(1)").attr("id");
    44        id = id.toString().replace("post-","");
    5         $(element).append("<span class='copy'> | <a href='#' class='en_copy' data-post-id='"+id+"'>Copy</a></span>");
     5        $(element).append("<span class='en_row_action_copy'> | <a href='#' class='en_copy' data-post-id='"+id+"'>Duplizieren</a></span>");
    66    })
    77
  • easynewsletter/trunk/src/core/resources/settingsPage.js

    r3495745 r3730982  
    1 jQuery(document).ready(function ($) {
    2     $("#easyNewsletterSettings").on('keydown', function(event) {
    3         if (event.keyCode === 13){
     1jQuery(function ($) {
     2    const form = document.getElementById('easyNewsletterSettings');
     3    if (!form) return;
     4    const links = Array.from(document.querySelectorAll('[data-en-settings-link]'));
     5    const panels = Array.from(document.querySelectorAll('[data-en-settings-panel]'));
     6    const selection = form.elements.en_settings_section;
     7    const valid = new Set(links.map(link => link.dataset.enSettingsLink));
     8    function showSection(id) {
     9        if (!valid.has(id)) id = 'general';
     10        panels.forEach(panel => { panel.hidden = panel.dataset.enSettingsPanel !== id; });
     11        links.forEach(link => {
     12            if (link.dataset.enSettingsLink === id) link.setAttribute('aria-current', 'page');
     13            else link.removeAttribute('aria-current');
     14        });
     15        selection.value = id;
     16    }
     17    function fromHash() {
     18        const match = location.hash.match(/^#en-settings-(general|signup|design)$/);
     19        return match ? match[1] : selection.value;
     20    }
     21    links.forEach(link => link.addEventListener('click', event => {
     22        event.preventDefault();
     23        const id = link.dataset.enSettingsLink;
     24        showSection(id);
     25        history.replaceState(null, '', link.getAttribute('href'));
     26    }));
     27    window.addEventListener('hashchange', () => showSection(fromHash()));
     28    // Hidden fields stay enabled: switching sections never discards edits and
     29    // saving submits the complete settings form, including unchecked checkboxes.
     30    form.addEventListener('invalid', event => {
     31        const panel = event.target.closest('[data-en-settings-panel]');
     32        if (panel) showSection(panel.dataset.enSettingsPanel);
     33    }, true);
     34    showSection(fromHash());
     35    $(form).on('keydown', function (event) {
     36        if (event.key === 'Enter' && $(event.target).is('input:not([type=submit]):not([type=button])')) {
    437            event.preventDefault();
    5             $("#en_submitSaveButton").click();
     38            document.getElementById('en_submitSaveButton').click();
    639        }
    7     })
    8 })
     40    });
     41});
  • easynewsletter/trunk/src/core/settingsPageContent.php

    r3710879 r3730982  
    1414    }
    1515
     16    $settingsValueMap['newsletterUseThemeStyles'] = isset($_POST['newsletterUseThemeStyles']) && $_POST['newsletterUseThemeStyles'] === 'true' ? 'true' : 'false';
     17    $excludedStyles = array_values(array_filter((array)($_POST['newsletterExcludedStyles'] ?? []), 'is_string'));
     18    $settingsValueMap['newsletterExcludedStyles'] = wp_json_encode(array_map('sanitize_text_field', wp_unslash($excludedStyles)));
     19    $settingsValueMap['newsletterCSSFile'] = esc_url_raw(wp_unslash((string)($_POST['newsletterCSSFile'] ?? '')), ['http', 'https']);
     20    // Root-relative paths are convenient for CSS kept with the theme.
     21    if (isset($_POST['newsletterCSSFile']) && str_starts_with(trim((string)$_POST['newsletterCSSFile']), '/')) {
     22        $settingsValueMap['newsletterCSSFile'] = esc_url_raw(newsletterThemeStyles::absoluteURL(trim(wp_unslash($_POST['newsletterCSSFile'])), home_url('/')));
     23    }
     24    update_option('en_style_revision', wp_generate_uuid4(), false);
     25    delete_option('en_style_file_warnings');
     26    unset($settingsValueMap['en_settings_section']);
    1627    unset($settingsValueMap['page']);
    1728    unset($settingsValueMap['submit']);
    … …  
    3950            }
    4051            continue;
     52        } else if ($setting == "signupCheckboxText"){
     53            $value = wp_kses_post(wp_unslash($value));
    4154        } else if ($setting == "newsletterContentWidth"){
    4255            $value = (string)generateEmailHtml::contentWidth($value);
     56        } else if ($setting == "newsletterSideSpacing"){
     57            $value = (string)generateEmailHtml::sideSpacing($value);
     58        } else if ($setting == "newsletterWideWidth"){
     59            $value = (string)(filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 200, 'max_range' => 2000]]) ?: 1100);
    4360        } else if ($setting == "newsletterCSS"){
    4461            // Prevent escaping quotes
    … …  
    157174    }
    158175    ?>
    159     <h1 class="wp-heading-inline"><?php esc_html_e("Easy Newsletter Settings", "easynewsletter");?></h1>
    160     <hr class="wp-header-end">
     176    <?php
     177    $settingsSections = [
     178        'general' => ['label' => __('Allgemein', 'easynewsletter'), 'title' => __('Allgemeine Einstellungen', 'easynewsletter')],
     179        'signup' => ['label' => __('Anmeldung', 'easynewsletter'), 'title' => __('Anmeldung', 'easynewsletter')],
     180        'design' => ['label' => __('Design', 'easynewsletter'), 'title' => __('Newsletter-Design', 'easynewsletter')],
     181    ];
     182    $settingSection = static function ($setting) {
     183        if (in_array($setting, ['newsletterCSS', 'newsletterContentWidth', 'newsletterWideWidth', 'newsletterSideSpacing', 'newsletterUseThemeStyles', 'newsletterCSSFile', 'newsletterExcludedStyles'], true)) { return 'design'; }
     184        if (in_array($setting, ['signupFormFields', 'signupCheckboxText', 'subscriberMode', 'subscriberCategory', 'subscriberRole', 'addedUserRoleKey', 'standardActivationPost', 'standardUnsubscribePost', 'standardWelcomePost', 'registrationPageID', 'registrationSuccessPageID', 'confirmationSuccessPageID', 'confirmationDeniedPageID', 'unsubscribedPageID', 'registrationFormPageID', 'unsubscribeFormPageID', 'unsubscribedConfirmedPageID'], true)) { return 'signup'; }
     185        return 'general';
     186    };
     187    $selectedSection = sanitize_key($_POST['en_settings_section'] ?? 'general');
     188    if (!isset($settingsSections[$selectedSection])) { $selectedSection = 'general'; }
     189    ?>
     190    <nav class="en-settings-navigation" aria-label="<?php esc_attr_e('Einstellungsbereiche', 'easynewsletter'); ?>">
     191        <?php foreach ($settingsSections as $sectionID => $section): ?>
     192            <a href="#en-settings-<?php echo esc_attr($sectionID); ?>" data-en-settings-link="<?php echo esc_attr($sectionID); ?>"><?php echo esc_html($section['label']); ?></a>
     193        <?php endforeach; ?>
     194    </nav>
    161195    <form method="post" name="easyNewsletterSettings" id="easyNewsletterSettings">
     196        <input type="hidden" name="en_settings_section" value="<?php echo esc_attr($selectedSection); ?>">
     197        <?php
     198        $allSettings = (array)databaseConnector::instance()->getAllSettings();
     199        $designSettings = [];
     200        foreach (['newsletterCSS', 'newsletterContentWidth', 'newsletterWideWidth', 'newsletterSideSpacing'] as $designKey) {
     201            if (array_key_exists($designKey, $allSettings)) {
     202                $designSettings[$designKey] = $allSettings[$designKey];
     203                unset($allSettings[$designKey]);
     204            }
     205        }
     206        $allSettings = array_merge($allSettings, $designSettings);
     207        ?>
     208        <?php foreach ($settingsSections as $sectionID => $section): ?>
     209        <section id="en-settings-<?php echo esc_attr($sectionID); ?>" data-en-settings-panel="<?php echo esc_attr($sectionID); ?>" aria-labelledby="en-settings-heading-<?php echo esc_attr($sectionID); ?>">
     210        <h1 id="en-settings-heading-<?php echo esc_attr($sectionID); ?>"><?php echo esc_html($section['title']); ?></h1>
    162211        <table class="form-table">
    163212            <tbody>
    164213            <?php
    165                 $allSettings = databaseConnector::instance()->getAllSettings();
    166214                foreach ($allSettings as $setting => $value){
     215                    if ($settingSection($setting) !== $sectionID) { continue; }
     216
     217                    if (in_array($setting, ['newsletterUseThemeStyles', 'newsletterCSSFile', 'newsletterExcludedStyles'], true)) { continue; }
     218                    if ($setting === 'newsletterCSS') {
     219                        require_once __DIR__ . '/newsletterStyleSettings.php';
     220                        newsletterStyleSettings::render();
     221                    }
    167222
    168223                    //TODO Maybe put them somewhere to be seen in a hidden settings menu or something.
    … …  
    195250                        'intervalInSeconds' => "<th scope='row'>".__("Interval in seconds ", 'easynewsletter')."</th>",
    196251                        'signupFormFields' => "<th scope='row'>".__("Signup form fields ", 'easynewsletter')."</th>",
     252                        'signupCheckboxText' => "<th scope='row'>".esc_html__("Text der Pflicht-Checkbox im Anmeldeformular", 'easynewsletter')."</th>",
    197253                        'activeNewsletter' => "<th scope='row'>".__("Currently active newsletter ", 'easynewsletter')."</th>",
    198254                        'activeNewsletterID' => "<th scope='row'>".__("Currently active newsletter ID ", 'easynewsletter')."</th>",
    … …  
    204260                        'subscriberCategory' => "<th scope='row'>".__("Available subscriber category ", 'easynewsletter')."</th>",
    205261                        'subscriberRole' => "<th scope='row'>".__("Available subscriber roles ", 'easynewsletter')."</th>",
    206                         'newsletterContentWidth' => "<th scope='row'>".__("Newsletter-Inhaltsbreite (px)", 'easynewsletter')."</th>",
    207                         'newsletterCSS' => "<th scope='row'>".__("Custom Newsletter CSS ", 'easynewsletter')."</th>",
     262                        'newsletterContentWidth' => "<th scope='row'>".__("Standard-Breite (px)", 'easynewsletter')."</th>",
     263                        'newsletterSideSpacing' => "<th scope='row'>".__("Seitlicher Inhaltsabstand (px)", 'easynewsletter')."</th>",
     264                        'newsletterWideWidth' => "<th scope='row'>".__("Weite Breite (px)", 'easynewsletter')."</th>",
     265                        'newsletterCSS' => "<th scope='row'>".__("Benutzerdefiniertes CSS", 'easynewsletter')."</th>",
    208266                        'addedUserRoleKey' => "<th scope='row'>".__("New user role key ", 'easynewsletter')."</th>",
    209267                        'registrationPageID' => "<th scope='row'>".__("Registration page id ", 'easynewsletter')."</th>",
    … …  
    219277                                'senderName' => "<input type='text' name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "' value='" .stripslashes(esc_attr($value)) . "'>",
    220278                        'maxEmailPerInterval', 'intervalInSeconds' => "<input type='number' name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "' value='" . esc_attr($value) . "'>",
    221                         'newsletterContentWidth' => "<input type='number' min='200' max='2000' step='1' name='newsletterContentWidth' id='newsletterContentWidth' value='" . esc_attr($value) . "'><p class='description'>" . esc_html__("Maximale Desktop-Inhaltsbreite und Grundlage für die Bildgrößenberechnung. Auf schmalen Bildschirmen bleibt der Newsletter flexibel. Standard: 800 px.", 'easynewsletter') . "</p>",
     279                        'newsletterContentWidth' => "<input type='number' min='200' max='2000' step='1' name='newsletterContentWidth' id='newsletterContentWidth' value='" . esc_attr($value) . "'>",
     280                        'newsletterSideSpacing' => "<input type='number' min='0' max='200' step='1' name='newsletterSideSpacing' id='newsletterSideSpacing' value='" . esc_attr($value) . "'>",
     281                        'newsletterWideWidth' => "<input type='number' min='200' max='2000' step='1' name='newsletterWideWidth' id='newsletterWideWidth' value='" . esc_attr($value) . "'>",
    222282                        'signupFormFields' => createCheckboxes($value),
     283                        'signupCheckboxText' => "<textarea name='signupCheckboxText' id='signupCheckboxText' rows='4' cols='50' aria-describedby='signupCheckboxTextDescription'>" . esc_textarea($value) . "</textarea><p class='description' id='signupCheckboxTextDescription'>" . esc_html__("Eigener Text, z. B. zur Bestätigung der Datenschutzhinweise. Sicheres HTML wie Links und Hervorhebungen ist erlaubt. Leer lassen für den Standardtext „Ich bin ein Mensch“. Die bestehende Spam-Prüfung bleibt aktiv.", 'easynewsletter') . "</p>",
    223284                        'subscriberMode' => generateModeSelection(),
    224285                        'sendingInProgress', 'activeNewsletter', 'activeNewsletterID' => "<input type='text' name='" . $setting . "' id='" . $setting . "' value='" . $value . "' disabled>",
    225286                                'subscriberCategory' => generateTargetGroupsCategory(),
    226287                                'subscriberRole' => generateTargetGroupsRoles(),
    227                                 'newsletterCSS' => "<textarea name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "'rows='4' cols='50' form='easyNewsletterSettings'>".$value."</textarea>",
     288                                'newsletterCSS' => "<textarea name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "'rows='4' cols='50' form='easyNewsletterSettings'>".esc_textarea($value)."</textarea>",
    228289                        default => "<input type='text' name='" . esc_attr($setting) . "' id='" . esc_attr($setting) . "' value='" . esc_attr($value) . "'>",
    229290                        };
    … …  
    232293            </tbody>
    233294        </table>
     295        </section>
     296        <?php endforeach; ?>
    234297        <?php echo "<input type='hidden' name='wp_en_nonce' value='".wp_create_nonce()."'>"?>
    235298        <input type="submit" id="en_submitSaveButton" class="button button-primary" name="submit" value="<?php _e("Save", 'easynewsletter')?>">
    236299
     300        <div data-en-settings-panel="general">
    237301        <h3><?php _e("Press this Button to stop the Newsletter sending Process", 'easynewsletter')?></h3>
    238302        <input type="submit" class="button button-primary" style="background-color: red; border-color: red" name="stopNewsletterSending" value="<?php _e("Stop Newsletter Sending Process", 'easynewsletter')?>">
     303        </div>
    239304    </form>
    240305    <br>
    241306</div>
    242 
  • easynewsletter/trunk/vendor-prefixed/autoload.php

    r3710879 r3730982  
    2020require_once __DIR__ . '/composer/autoload_real.php';
    2121
    22 return ComposerAutoloaderInit9130e49187f279f2bf1d421ae64cfb1b::getLoader();
     22return ComposerAutoloaderInit2a106f58d8aeb22de715011744b3955b::getLoader();
  • easynewsletter/trunk/vendor-prefixed/composer/autoload_real.php

    r3710879 r3730982  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInit9130e49187f279f2bf1d421ae64cfb1b
     5class ComposerAutoloaderInit2a106f58d8aeb22de715011744b3955b
    66{
    77    private static $loader;
    … …  
    2323        }
    2424
    25         spl_autoload_register(array('ComposerAutoloaderInit9130e49187f279f2bf1d421ae64cfb1b', 'loadClassLoader'), true, true);
     25        spl_autoload_register(array('ComposerAutoloaderInit2a106f58d8aeb22de715011744b3955b', 'loadClassLoader'), true, true);
    2626        self::$loader = $loader = new \Farn\EasyNewsletter\Composer\Autoload\ClassLoader(\dirname(__DIR__));
    27         spl_autoload_unregister(array('ComposerAutoloaderInit9130e49187f279f2bf1d421ae64cfb1b', 'loadClassLoader'));
     27        spl_autoload_unregister(array('ComposerAutoloaderInit2a106f58d8aeb22de715011744b3955b', 'loadClassLoader'));
    2828
    2929        require __DIR__ . '/autoload_static.php';
    30         call_user_func(\Farn\EasyNewsletter\Composer\Autoload\ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b::getInitializer($loader));
     30        call_user_func(\Farn\EasyNewsletter\Composer\Autoload\ComposerStaticInit2a106f58d8aeb22de715011744b3955b::getInitializer($loader));
    3131
    3232        $loader->setClassMapAuthoritative(true);
  • easynewsletter/trunk/vendor-prefixed/composer/autoload_static.php

    r3710879 r3730982  
    55namespace Farn\EasyNewsletter\Composer\Autoload;
    66
    7 class ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b
     7class ComposerStaticInit2a106f58d8aeb22de715011744b3955b
    88{
    99    public static $prefixLengthsPsr4 = array (
    … …  
    3333    {
    3434        return \Closure::bind(function () use ($loader) {
    35             $loader->prefixLengthsPsr4 = ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b::$prefixLengthsPsr4;
    36             $loader->prefixDirsPsr4 = ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b::$prefixDirsPsr4;
    37             $loader->classMap = ComposerStaticInit9130e49187f279f2bf1d421ae64cfb1b::$classMap;
     35            $loader->prefixLengthsPsr4 = ComposerStaticInit2a106f58d8aeb22de715011744b3955b::$prefixLengthsPsr4;
     36            $loader->prefixDirsPsr4 = ComposerStaticInit2a106f58d8aeb22de715011744b3955b::$prefixDirsPsr4;
     37            $loader->classMap = ComposerStaticInit2a106f58d8aeb22de715011744b3955b::$classMap;
    3838
    3939        }, null, ClassLoader::class);
Note: See TracChangeset for help on using the changeset viewer.