diff --git a/admin/class-create-block-theme-admin.php b/admin/class-create-block-theme-admin.php index 72ba1aa5..66523350 100644 --- a/admin/class-create-block-theme-admin.php +++ b/admin/class-create-block-theme-admin.php @@ -556,6 +556,103 @@ function get_theme_templates( $export_type, $new_slug ) { } + function is_absolute_url( $url ) { + return isset( parse_url( $url )[ 'host' ] ); + } + + function make_image_blocks_local ( $nested_blocks ) { + $new_blocks = []; + foreach ( $nested_blocks as $block ) { + // recursive call for inner blocks + if ( !empty ( $block['innerBlocks'] ) ) { + $block['innerBlocks'] = $this->make_image_blocks_local( $block[ 'innerBlocks' ] ); + } + if ( 'core/image' === $block[ 'blockName' ] ) { + $doc = new DOMDocument(); + @$doc->loadHTML( $block[ 'innerHTML' ], LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); + $tags = $doc->getElementsByTagName( 'img' ); + $block_has_external_images = false; + foreach ( $tags as $tag ) { + $image_url = $tag->getAttribute( 'src' ); + if ( $this->is_absolute_url( $image_url ) ) { + $block_has_external_images = true; + $media[] = $tag->getAttribute( 'src' ); + $tag->setAttribute( + 'src', + '/assets/images/'. basename( $tag->getAttribute( 'src' ) ) + ); + } + } + if ( $block_has_external_images ) { + $block['innerHTML'] = $doc->saveHTML(); + $block['innerContent'] = array ( $doc->saveHTML() ); + } + } + $new_blocks[] = $block; + } + return $new_blocks; + } + + function get_media_absolute_urls_from_blocks ( $flatten_blocks ) { + $media = []; + foreach ( $flatten_blocks as $block ) { + if ('core/image' === $block[ 'blockName' ]) { + $doc = new DOMDocument(); + @$doc->loadHTML( $block['innerHTML'], LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); + $tags = $doc->getElementsByTagName( 'img' ); + $block_has_external_images = false; + foreach ($tags as $tag) { + $image_url = $tag->getAttribute( 'src' ); + if ($this->is_absolute_url( $image_url )) { + $media[] = $tag->getAttribute( 'src' ); + } + } + } + } + return $media; + } + + // find all the media files used in the templates and add them to the zip + function make_template_images_local ( $template ) { + $new_content = $template->content; + $template_blocks = parse_blocks( $template->content ); + $flatten_blocks = _flatten_blocks( $template_blocks ); + + $blocks = $this->make_image_blocks_local( $template_blocks ); + $blocks = serialize_blocks ( $blocks ); + + $template->content = $this->clean_serialized_markup ( $blocks ); + $template->media = $this->get_media_absolute_urls_from_blocks ( $flatten_blocks ); + return $template; + } + + function clean_serialized_markup ( $markup ) { + $markup = str_replace( '%20', ' ', $markup ); + $markup = str_replace( '<', '<', $markup ); + $markup = str_replace( '>', '>', $markup ); + return $markup; + } + + function pattern_from_template ( $template ) { + $theme_slug = wp_get_theme()->get( 'TextDomain' ); + $pattern_slug = $theme_slug . '/' . $template->slug; + $pattern_content = ( +'slug .' + * Slug: ' . $pattern_slug. ' + * Categories: hidden + * Inserter: no + */ +?> +'. $template->content + ); + return array ( + 'slug' => $pattern_slug, + 'content' => $pattern_content + ); + } + /** * Add block templates and parts to the zip. * @@ -567,7 +664,6 @@ function get_theme_templates( $export_type, $new_slug ) { * all = all templates no matter what */ function add_templates_to_zip( $zip, $export_type, $new_slug ) { - $theme_templates = $this->get_theme_templates( $export_type, $new_slug ); if ( $theme_templates->templates ) { @@ -579,16 +675,59 @@ function add_templates_to_zip( $zip, $export_type, $new_slug ) { } foreach ( $theme_templates->templates as $template ) { + $template_data = $this->make_template_images_local( $template ); + + // If there are images in the template, add it as a pattern + if ( count( $template_data->media ) > 0 ) { + $pattern = $this->pattern_from_template( $template_data ); + $template_data->content = ''; + + // Add pattern to zip + $zip->addFromString( + 'patterns/' . $template_data->slug . '.php', + $pattern[ 'content' ] + ); + + // Add image assets to zip + foreach ( $template_data->media as $media ) { + $download_file = file_get_contents( $media ); + $zip->addFromString( 'assets/images/' . basename( $media ), $download_file ); + } + } + + // Add template to zip $zip->addFromString( - 'templates/' . $template->slug . '.html', - $template->content + 'templates/' . $template_data->slug . '.html', + $template_data->content ); + } foreach ( $theme_templates->parts as $template_part ) { + $template_data = $this->make_template_images_local( $template_part ); + + // If there are images in the template, add it as a pattern + if ( count( $template_data->media ) > 0 ) { + $pattern = $this->pattern_from_template( $template_data ); + $template_data->content = ''; + + // Add pattern to zip + $zip->addFromString( + 'patterns/' . $template_data->slug . '.php', + $pattern[ 'content' ] + ); + + // Add image assets to zip + foreach ( $template_data->media as $media ) { + $download_file = file_get_contents( $media ); + $zip->addFromString( 'assets/images/' . basename( $media ), $download_file ); + } + } + + // Add template to zip $zip->addFromString( - 'parts/' . $template_part->slug . '.html', - $template_part->content + 'parts/' . $template_data->slug . '.html', + $template_data->content ); } @@ -601,27 +740,92 @@ function add_templates_to_local( $export_type ) { $template_folders = get_block_theme_folders(); // If there is no templates folder, create it. - if ( ! is_dir( get_stylesheet_directory() . '/' . $template_folders['wp_template'] ) ) { - wp_mkdir_p( get_stylesheet_directory() . '/' . $template_folders['wp_template'] ); + if ( ! is_dir( get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template'] ) ) { + wp_mkdir_p( get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template'] ); + } + + if ( ! is_dir( get_stylesheet_directory() . '/assets/images' ) ) { + wp_mkdir_p( get_stylesheet_directory() . '/assets/images' ); } foreach ( $theme_templates->templates as $template ) { + $template_data = $this->make_template_images_local( $template ); + + // If there are images in the template, add it as a pattern + if ( ! empty ( $template_data->media ) ) { + // If there is no templates folder, create it. + if ( ! is_dir( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' ) ) { + wp_mkdir_p( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' ); + } + + // If there are external images, add it as a pattern + $pattern = $this->pattern_from_template( $template_data ); + $template_data->content = ''; + + // Write the pattern + file_put_contents( + get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' . DIRECTORY_SEPARATOR . $template_data->slug . '.php', + $pattern[ 'content' ] + ); + } + + // Write the template content file_put_contents( - get_stylesheet_directory() . '/' . $template_folders['wp_template'] . '/' . $template->slug . '.html', - $template->content + get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template'] . DIRECTORY_SEPARATOR . $template->slug . '.html', + $template_data->content ); + + // Write the image assets + foreach ( $template_data->media as $media ) { + $download_file = file_get_contents( $media ); + file_put_contents( + get_stylesheet_directory() . DIRECTORY_SEPARATOR .'assets' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . basename( $media ), + $download_file + ); + } + } // If there is no parts folder, create it. - if ( ! is_dir( get_stylesheet_directory() . '/' . $template_folders['wp_template_part'] ) ) { - wp_mkdir_p( get_stylesheet_directory() . '/' . $template_folders['wp_template_part'] ); + if ( ! is_dir( get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template_part'] ) ) { + wp_mkdir_p( get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template_part'] ); } foreach ( $theme_templates->parts as $template_part ) { + $template_data = $this->make_template_images_local( $template_part ); + + // If there are images in the template, add it as a pattern + if ( ! empty ( $template_data->media ) ) { + // If there is no templates folder, create it. + if ( ! is_dir( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' ) ) { + wp_mkdir_p( get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' ); + } + + // If there are external images, add it as a pattern + $pattern = $this->pattern_from_template( $template_data ); + $template_data->content = ''; + + // Write the pattern + file_put_contents( + get_stylesheet_directory() . DIRECTORY_SEPARATOR . 'patterns' . DIRECTORY_SEPARATOR . $template_data->slug . '.php', + $pattern[ 'content' ] + ); + } + + // Write the template content file_put_contents( - get_stylesheet_directory() . '/' . $template_folders['wp_template_part'] . '/' . $template_part->slug . '.html', - $template_part->content + get_stylesheet_directory() . DIRECTORY_SEPARATOR . $template_folders['wp_template_part'] . DIRECTORY_SEPARATOR . $template_data->slug . '.html', + $template_data->content ); + + // Write the image assets + foreach ( $template_data->media as $media ) { + $download_file = file_get_contents( $media ); + file_put_contents( + get_stylesheet_directory() . DIRECTORY_SEPARATOR .'assets' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . basename( $media ), + $download_file + ); + } } } diff --git a/admin/js/google-fonts.js b/admin/js/google-fonts.js new file mode 100644 index 00000000..03f07985 --- /dev/null +++ b/admin/js/google-fonts.js @@ -0,0 +1,155 @@ +const DEMO_TEXT = "The quick brown fox jumps over the lazy dog"; +let fonts = []; +let fontSelected = null; +let variantsSelected = {}; + +function prepareToggleSelectAllVariants () { + const selectAllVariantsElement = document.getElementById('select-all-variants'); + selectAllVariantsElement.addEventListener('click', toggleSelectAllVariants); +} + +function toggleSelectAllVariants () { + const variantCheckboxes = document.querySelectorAll('#font-options input[type="checkbox"]'); + variantCheckboxes.forEach(checkbox => { + checkbox.checked = this.checked; + }); + onFontVariantChange(); + checkIfFormIsAbleToSubmit(); +} + +async function get_google_fonts() { + const currentUrl = new URL(document.getElementById('google-fonts-script-js').src); + const fallbackURL = currentUrl.origin + currentUrl.pathname.replace('admin/js/google-fonts.js', 'assets/google-fonts/fallback-fonts-list.json'); + const response = await fetch(fallbackURL); + const { items } = await response.json(); + return items; +} + +function prepareSelectElement () { + const selectElement = document.getElementById('google-font-id'); + selectElement.addEventListener('change', onGoogleFontNameChange); +} + +async function fillFontSelect() { + fonts = await get_google_fonts(); + const selectElement = document.getElementById('google-font-id'); + for (const i in fonts) { + const font = fonts[i]; + const opt = document.createElement("option"); + opt.value = i; + opt.innerHTML = font['family']; + selectElement.appendChild(opt); + } +} + +function onGoogleFontNameChange() { + const fontNameElement = document.getElementById("font-name"); + const fontsTableElement = document.getElementById("google-fonts-table"); + const hintElements = document.querySelector('.hint'); + + emptyFontOptions(); + + if(this.value) { + fontNameElement.value = fonts[this.value]['family']; + fontSelected = fonts[this.value]; + fontsTableElement.style.display = "block"; + hintElements.style.display = "block"; + displayFontOptions(); + } else { + fontNameElement.value = ""; + fontSelected = null; + fontsTableElement.style.display = "none"; + hintElements.style.display = "none"; + } +} + +function displayFontOptions () { + const fontOptionsElement = document.getElementById('font-options'); + + for ( const variant of fontSelected.variants ) { + // Loads the selected font to create the previews + const style = variant.includes('italic') ? 'italic' : 'normal'; + const weight = variant === 'regular' || variant === 'italic' ? '400' : variant.replace('italic', ''); + // Force https because sometimes Google Fonts API returns http instead of https + const variantUrl = fontSelected['files'][variant].replace("http://", "https://"); + const newFont = new FontFace(fontSelected['family'], `url(${variantUrl})`, { style: style, weight: weight }); + newFont.load().then(function(loaded_face) { + document.fonts.add(loaded_face); + }).catch(function(error) { + console.error(error); + }); + + // Creates the font variant elements and adds them to the page + const tr = document.createElement("tr"); + const td1 = document.createElement("td"); + const td2 = document.createElement("td"); + const td3 = document.createElement("td"); + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.id = variant; + checkbox.name = variant; + checkbox.addEventListener('change', onFontVariantChange); + td1.appendChild(checkbox); + td2.innerHTML = variant; + + const paragraph = document.createElement("p"); + paragraph.style.fontFamily = fontSelected['family']; + paragraph.style.fontStyle = style; + paragraph.style.fontWeight = weight; + paragraph.innerText = `${DEMO_TEXT}`; + td3.appendChild(paragraph); + + tr.appendChild(td1); + tr.appendChild(td2); + tr.appendChild(td3); + + fontOptionsElement.appendChild(tr); + } +} + +function onFontVariantChange () { + const variantCheckboxes = document.querySelectorAll('#font-options input[type="checkbox"]'); + + // updates the variantsSelected object with the selected variants + for (const checkbox of variantCheckboxes) { + if (checkbox.checked) { + variantsSelected[checkbox.id] = fontSelected['files'][checkbox.id]; + } else { + delete variantsSelected[checkbox.id]; + } + } + + // write the input that will be submitted to the server + const googleFontsSelectedElement = document.getElementById('google-font-variants'); + googleFontsSelectedElement.value = Object.keys(variantsSelected).map(key => `${key}::${variantsSelected[key]}`).join(','); + + // enable/disable the form submit button + checkIfFormIsAbleToSubmit(); +} + +function checkIfFormIsAbleToSubmit () { + const variantCheckboxes = document.querySelectorAll('#font-options input[type="checkbox"]'); + const submitElement = document.getElementById('google-fonts-submit'); + submitElement.disabled = ! Array.from(variantCheckboxes).find(checkbox => checkbox.checked); +} + +function emptyFontOptions () { + const fontOptionsElement = document.getElementById('font-options'); + fontOptionsElement.innerHTML = ""; + const googleFontsSelectedElement = document.getElementById('google-font-variants'); + googleFontsSelectedElement.value = ""; + const submitElement = document.getElementById('google-fonts-submit'); + submitElement.disabled = true; + variantsSelected = {}; + const selectAllVariantsElement = document.getElementById('select-all-variants'); + selectAllVariantsElement.checked = false; +} + +function init () { + fillFontSelect(); + prepareSelectElement(); + prepareToggleSelectAllVariants(); +} + +init(); diff --git a/css/google-fonts.css b/css/google-fonts.css new file mode 100644 index 00000000..7d6ef8b3 --- /dev/null +++ b/css/google-fonts.css @@ -0,0 +1,24 @@ +#google-fonts-table { + display: none; +} + +#google-fonts-table.widefat thead td input { + margin: 0; +} + +#google-fonts-table tbody tr td:nth-of-type(3) { + width: 100%; +} + +#google-fonts-table tbody td { + vertical-align: middle; +} + +#google-fonts-table tbody p { + font-size: 2rem; + margin: 0; +} + +.google-fonts-page .hint { + display: none; +} \ No newline at end of file diff --git a/css/manage-fonts.css b/css/manage-fonts.css new file mode 100644 index 00000000..ef56e84d --- /dev/null +++ b/css/manage-fonts.css @@ -0,0 +1,70 @@ +.hidden { + display: none; + visibility: hidden; +} + +.help { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.font-families > table { + margin-bottom: 2rem; +} + +.font-families > table > thead > td { + font-size: 1.1rem; +} + +.font-family-contents .slide { + border-bottom-left-radius: 15px; + margin: 0 0 0 1.5rem; +} + +.font-family-contents .container { + overflow: hidden; +} + +.font-family-contents .slide { + transition: all 0.1s ease-in-out; +} + +.font-family-contents .slide.open { + transform: translateY(0); + max-height: 999999px; +} + +.font-family-contents .slide.close { + transform: translateY(-80%); + opacity: 0; + max-height: 0px; +} + +.font-family-contents table { + border-bottom: none; + border-right: none; + border-top: none; +} + +.font-face .demo-cell p { + font-size: 1.7rem; + line-height: 1.5; + margin: 0; + padding: 0; +} + +.font-family-contents tbody td { + vertical-align: middle; +} + +.font-family-head { + display: flex; + justify-content: space-between; + align-items: center; +} + +.font-family-head div:last-of-type { + display: flex; + gap: 1rem; +} diff --git a/package.json b/package.json index cd4cbaa3..feacae61 100644 --- a/package.json +++ b/package.json @@ -36,4 +36,4 @@ "dependencies": { "@wordpress/icons": "^9.17.0" } -} \ No newline at end of file +} diff --git a/src/font-face.js b/src/font-face.js new file mode 100644 index 00000000..18942f4d --- /dev/null +++ b/src/font-face.js @@ -0,0 +1,41 @@ +import { Button } from '@wordpress/components'; + +const { __ } = wp.i18n; + +function FontFace ( { + fontFamily, + fontWeight, + fontStyle, + demoText, + deleteFontFace, + shouldBeRemoved, +} ) { + + const demoStyles = { + fontFamily, + fontStyle, + // Handle cases like fontWeight is a number instead of a string or when the fontweight is a 'range', a string like "800 900". + fontWeight: fontWeight ? String(fontWeight).split(' ')[0] : "normal", + }; + + if ( shouldBeRemoved ) { + return null; + } + + return ( +
{demoText}
+ {fontFamily.name || fontFamily.fontFamily}
+
+
+
+
+ |
+
+
+
{__('Style', 'create-block-theme')} | +{__('Weight', 'create-block-theme')} | +{__('Preview', 'create-block-theme')} | + { hasFontFaces &&} + + + { hasFontFaces && fontFamily.fontFace.map((fontFace, i) => ( + |
+ {__("This is a list of your font families listed in the theme.json file of your theme.", "create-block-theme")} +
++ {__("If your theme.json makes reference to fonts providers other than local they may not be displayed correctly.", "create-block-theme")} +
++ {__("These are the fonts currently embedded in your theme ", "create-block-theme")} + +
+ +{__('This action will delete the font definition and the font file assets from your theme.', "create-block-theme")}
+