1/* W3Schools Tools showcase â a rotating promo unit for the /tools/ catalogue. 2 * 3 * Usage (declarative â mounts itself on DOM ready): 4 * <div data-ws-tools-showcase="hero"></div> 5 * <script src="/tools/showcase.js"></script> 6 * 7 * Usage (programmatic â for slots built by other scripts): 8 * WsToolsShowcase.mount(element, 'square'); 9 * 10 * Formats: hero 100% x 350 | square 300x300 11 * wide 160x300 | sky 120x300 12 * auto picks square/sky/wide from the viewport, tracking the sidebar slot's 13 * 990 / 1260 / 1700 breakpoints, and re-formats on resize. 14 * 15 * The TOOLS table below is GENERATED from the tool pages themselves â each entry is 16 * [slug, og:title, tool-hero-tagline] for every /tools/tool_*.php that has a matching 17 * screenshots/<slug>.webp. Delegating converter stubs (tool_png_to_jpg.php etc.) are 18 * excluded on purpose: their copy is templated by the engine .inc and they would 19 * crowd the rotation with near-identical entries. To regenerate, re-scan the tool 20 * pages for og:title + .tool-hero-tagline and rebuild the array. 21 */ 22(function () { 23 var SHOTS = '/tools/screenshots/showcase/'; 24 var FALLBACK = SHOTS + 'tool_meme_generator_square.png'; 25 var LINK = '/tools/'; 26 var INTERVAL = 7000; 27 28 var TOOLS = [ 29 ["tool_aes_encrypt","AES Encrypt / Decrypt","Encrypt and decrypt text securely in your browser using AES-256-GCM with a password-derived key."], 30 ["tool_aspect_ratio","Aspect Ratio Calculator","Calculate and convert aspect ratios for images, video, and responsive design."], 31 ["tool_audio_converter","Audio to WAV Converter","Convert MP3, OGG, M4A, FLAC, and other audio files to uncompressed WAV format."], 32 ["tool_audio_cutter","Audio Cutter","Cut a section out of an audio file. Drag across the waveform to select the part to keep."], 33 ["tool_audio_info","Audio Metadata Viewer","View the duration, sample rate, channels, and estimated bitrate of an audio file."], 34 ["tool_audio_joiner","Audio Joiner","Join several audio files into one continuous track. Reorder them, then export as WAV."], 35 ["tool_audio_volume","Audio Volume & Fade","Make an audio file louder or quieter and add smooth fade-in and fade-out effects."], 36 ["tool_background_remover","Background Remover","Cut the background out of a photo automatically using an AI model that runs in your browser. Your image is never..."], 37 ["tool_barcode_generator","Barcode Generator","Create CODE128, EAN, UPC, CODE39 and ITF-14 barcodes and download them as SVG or PNG."], 38 ["tool_base32","Base32 Encode/Decode","Encode text to Base32 or decode Base32 back to text, using the standard RFC 4648 alphabet (A-Z, 2-7)."], 39 ["tool_base58","Base58 Encode/Decode","Encode text to Base58 or decode Base58 back to text, using the Bitcoin alphabet."], 40 ["tool_base64","Base64 Encode/Decode","Encode text to Base64 or decode Base64 to text."], 41 ["tool_bcrypt","Bcrypt Hash Generator","Generate and verify bcrypt password hashes."], 42 ["tool_binary_calculator","Binary and Bitwise Calculator","Enter two integers (decimal, or hexadecimal with a 0x prefix) to see the result of bitwise AND, OR, XOR, NOT, and shift..."], 43 ["tool_border_radius","Border Radius Generator","Create CSS border-radius values visually with independent corner control."], 44 ["tool_bulk_image_compress","Bulk Image Compressor","Compress many images together to a target file size or a fixed quality, then download them all."], 45 ["tool_case_converter","Case Converter","Convert text between different casing styles used in programming."], 46 ["tool_char_counter","Character Counter","Count characters, words, sentences, paragraphs, and lines in real time. Check your text against common social media and..."], 47 ["tool_chmod","Chmod Calculator","Calculate Unix file permissions in numeric and symbolic notation."], 48 ["tool_code_preview","Online Code Editors and Compilers","Write, edit, and run code directly in your browser."], 49 ["tool_color_blindness","Color Blindness Simulator","Upload an image to see how it looks to people with different types of color blindness."], 50 ["tool_color_converter","Color Converter","Convert colors between HEX, RGB, HSL, and HSV formats."], 51 ["tool_color_name","Color Name Finder","Enter a hex or RGB color to find the closest matching named CSS color."], 52 ["tool_color_palette","Color Palette Generator","Generate harmonious color palettes from a base color using color theory."], 53 ["tool_color_picker","Color Picker","This tool helps you pick colors and see how they look in different c
53ontexts."], 54 ["tool_color_shades","Shades and Tints Generator","Pick a base color and generate matching tints, shades, and tones. Click any swatch to copy its hex code."], 55 ["tool_contrast_checker","Color Contrast Checker","Check the contrast ratio between a text color and a background color and see if it passes WCAG accessibility guidelines."], 56 ["tool_crc32","CRC32 Checksum Calculator","Compute the CRC32 checksum of text or a file using the standard IEEE 802.3 polynomial. The result is shown as..."], 57 ["tool_cron","Cron Expression Parser","Parse cron expressions and see a human-readable explanation with next run times."], 58 ["tool_cron_builder","Cron Expression Builder","Build a cron expression with dropdowns and see a plain-English description plus the next run times."], 59 ["tool_css_animation","CSS Animation Generator","Create CSS keyframe animations visually with live preview."], 60 ["tool_css_blob","CSS Blob Generator","Generate organic blob shapes with the border-radius property and copy the code."], 61 ["tool_css_button","CSS Button Generator","Design custom CSS buttons visually and copy the code, including hover styles."], 62 ["tool_css_clip_path","CSS Clip Path Generator","Create CSS clip-path shapes visually with presets and custom polygon editing."], 63 ["tool_css_columns","CSS Multi-Column Generator","Create CSS multi-column layouts visually with live preview."], 64 ["tool_css_cubic_bezier","Cubic Bezier Editor","Drag the control points to design a custom easing curve and copy the code."], 65 ["tool_css_filter","CSS Filter Generator","Apply CSS filter effects to images with live preview."], 66 ["tool_css_flexbox","CSS Flexbox Playground","Experiment with Flexbox properties and see the results in real time."], 67 ["tool_css_formatter","CSS Formatter","Format (beautify) or minify CSS code."], 68 ["tool_css_glassmorphism","Glassmorphism Generator","Create frosted-glass UI effects visually and copy the CSS code."], 69 ["tool_css_gradient","CSS Gradient Generator","Create beautiful CSS gradients visually and copy the code."], 70 ["tool_css_grid","CSS Grid Generator","Create CSS Grid layouts visually and copy the code."], 71 ["tool_css_loader","CSS Loader Generator","Create animated CSS loaders visually and copy the HTML and CSS code."], 72 ["tool_css_minifier","CSS/JS Minifier","Minify CSS or JavaScript code to reduce file size."], 73 ["tool_css_neumorphism","Neumorphism Generator","Create soft-UI (neumorphic) elements visually and copy the CSS code."], 74 ["tool_css_shadow","CSS Box Shadow Generator","Create CSS box shadows visually and copy the code."], 75 ["tool_css_transform","CSS Transform Generator","Create CSS transform effects with live preview - rotate, scale, skew, translate, and 3D."], 76 ["tool_css_units","CSS Unit Converter","Convert between px, rem, em, pt, vw, and vh units."], 77 ["tool_csv_markdown","CSV to Markdown Table","Paste CSV data and convert it into a Markdown table. Supports quoted fields, custom delimiters, and column alignment."], 78 ["tool_csv_viewer","CSV Viewer","View CSV data as a formatted, sortable table."], 79 ["tool_curl_converter","cURL to Code Converter","Paste a curl command and generate equivalent JavaScript fetch() or Python requests code."], 80 ["tool_data_generator","Mock Data Generator","Generate realistic fake data for testing - names, emails, addresses, and more."], 81 ["tool_date_calculator","Date and Age Calculator","Calculate the difference between two dates, add or subtract time from a date, or work out an exact age."], 82 ["tool_demotivational_poster","Demotivational Poster Maker","Turn an image into the classic black-framed motivational-poster parody with a title and caption."], 83 ["tool_device_mockup","Device Mockup Generator","Upload a screenshot and place it inside a device frame. Download as PNG."], 84 ["tool_diff","Text Diff","Compare two texts and see the differences highlighted."], 85 ["tool_dockerfile_generator","Dockerfile Generator","Fill in the fields below to generate a starter Dockerfile for containerizing your application."], 86 ["tool_editorconfig_generator",".editorconfig Generator","Generate an .editorconfig file to keep coding styles consistent across editors and IDEs."],
87 ["tool_emoji_picker","Emoji Picker","Browse, search, and copy emojis. Click any emoji to copy it to clipboard."], 88 ["tool_encoding_detector","Text Encoding Detector","Analyze text encoding, view byte representation, and convert between encodings."], 89 ["tool_env_generator",".env Example Generator","Turn a real .env file into a safe .env.example, or build a new environment file from scratch."], 90 ["tool_escape","Escape / Unescape","Escape or unescape strings for use in JSON, HTML, URLs, SQL, and more."], 91 ["tool_exif_remover","Remove EXIF Data","Remove EXIF metadata - including GPS location - from a photo before sharing it."], 92 ["tool_favicon_generator","Favicon Generator","Turn any image into a complete favicon set - a multi-size favicon.ico plus PNG icons for modern browsers and mobile."], 93 ["tool_find_replace","Find and Replace","Search for text in your content and replace it with something else. Supports case-insensitive matching, whole-word..."], 94 ["tool_font_preview","Font Preview","Preview popular web-safe and Google Fonts with custom text and sizes."], 95 ["tool_gif_add_text","Add Text to GIF","Add a text caption to every frame of an animated GIF, then download the result."], 96 ["tool_gif_maker","GIF Maker","Turn a set of images into an animated GIF. Reorder the frames and set the speed, then download."], 97 ["tool_gif_speed","GIF Speed & Reverse","Speed up or slow down an animated GIF, play it in reverse, or both."], 98 ["tool_gif_to_frames","GIF to Frames","Split an animated GIF into its individual frames and download them as PNG images."], 99 ["tool_github_readme","README Generator","Generate a professional README.md file for your GitHub project."], 100 ["tool_gitignore_generator",".gitignore Generator","Select the technologies, languages, and tools used in your project to generate a combined .gitignore file."], 101 ["tool_hash","Hash Generator","Generate MD5, SHA-1, SHA-256, SHA-384, and SHA-512 hashes from text."], 102 ["tool_hmac_generator","HMAC Generator","Generate keyed-hash message authentication codes (HMAC) using SHA-1, SHA-256, SHA-384, or SHA-512."], 103 ["tool_hreflang_generator","Hreflang Tag Generator","Generate hreflang link tags to tell search engines which language and regional version of a page to serve."], 104 ["tool_htaccess",".htaccess Generator","Generate common .htaccess rules for Apache web servers."], 105 ["tool_html_entities","HTML Entities Encoder/Decoder","Encode special characters to HTML entities or decode HTML entities back to text."], 106 ["tool_html_formatter","HTML Formatter","Format (beautify) or minify HTML code."], 107 ["tool_html_markdown","HTML to Markdown Converter","Convert HTML to Markdown or Markdown to HTML, with live preview of the result."], 108 ["tool_http_headers","HTTP Header Analyzer","Paste raw HTTP response headers to see them parsed, explained, and checked for important security headers."], 109 ["tool_http_status","HTTP Status Codes","Searchable reference for all HTTP response status codes."], 110 ["tool_image_blur","Blur Image","Apply a Gaussian blur to an image. Adjust the radius for a soft or heavy blur."], 111 ["tool_image_border","Add Border to Image","Add a solid color border or frame around an image, with optional rounded corners."], 112 ["tool_image_circle_crop","Circle Crop Image","Crop an image into a circle with a transparent background. Drag to pan and use the slider to zoom."], 113 ["tool_image_color_picker","Image Color Picker","Upload an image and click anywhere on it to grab the exact color as HEX, RGB, and HSL."], 114 ["tool_image_compress","Compress Image","Reduce the file size of an image. Compress to a target size in KB or pick a quality level."], 115 ["tool_image_converter","Image Format Converter","Convert images between PNG, JPEG, WebP, and BMP formats."], 116 ["tool_image_crop","Crop Image","Crop an image to any size. Drag on the picture to select an area, then download the result."], 117 ["tool_image_exif","Image EXIF Metadata Viewer","Inspect the EXIF metadata stored in a photo - camera, lens, exposure, date, and GPS location."], 118 ["tool_image_flip","Flip Image","Mirror an image horizontally or vertically, then download the flipped result."], 119 ["tool_image_grayscale","Grayscale & Black and White Image","Convert a color photo to grayscale, pure black & white, or sepia tone."], 120 ["tool_image_increase_size","Increase Image Size in KB","Pad an image file up to a target size in KB
120- useful for upload forms that require a minimum file size."], 121 ["tool_image_join","Join Images","Combine multiple images into one by stacking them horizontally or vertically."], 122 ["tool_image_palette","Color Palette from Image","Upload a photo and extract its dominant colors as a ready-to-use palette."], 123 ["tool_image_pixelate","Pixelate Image","Apply a mosaic / pixelation effect to an image. Great for censoring details or retro art."], 124 ["tool_image_resize","Resize Image","Resize an image by pixels, percentage, centimeters, millimeters, or inches."], 125 ["tool_image_rotate","Rotate Image","Rotate an image by 90° steps or any custom angle, then download the result."], 126 ["tool_image_splitter","Image Splitter","Cut an image into an even grid of smaller pieces - great for Instagram grids and puzzles."], 127 ["tool_image_text","Add Text to Image","Add a custom text caption to an image. Drag the text on the preview to position it exactly."], 128 ["tool_image_to_pdf","Image to PDF Converter","Combine JPG, PNG, and WebP images into a single PDF document."], 129 ["tool_image_to_text","Image to Text Converter (OCR)","Use OCR (optical character recognition) to pull the text out of a photo, screenshot, or scanned document."], 130 ["tool_image_upscale","Image Upscaler","Upscale an image to 2×, 3×, or 4× its original size with smooth interpolation and optional sharpening."], 131 ["tool_image_watermark","Watermark Image","Add a text or logo watermark to an image. Place it in a corner or tile it across the whole picture."], 132 ["tool_js_formatter","JavaScript Formatter","Format (beautify) or minify JavaScript code."], 133 ["tool_json_compare","JSON Compare","Compare two JSON objects and see the differences highlighted."], 134 ["tool_json_csharp","JSON to C#","Generate C# classes from JSON data."], 135 ["tool_json_csv","JSON to CSV Converter","Convert between JSON arrays and CSV format."], 136 ["tool_json_formatter","JSON Formatter & Validator","Format, validate, and minify JSON data."], 137 ["tool_json_go","JSON to Go","Generate Go struct definitions from JSON data."], 138 ["tool_json_java","JSON to Java","Generate Java POJO classes from JSON data."], 139 ["tool_json_path","JSON Path Evaluator","Evaluate JSONPath expressions against JSON data."], 140 ["tool_json_python","JSON to Python","Generate Python dataclass definitions from JSON data."], 141 ["tool_json_schema","JSON Schema Generator","Generate a JSON Schema from sample JSON data."], 142 ["tool_json_tree","JSON Tree Viewer","Visualize JSON data as an interactive, collapsible tree with path navigation."], 143 ["tool_json_ts","JSON to TypeScript","Generate TypeScript interfaces from JSON data."], 144 ["tool_json_yaml","JSON to YAML Converter","Convert between JSON and YAML formats."], 145 ["tool_jsonld_schema","JSON-LD Schema Generator","Generate Schema.org JSON-LD structured data to help search engines understand your content and enable rich results."], 146 ["tool_jwt_decoder","JWT Decoder","Decode and inspect JSON Web Tokens."], 147 ["tool_jwt_signer","JWT Signer","Build and sign HMAC-based JSON Web Tokens (JWT) directly in your browser."], 148 ["tool_keypair_generator","Key Pair Generator","Generate RSA and Elliptic Curve key pairs in PEM format directly in your browser."], 149 ["tool_license_generator","License Generator","Generate a ready-to-use LICENSE file for your project from a popular open source license."], 150 ["tool_line_sort","Line Sort / Dedupe","Sort lines alphabetically, numerically, or randomly. Remove duplicates and empty lines."], 151 ["tool_lorem_ipsum","Lorem Ipsum Generator","Generate placeholder text for your designs and layouts."], 152 ["tool_markdown","Markdown Preview","Write Markdown and see a live preview side by side."], 153 ["tool_markdown_table","Markdown Table Generator","Build a Markdown table with an interactive grid editor, set column alignment, and copy the generated code."], 154 ["tool_meme_generator","Meme Generator","Add classic top and bottom captions to an image and download your meme."], 155 ["tool_meta_generator","' + esc(title) + '","Generate HTML meta tags for SEO and social media (Open Graph, Twitter Cards)."], 156 ["tool_mic_test","Microphone Test","Check that your microphone works: see a live volume meter and waveform."], 157 ["tool_mime_types","MIME Type Lookup","Searchable reference for common MIME types by file extension."],
158 ["tool_morse_code","Morse Code Translator","Convert plain text into International Morse code and decode Morse back into text. You can also play the Morse code as..."], 159 ["tool_mp4_to_mp3","MP4 to MP3 Converter","Extract the audio track from an MP4 (or MOV, WebM, MKV) video and save it as MP3 or WAV."], 160 ["tool_nato_phonetic","NATO Phonetic Alphabet","Spell out words and codes using the NATO phonetic alphabet, where each letter becomes a clear, unmistakable word like..."], 161 ["tool_notepad","Online Notepad","A simple notepad that automatically saves as you type."], 162 ["tool_number_base","Number Base Converter","Convert numbers between decimal, binary, octal, and hexadecimal."], 163 ["tool_open_graph_preview","' + esc(title) + '","Preview how your page will look when shared on Facebook, Twitter/X, and LinkedIn."], 164 ["tool_package_json","package.json Generator","Fill in the fields below to generate a valid package.json for your Node.js project."], 165 ["tool_password","Password Generator","Generate secure random passwords with customizable options."], 166 ["tool_password_strength","Password Strength Checker","Analyze the strength of a password as you type."], 167 ["tool_percentage_calculator","Percentage Calculator","Four percentage calculators in one: percent of a number, what percent X is of Y, percentage change, and increasing or..."], 168 ["tool_pixel_ruler","Pixel Ruler & Spacing Tool","Visualize pixel measurements, spacing scales, and common sizing references."], 169 ["tool_placeholder_img","Placeholder Image Generator","Generate placeholder images with custom size, color, and text using Canvas."], 170 ["tool_punycode","Punycode Converter","Convert internationalized domain names (IDN) to Punycode (xn-- form) and back, following RFC 3492."], 171 ["tool_qr_decoder","QR Code Scanner / Decoder","Decode a QR code from a picture, or scan one live with your camera."], 172 ["tool_qrcode","QR Code Generator","Generate QR codes for URLs, text, email, phone numbers, and more."], 173 ["tool_query_string","Query String Converter","Convert a URL query string into a JSON object and back. Accepts a full URL or a bare query string, decodes values, and..."], 174 ["tool_quoted_printable","Quoted-Printable Encode/Decode","Encode text to Quoted-Printable or decode it back, following the MIME rules from RFC 2045."], 175 ["tool_random_number","Random Number Generator","Generate cryptographically strong random integers in any range, roll dice, flip coins, or pick random items from a list."], 176 ["tool_regex","RegEx Tester","Test regular expressions against text with real-time matching and group capture."], 177 ["tool_responsive_test","Responsive Breakpoint Tester","Test how a URL looks at different screen widths using common device breakpoints."], 178 ["tool_robots_txt","robots.txt Generator","Generate a robots.txt file for your website to control search engine crawling."], 179 ["tool_roman_numerals","Roman Numeral Converter","Convert numbers (1 to 3999) to Roman numerals and Roman numerals back to numbers."], 180 ["tool_rot13","ROT13 and ROT47 Cipher","Apply the ROT13 or ROT47 cipher to your text. Both ciphers are their own inverse, so the same action both encodes and..."], 181 ["tool_signature_pad","Signature Maker","Draw a signature with your mouse, finger, or pen - or type it in a script font - then download it as an image."], 182 ["tool_sitemap_generator","Sitemap Generator","Paste a list of page URLs and generate a valid XML sitemap for search engines."], 183 ["tool_slug","Slug Generator","Convert text into URL-friendly slugs for web pages, blog posts, and APIs."], 184 ["tool_sql_formatter","SQL Formatter","Format (beautify) or minify SQL queries."], 185 ["tool_sql_to_json","SQL to JSON Converter","Convert SQL CREATE TABLE statements and INSERT queries to JSON Schema or JSON data."], 186 ["tool_string_inspector","String Inspector","Analyze text: character count, word count, line count, character frequency, and more."], 187 ["tool_subnet_calculator","IP Subnet Calculator","Enter an IPv4 address and CIDR prefix to calculate the network address, broadcast address, usable host range, and..."], 188 ["tool_svg_blob","SVG Blob Generator","Generate smooth, randomized blob shapes as SVG paths for backgrounds, avatars, and decorative design elements."], 189 ["tool_svg_optimizer","SVG Optimizer","Paste your SVG code below to remove comments, metadata, editor namespaces, and unnecessary whitespace."], 190 ["tool_svg_pattern","SVG Pattern Generator","Generate seamless, repeatable SVG background patterns and copy them as SVG code or a ready-to-use CSS data URI."], 191 ["tool_svg_to_datauri","SVG to Data URI","Turn SVG markup into a compact data URI you can embed directly in CSS or HTML with
191no extra HTTP request."], 192 ["tool_svg_to_png","SVG to PNG Converter","Paste SVG code or upload an SVG file, choose a size, and download a rasterized PNG image."], 193 ["tool_svg_viewer","SVG Preview & Optimizer","Preview SVG code, view stats, and optimize by removing unnecessary attributes."], 194 ["tool_svg_wave","SVG Wave Generator","Generate smooth wave shapes in SVG to use as section dividers and decorative separators between page sections."], 195 ["tool_table_generator","HTML Table Generator","Generate HTML table code with custom rows, columns, and styling options."], 196 ["tool_text_binary","Text to Binary","Encode text into binary, hexadecimal, or decimal ASCII codes, and decode it back again. Choose a conversion mode and..."], 197 ["tool_text_shadow","CSS Text Shadow Generator","Create CSS text-shadow effects with live preview."], 198 ["tool_text_to_html","Text to HTML Converter","Convert plain text to HTML with automatic paragraph, link, and list detection."], 199 ["tool_text_to_speech","Text to Speech","Type or paste text and have your browser read it aloud. Choose a voice and adjust speed, pitch, and volume. Everything..."], 200 ["tool_timestamp","Unix Timestamp Converter","Convert between Unix timestamps and human-readable dates."], 201 ["tool_timezone_converter","Time Zone Converter","Convert a date and time between any two time zones, and see the same moment around the world."], 202 ["tool_token_generator","Token Generator","Generate cryptographically secure random tokens, API keys, PINs, and UUIDs in your browser."], 203 ["tool_unit_converter","Unit Converter","Convert between units of length, weight, temperature, data storage, area, and speed."], 204 ["tool_url_encoder","URL Encode/Decode","Encode special characters for use in URLs, or decode URL-encoded strings."], 205 ["tool_url_parser","URL Parser","Parse a URL into its components: protocol, host, port, path, query parameters, and fragment."], 206 ["tool_user_agent","User Agent Parser","Parse a user-agent string to identify the browser, operating system, rendering engine, and device type."], 207 ["tool_utm_builder","UTM Link Builder","Add UTM tracking parameters to any URL so you can measure marketing campaigns in your analytics."], 208 ["tool_uuid","UUID Generator","Generate random UUIDs (Universally Unique Identifiers) / GUIDs."], 209 ["tool_voice_recorder","Voice Recorder","Record your voice with the microphone, listen to it, and download it as an audio file."], 210 ["tool_webcam_test","Webcam Test","Check that your webcam works: see a live preview, resolution, and frame rate."], 211 ["tool_whitespace_cleaner","Whitespace Cleaner","Remove extra spaces, blank lines, tabs, and line breaks from text. Pick the operations you need and the cleaned result..."], 212 ["tool_word_counter","Word Counter","Count words, characters, sentences, paragraphs, and estimate reading time."], 213 ["tool_xml_formatter","XML Formatter","Format (beautify) or minify XML code. Also validates XML structure."], 214 ["tool_xml_json","XML to JSON Converter","Convert XML to JSON and JSON back to XML. Attributes are stored as \"@\" keys and text content as \"#text\"."], 215 ["tool_yaml_formatter","YAML Formatter & Validator","Validate YAML as you type, re-indent it with your preferred indentation, sort keys, and view it as JSON."] 216 ]; 217 218 /* ââ styles (injected once) ââââââââââââââââââââââââââââââââââââââââââââââ */ 219 var CSS = [ 220 // text-align is set explicitly: host pages like the homepage bands centre their 221 // content, and the unit must not inherit that 222 // text-align is set explicitly: host pages like the homepage bands centre their 223 // content, and the unit must not inherit that 224 '.wts{position:relative;display:block;overflow:hidden;box-sizing:border-box;text-align:left;', 225 'background:#fff;border:1px solid #ddd;border-radius:5px;text-decoration:none;color:#282A35;', 226 "font-family:'Source Sans Pro',sans-serif;transition:border-color .15s ease,box-shadow .15s ease}", 227 '.wts:hover{border-color:#04AA6D;box-shadow:0 2px 10px rgba(0,0,0,.08)}', 228 '.wts *{box-sizing:border-box}', 229 '.wts-shot{display:block;width:100%;background:#f1f1f1;overflow:hidden;position:relative;z-index:1}', 230 '.wts-shot img{display:block;width:100%;height:100%;object-fit:cover;object-position:top center}',
231 '.wts-body{display:flex;flex-direction:column;gap:4px;flex:0 0 auto;position:relative;z-index:1}', 232 /* the three column formats: screenshot absorbs whatever the copy does not use, 233 so a short tagline never leaves a gap and a long one never overflows */ 234 '.wts-square,.wts-wide,.wts-sky{display:flex;flex-direction:column}', 235 /* 300x300 takes the wide capture, so its shot is 8:5 - the source lands 236 uncropped and the rest of the card is copy */ 237 '.wts-square .wts-shot{flex:0 0 auto;height:auto;aspect-ratio:8/5}', 238 '.wts-square .wts-body{flex:1 1 auto;min-height:0}', 239 /* rail formats: the shot is exactly square, so the square capture lands 1:1 with 240 no side crop, and every pixel left over goes to the copy */ 241 '.wts-wide .wts-shot,.wts-sky .wts-shot{flex:0 0 auto;height:auto;aspect-ratio:1/1}', 242 '.wts-wide .wts-body,.wts-sky .wts-body{flex:1 1 auto;min-height:0}', 243 '.wts-eyebrow{color:#04AA6D;font-weight:700;letter-spacing:.10em;text-transform:uppercase;line-height:1;align-self:flex-start}', 244 '.wts-title{font-weight:700;line-height:1.32;margin:0;padding-bottom:3px;flex:0 0 auto;', 245 'color:#282A35;overflow:hidden;'+ 246 'display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}', 247 '.wts-desc{color:#757575;line-height:1.4;margin:0;padding-bottom:3px;overflow:hidden;', 248 'display:-webkit-box;-webkit-box-orient:vertical}', 249 '.wts-fade{transition:opacity .35s ease}', 250 '.wts-fade.wts-out{opacity:0}', 251 252 /* hero â full width x 350 */ 253 '.wts-hero{width:100%;height:350px;display:flex;flex-direction:row-reverse}', 254 /* inset the capture so it reads as a framed screenshot instead of running flush 255 into the card's right border */ 256 /* width is derived from the height, so the shot stays exactly 1:1 whatever the 257 card is wide and the square capture lands uncropped. Padding is symmetric or 258 the image inside the box would not be square. The copy column takes the rest. */ 259 '.wts-hero .wts-shot{height:100%;aspect-ratio:1/1;width:auto;flex:none;background:#fff;padding:24px}', 260 '.wts-hero .wts-shot img{border:1px solid #e4e4e4;border-radius:6px}', 261 // landscape variant: 8:5 box for the 1200x750 capture, still uncropped 262 // 8:5 box for the 1200x750 capture. Sized from the WIDTH, not the height: 263 // driving it off the height made the art 557px wide and left the copy 178px. 264 '.wts-hero--wide .wts-shot{aspect-ratio:8/5;width:52%;height:auto;max-height:100%;align-self:center}', 265 '.wts-hero .wts-body{flex:1;justify-content:center;padding:32px;gap:10px}', 266 '.wts-hero .wts-eyebrow{font-size:13px}', 267 '.wts-hero .wts-title{font-size:30px}', 268 '.wts-hero .wts-desc{font-size:16px;-webkit-line-clamp:3;max-width:46ch}', 269 270 /* square â 300x300 */ 271 '.wts-square{width:300px;height:300px;max-width:100%}', 272 // tool captures sat flush to the card edges and read as cramped - float them on 273 // a white mat instead. contain (not cover) so the inset never reintroduces a crop. 274 '.wts-square .wts-shot,.wts-wide .wts-shot,.wts-sky .wts-shot{background:#fff;padding:12px 12px 0}', 275 '.wts-square .wts-shot img,.wts-wide .wts-shot img,.wts-sky .wts-shot img{', 276 'object-fit:contain;object-position:top center;border:1px solid #eee;border-radius:4px}', 277 'body.darkpagetheme .wts-square .wts-shot,body.darkpagetheme .wts-wide .wts-shot,', 278 'body.darkpagetheme .wts-sky .wts-shot{background:#1d2a35}', 279 'body.darkpagetheme .wts-square .wts-shot img,body.darkpagetheme .wts-wide .wts-shot img,', 280 'body.darkpagetheme .wts-sky .wts-shot img{border-color:#3a4a5a}', 281 // Cap the art so a two-line heading plus the button always clear the card. 282 // The copy no longer flex-shrinks, so the art is what has to give. 283 '.wts-square .wts-shot{min-height:0;max-height:150px}', 284 '.wts-wide .wts-shot{max-height:130px}', 285 '.wts-sky .wts-shot{max-height:112px}', 286 '.wts-square .wts-body{padding:12px 14px;gap:5px}', 287 '.wts-square .wts-eyebrow{font-size:10px}', 288 '.wts-square .wts-title{font-size:17px}', 289 '.wts-square .wts-desc{font-size:13px;-webkit-line-clamp:2}', 290 291 /* wide skyscraper â 160x300 */ 292 '.wts-wide{width:160px;max-width:100%;height:300px}', 293 '.wts-wide .wts-body{padding:18px 10px 10px;gap:4px}', 294 '.wts-wide .wts-eyebrow{font-size:9px}', 295 '.wts-wide .wts-title{font-size:14px}', 296 '.wts-wide .wts-desc{font-size:11.5px;-webkit-line-clamp:6}', 297
298 /* skyscraper â 120x300 */ 299 '.wts-sky{width:120px;max-width:100%;height:300px}', 300 '.wts-sky .wts-body{padding:16px 8px 8px;gap:3px}', 301 '.wts-sky .wts-eyebrow{font-size:8px}', 302 '.wts-sky .wts-title{font-size:12.5px}', 303 '.wts-sky .wts-desc{font-size:10.5px;-webkit-line-clamp:7}', 304 305 /* ââ promo variant âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 306 Only the practice / projects slides take the share-card look (og-card.php): 307 #282A35 ground, green rule down the left, angled W3 mark. Tool slides keep 308 the light card. */ 309 '.wts--promo{background:#282A35;border-color:#282A35;color:#fff}', 310 '.wts--promo:hover{border-color:#04AA6D;box-shadow:0 4px 16px rgba(0,0,0,.28)}', 311 '.wts--promo::before{content:"";position:absolute;left:0;top:0;bottom:0;background:#04AA6D;z-index:3}', 312 '.wts--promo::after{content:"";position:absolute;right:-5%;bottom:-10%;width:32%;height:44%;', 313 'background-color:#3a5a4a;transform:rotate(-18deg);z-index:0;pointer-events:none;', 314 '-webkit-mask:url(/safari-pinned-tab.svg) center/contain no-repeat;', 315 'mask:url(/safari-pinned-tab.svg) center/contain no-repeat}', 316 '.wts--promo.wts-hero::before{width:10px}', 317 '.wts--promo.wts-square::before{width:7px}', 318 '.wts--promo.wts-wide::before,.wts--promo.wts-sky::before{width:5px}', 319 /* kicker chip above the art, modelled on the practice hero's WEEKLY PROBLEM label */ 320 '.wts-header{position:relative;z-index:2;align-self:flex-start;display:inline-block;', 321 'background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.10);color:#F59E0B;', 322 'font-weight:700;text-transform:uppercase;letter-spacing:.06em;border-radius:999px;white-space:nowrap}', 323 '.wts--promo.wts-square .wts-header,.wts--promo.wts-wide .wts-header,.wts--promo.wts-sky .wts-header{', 324 'margin:12px 12px 0}', 325 '.wts--promo.wts-square .wts-header{font-size:10px;padding:5px 10px}', 326 '.wts--promo.wts-wide .wts-header{font-size:9px;padding:4px 8px}', 327 '.wts--promo.wts-sky .wts-header{font-size:8px;padding:4px 7px;letter-spacing:.04em}', 328 /* the hero is a row, so float the chip over the top-left instead of in flow */ 329 '.wts--promo.wts-hero .wts-header{position:absolute;top:22px;left:40px;font-size:11px;padding:5px 12px}', 330 '.wts--promo.wts-hero .wts-body{padding-top:56px}', 331 '.wts-meta{position:relative;z-index:2;color:#fff;font-weight:700;opacity:.85;white-space:nowrap}', 332 '.wts--promo.wts-square .wts-meta,.wts--promo.wts-wide .wts-meta,.wts--promo.wts-sky .wts-meta{', 333 'display:block;margin:8px 14px 0}', 334 '.wts--promo.wts-square .wts-meta{font-size:12px}', 335 '.wts--promo.wts-wide .wts-meta{font-size:11px}', 336 '.wts--promo.wts-sky .wts-meta{font-size:10px;margin:8px 8px 0}', 337 /* hero is a row, so lay it out as a grid: copy on the left spanning both rows, 338 art top-right with the countdown directly beneath it */ 339 // rows: the art row flexes, the countdown row is content-sized. align-content 340 // cannot centre here because .wts-body spans both rows and sets their height, 341 // so the art centres within its own row instead. 342 '.wts--promo.wts-hero{display:grid;grid-template-columns:1fr 54%;grid-template-rows:1fr auto;', 343 'align-items:center}', 344 '.wts--promo.wts-hero .wts-body{grid-column:1;grid-row:1}', 345 // width:100% so the art fills its grid cell; width:auto sized it to the image's 346 // intrinsic width, which collapses to a quarter of the card before load 347 '.wts--promo.wts-hero .wts-shot{grid-column:2;grid-row:1;width:100%;height:auto;min-height:0;', 348 'aspect-ratio:auto;max-height:none;padding:0 30px 0 0;align-self:center}', 349 // fill the art column rather than sitting at the image's natural size, which 350 // rendered the code panel small against a large empty card 351 // cap the art so a tall capture (the project preview is 1000x816) still clears 352 // the card's 350px height once the grid has centred it 353 '.wts--promo.wts-hero .wts-shot img{width:100%;height:auto;max-height:246px;', 354 'object-fit:contain;object-position:center}', 355 '.wts--promo-practice.wts-hero .wts-shot img{max-height:210px}', 356 // hero only: the countdown reads better beneath the copy than under the art 357 '.wts--promo.wts-hero .wts-meta{grid-column:1;grid-row:2;font-size:18px;', 358 'margin:0 0 30px 40px;text-align:left;align-self:start}', 359 /* the promo card carries a kicker and a countdown on top of the usual copy, so the 360 art takes a fixed modest height and the copy clamps tighter than a tool slide */ 361 '.wts--promo.wts-square .wts-shot{height:142px;min-height:0;aspect-ratio:auto;flex:0 0 auto}', 362 '.wts--promo.wts-wide .wts-shot{height:112px;min-height:0;aspect-ratio:auto;flex:0 0 auto}', 363 '.wts--promo.wts-sky .wts-shot{height:96px;min-height:0;aspect-ratio:auto;flex:0 0 auto}', 364 '.wts--promo.wts-square .wts-desc{-webkit-line-clamp:3}', 365 '.wts--promo.wts-wide .wts-desc{-webkit-line-clamp:4}', 366 '.wts--promo.wts-sky .wts-desc{-webkit-line-clamp:4}', 367 '.wts--promo.wts-hero .wts-title{font-size:28px}', 368 '.wts--promo.wts-hero .wts-desc{-webkit-line-clamp:2}', 369 '.wts--promo.wts-hero .wts-body{padding:48px 18px 10px 40px}', 370 /* the project card has no kicker chip and no countdown, so its art takes the 371 space those would have used */ 372 '.wts--promo-project.wts-square .wts-shot{height:148px}', 373 '.wts--promo-project.wts-wide .wts-shot{height:110px}', 374 '.wts--promo-project.wts-sky .wts-shot{height:100px}', 375 '.wts--promo-project.wts-hero .wts-shot{padding:0 30px 0 0}', 376 '.wts--promo-project.wts-square .wts-desc{-webkit-line-clamp:2}', 377 '.wts--promo-project.wts-wide .wts-desc{-webkit-line-clamp:3}', 378 '.wts--promo-project.wts-sky .wts-desc{-webkit-line-clamp:3}', 379 '.wts--promo .wts-title{color:#fff}', 380 '.wts--promo .wts-desc{color:#aab4c0}', 381 '.wts--promo .wts-shot{background:transparent}', 382 /* the art is a panel on the card's own ground - contain it so it is never cropped 383 and never reads as a box inside a box */ 384 // the tool mat's light border reads as a stray box on the promo card's own ground 385 '.wts--promo .wts-shot img{object-fit:contain;border:0;border-radius:6px}', 386 '.wts--promo.wts-hero .wts-shot img{border:0;border-radius:8px}', 387 '.wts--promo.wts-hero .wts-body{padding:34px 28px 34px 40px;
387gap:11px}', 388 '.wts--promo.wts-hero .wts-title{font-size:34px;letter-spacing:-.01em}', 389 '.wts--promo.wts-hero .wts-desc{max-width:52ch}', 390 '.wts--promo.wts-square .wts-shot,.wts--promo.wts-wide .wts-shot,.wts--promo.wts-sky .wts-shot{padding:14px 14px 0}', 391 'body.darkpagetheme .wts--promo{background:#222430;border-color:#222430}', 392 /* promo cards run less copy than a tool slide, so spend the spare room on padding */ 393 '.wts--promo.wts-square .wts-body{padding:14px 18px 18px;gap:6px}', 394 '.wts--promo.wts-wide .wts-body{padding:12px 14px 16px;gap:5px}', 395 '.wts--promo.wts-sky .wts-body{padding:10px 12px 14px;gap:4px}', 396 '.wts--promo.wts-square .wts-shot,.wts--promo.wts-wide .wts-shot,.wts--promo.wts-sky .wts-shot{padding:16px 16px 0}', 397 '.wts--promo.wts-sky .wts-shot{padding:12px 11px 0}', 398 '.wts--promo.wts-square .wts-header{margin:14px 18px 0}', 399 '.wts--promo.wts-wide .wts-header{margin:12px 14px 0}', 400 '.wts--promo.wts-sky .wts-header{margin:10px 11px 0}', 401 '.wts--promo.wts-square .wts-meta{margin:10px 18px 0}', 402 '.wts--promo.wts-wide .wts-meta{margin:9px 14px 0}', 403 '.wts--promo.wts-sky .wts-meta{margin:8px 11px 0}', 404 /* ââ project promo: the light mint surface from projects/inc_project_hero.inc, 405 not the dark share card, so it reads as the projects section it links to ââ */ 406 '.wts--promo-project{color:#282A35;border-color:#e5e7eb;', 407 'background:radial-gradient(circle at 18% 12%,rgba(4,170,109,.10),transparent 45%),', 408 'radial-gradient(circle at 92% 88%,rgba(4,170,109,.06),transparent 50%),', 409 'linear-gradient(180deg,#fbfdfc 0%,#f2f7f4 100%);background-color:#fbfdfc}', 410 '.wts--promo-project .wts-title{color:#282A35}', 411 '.wts--promo-project .wts-desc{color:#5a6470}', 412 '.wts--promo-project .wts-eyebrow{color:#04AA6D}', 413 '.wts--promo-project .wts-meta{color:#282A35}', 414 // the W3 mark reads as a smudge on a light ground - drop it to a faint mint 415 '.wts--promo-project::after{background-color:#D9EEE1;opacity:.85}', 416 417 /* ââ narrow formats: heading + button only ââââââââââââââââââââââââââââââââ 418 300x300 and the two skyscrapers drop the eyebrow and the description and end on a 419 button, so the card reads as image / heading / action. The hero keeps the fuller 420 layout, so the button and these overrides are scoped away from it. */ 421 '.wts-square .wts-eyebrow,.wts-wide .wts-eyebrow,.wts-sky .wts-eyebrow,', 422 '.wts-square .wts-desc,.wts-wide .wts-desc,.wts-sky .wts-desc{display:none}', 423 '.wts-hero .wts-cta{display:none}', 424 /* W3Schools mark, like the login promo. Only the two skyscrapers show it - they 425 have the vertical room to spare and it puts the empty top strip to work. */ 426 '.wts-logo{display:none}', 427 /* 300x300 tool cards set the mark beside the heading rather than above the art - 428 the square has the horizontal room the skyscrapers do not. Grid, so the pair can 429 centre as a unit while the button still spans the full width beneath. */ 430 '.wts-logo-inline{display:none}', 431 /* optional sub-headline under the heading in the narrow formats (the hero has the 432 real description instead, so this never shows there) */ 433 '.wts-sub{display:block;text-align:center;line-height:1.45;margin:0;flex:0 0 auto;color:#5a6470;', 434 'overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;', 435 'padding-bottom:3px}', 436 '.wts--promo .wts-sub{color:#aab4c0}', 437 '.wts--promo-project .wts-sub{color:#5a6470}', 438 '.wts-square .wts-sub{font-size:13.5px;margin-top:2px}', 439 '.wts-wide .wts-sub{font-size:11.5px;margin-top:2px}', 440 '.wts-sky .wts-sub{font-size:10.5px;margin-top:2px}', 441 '.wts-square:not(.wts--promo) .wts-logo-inline{display:block;line-height:0}', 442 '.wts-square:not(.wts--promo) .wts-logo-inline img{height:30px;width:auto;border:0;', 443 'border-radius:0;object-fit:contain}', 444 '.wts-square:not(.wts--promo) .wts-body{display:grid;grid-template-columns:auto minmax(0,1fr);', 445 'grid-template-rows:auto 1fr;column-gap:10px;align-items:center}', 446 '.wts-square:not(.wts--promo) .wts-logo-inline{grid-column:1;grid-row:1}', 447 '.wts-square:not(.wts--promo) .wts-title{grid-column:2;grid-row:1;text-align:left}', 448 '.wts-square:not(.wts--promo) .wts-cta{grid-column:1/3;grid-row:2;
448align-self:end}', 449 'body.darkpagetheme .wts-square .wts-logo-inline img{filter:brightness(0) invert(1);opacity:.92}', 450 '.wts-wide .wts-logo,.wts-sky .wts-logo{display:block;position:relative;z-index:2;text-align:center}', 451 '.wts-wide .wts-logo img,.wts-sky .wts-logo img{display:inline-block;width:auto;height:auto;', 452 'object-fit:contain;border:0;border-radius:0}', 453 '.wts-wide .wts-logo{margin:12px 0 2px}', 454 '.wts-wide .wts-logo img{height:26px}', 455 '.wts-sky .wts-logo{margin:10px 0 2px}', 456 '.wts-sky .wts-logo img{height:22px}', 457 // the mark is dark-on-light; invert it on the dark promo surface 458 '.wts--promo .wts-logo img{filter:brightness(0) invert(1);opacity:.92}', 459 '.wts--promo-project .wts-logo img{filter:none;opacity:1}', 460 'body.darkpagetheme .wts-wide .wts-logo img,body.darkpagetheme .wts-sky .wts-logo img{', 461 'filter:brightness(0) invert(1);opacity:.92}', 462 // practice moves its kicker down to become the heading, so hide the top chip here 463 '.wts-square .wts-header,.wts-wide .wts-header,.wts-sky .wts-header{display:none}', 464 // bigger headings now that the copy is gone 465 // headings centre with the button; the countdown sits in the same stack so it 466 // centres too, or it would read as misaligned between them 467 '.wts-square .wts-title,.wts-wide .wts-title,.wts-sky .wts-title,', 468 '.wts-square .wts-meta,.wts-wide .wts-meta,.wts-sky .wts-meta{text-align:center}', 469 '.wts-square .wts-title{font-size:21px;-webkit-line-clamp:2}', 470 '.wts-wide .wts-title{font-size:16px;-webkit-line-clamp:3}', 471 '.wts-sky .wts-title{font-size:13.5px;-webkit-line-clamp:3}', 472 // the body becomes heading -> (countdown) -> button, pinned to the bottom 473 '.wts-square .wts-body,.wts-wide .wts-body,.wts-sky .wts-body{justify-content:flex-start}', 474 // button modelled on the sign-in CTA: full width, solid green, bold 475 '.wts-cta{display:block;width:100%;box-sizing:border-box;margin-top:auto;text-align:center;', 476 'background:#04AA6D;color:#fff;font-weight:700;border-radius:5px;line-height:1.2;', 477 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', 478 '.wts:hover .wts-cta{background:#059862}', 479 '.wts-square .wts-cta{font-size:15px;padding:11px 12px}', 480 '.wts-wide .wts-cta{font-size:12.5px;padding:9px 8px}', 481 '.wts-sky .wts-cta{font-size:11px;padding:8px 6px}', 482 483 /* dark mode â matches the tools template surfaces */ 484 'body.darkpagetheme .wts{background:#1d2a35;border-color:#3a4a5a;color:#ddd}', 485 'body.darkpagetheme .wts-title{color:#ddd}', 486 'body.darkpagetheme .wts-desc{color:#aab}', 487 'body.darkpagetheme .wts-shot{background:#121f2b}', 488 /* the hero shot's padding is card-coloured, not shot-coloured, so the capture 489 floats on the card rather than sitting in a grey well */ 490 'body.darkpagetheme .wts-hero .wts-shot{background:#1d2a35}', 491 'body.darkpagetheme .wts-hero .wts-shot img{border-color:#3a4a5a}', 492 493 /* hero stacks on narrow screens */ 494 '@media (max-width:600px){', 495 '.wts-hero{flex-direction:column;height:auto}', 496 '.wts-hero .wts-shot{width:100%;height:170px;aspect-ratio:auto;padding:14px 14px 0}', 497 '.wts-hero .wts-body{padding:18px}', 498 '.wts-hero .wts-title{font-size:22px}', 499 '.wts-hero .wts-desc{font-size:14px;-webkit-line-clamp:2}', 500 '}', 501 '@media (prefers-reduced-motion:reduce){.wts-fade{transition:none}}' 502 ].join(''); 503 504 function injectCss() { 505 if (document.getElementById('wts-css')) return; 506 var s = document.createElement('style'); 507 s.id = 'wts-css'; 508 s.appendChild(document.createTextNode(CSS)); 509 (document.head || document.documentElement).appendChild(s); 510 } 511 512 /* ââ rendering âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 513 function esc(s) { 514 return String(s).replace(/&/g, '&').replace(/</g, '<') 515 .replace(/>/g, '>').replace(/"/g, '"'); 516 } 517 518 var FORMATS = { hero: 1, square: 1, wide: 1, sky: 1 }; 519 520 /* format:"auto" tracks the sidebar slot's existing <source media> breakpoints in 521 footer.inc / inc_footer.inc, so the unit keeps the rail's footprint at every width. */ 522 var BREAKS = [[990, 'square'], [1260, 'sky'], [1700, 'wide']];
523 function autoFormat() { 524 var w = window.innerWidth || document.documentElement.clientWidth; 525 for (var i = 0; i < BREAKS.length; i++) if (w <= BREAKS[i][0]) return BREAKS[i][1]; 526 return 'square'; 527 } 528 529 /* Which capture each format wants, by the shape of its shot box: 530 hero ~330x348 (1:1) square square ~298x150 (2:1) wide 531 wide ~158x160 (1:1) square sky ~118x160 (3:4) square 532 The square captures are shot at a mobile viewport, so they are tall and full of 533 UI; the wide ones are the desktop layout. Picking the nearer aspect keeps 534 object-fit:cover from throwing away half the screenshot. */ 535 var SHOT_FOR = { hero: 'square', square: 'wide', wide: 'square', sky: 'square' }; 536 537 /* The hero is the one format whose width varies a lot - 735px on the showcase page, 538 980px in a homepage band, less in a narrow column. Above this width there is room 539 for the landscape capture; below it the square one keeps the copy readable. */ 540 var HERO_WIDE_MIN = 700; 541 function shotFor(fmt, el) { 542 if (fmt !== 'hero') return SHOT_FOR[fmt] || 'wide'; 543 var w = el ? el.getBoundingClientRect().width : 0; 544 if (!w && el && el.parentElement) w = el.parentElement.getBoundingClientRect().width; 545 return w >= HERO_WIDE_MIN ? 'wide' : 'square'; 546 } 547 548 /* Tool entries are compact tuples; promo slides are objects. Normalise both to one 549 shape so the render path does not care which it is holding. */ 550 function toSlide(t) { 551 if (t && t.href) return t; 552 return { 553 slug: t[0], 554 title: t[1], 555 desc: t[2], 556 href: LINK + t[0] + '.php', 557 eyebrow: 'W3Schools Tools', 558 ga: 'ga-tool-' + t[0].replace(/^tool_/, '').replace(/_/g, '-') 559 }; 560 } 561 562 function markup(slide, fmt, variant) { 563 var shot = SHOTS + slide.slug + '_' + (variant || SHOT_FOR[fmt] || 'wide'); 564 // The narrow formats drop the eyebrow and the description and carry a button 565 // instead; the hero keeps the fuller layout. 566 var rail = (fmt !== 'hero'); 567 var title = (rail && slide.railTitle) ? slide.railTitle : slide.title; 568 var cta = slide.cta || 'Open Tool'; 569 // Only WebP is shipped per tool. The <img> needs some src for the handful of 570 // browsers without WebP, so every unit points at one shared PNG rather than 571 // doubling the asset count for a fallback almost nobody takes. 572 var webp = (rail && slide.railArt) ? slide.railArt : shot + '.webp'; 573 var fallback = (rail && slide.railArtFallback) ? slide.railArtFallback : FALLBACK; 574 return '<span class="wts-logo" aria-hidden="true">' + 575 '<img src="/images/w3schools_logo_200.png" alt="" loading="lazy" decoding="async">' + 576 '</span>' + 577 (slide.header ? '<span class="wts-header">' + esc(slide.header) + '</span>' : '') + 578 '<span class="wts-shot">' + 579 '<picture>' + 580 '<source srcset="' + webp + '" type="image/webp">' + 581 '<img src="' + fallback + '" alt="" loading="lazy" decoding="async">' + 582 '</picture>' + 583 '</span>' + 584 (slide.meta ? '<span class="wts-meta">' + esc(slide.meta) + '</span>' : '') + 585 '<span class="wts-body">' + 586 '<span class="wts-eyebrow">' + esc(slide.eyebrow || 'W3Schools Tools') + '</span>' + 587 '<span class="wts-logo-inline" aria-hidden="true">' + 588 '<img src="/images/w3schools_logo_200.png" alt="" loading="lazy" decoding="async">' + 589 '</span>' + 590 '<span class="wts-title">' + esc(title) + '</span>' + 591 (rail && slide.railSub ? '<span class="wts-sub">' + esc(slide.railSub) + '</span>' : '') + 592 '<span class="wts-desc">' + esc(slide.desc) + '</span>' + 593 '<span class="wts-cta">' + esc(cta) + '</span>' + 594 '</span>'; 595 } 596 597 function apply(el, tool, fmt) { 598 var slide = toSlide(tool); 599 el.href = slide.href; 600 el.target = '_blank'; 601 el.rel = 'noopener'; 602 el.setAttribute('aria-label', slide.title + ' - W3Schools (opens in a new tab)'); 603 var variant = shotFor(fmt, el); 604 el.innerHTML = markup(slide, fmt, variant); 605 // A landscape capture needs a landscape shot box, or it would letterbox. Promos are 606 // exempt: they ship one art file for both variants and lay the hero out as a grid, 607 // so the wide rule's width:52% would fight the grid column. 608 el.classList.toggle('wts-hero--wide', 609 fmt === 'hero' && variant === 'wide' && !slide.promo); 610 611 /* Analytics. The hero is a featured placement; the fixed-size units live in the
612 right rail. The second class names what was clicked and is refreshed on every 613 rotation and on an auto re-format, so a click is attributed to what was shown. */ 614 el.classList.remove('ga-featured', 'ga-right'); 615 el.classList.add(fmt === 'hero' ? 'ga-featured' : 'ga-right'); 616 for (var c = el.classList.length - 1; c >= 0; c--) { 617 var cl = el.classList[c]; 618 if (cl.indexOf('ga-tool-') === 0 || cl.indexOf('ga-practice-') === 0 || 619 cl.indexOf('ga-project-') === 0) el.classList.remove(cl); 620 } 621 el.classList.add(slide.ga); 622 // promo slides carry the share-card surface; tool slides keep the light card 623 el.classList.remove('wts--promo', 'wts--promo-practice', 'wts--promo-project'); 624 if (slide.promo) { 625 el.classList.add('wts--promo'); 626 if (slide.kind) el.classList.add('wts--promo-' + slide.kind); 627 } 628 } 629 630 /* ââ promo slides âââââââââââââââââââââââââââââââââââââââââ */ 631 /* Two non-tool slides. Which one a page gets is decided by its subject folder: 632 the front-end tutorials point at the beginner HTML project, everything else at 633 the weekly practice problem. */ 634 var PROMOS = { 635 practice: { 636 shot: 'promo_practice', 637 header: 'Free Weekly Problem', 638 // the code panel reads well large in the hero but turns to noise at 120px wide, 639 // so the skyscrapers and the square get a device mockup instead 640 railArt: '/images/img_mockup.webp', 641 railArtFallback: '/images/img_mockup.png', 642 railTitle: 'Free Weekly Problem', 643 cta: 'Start Practicing', 644 eyebrow: 'W3Schools Practice', 645 title: 'Weekly Practice Problem', 646 desc: 'A new coding challenge every week. Solve it in your language of choice and earn XP.', 647 href: '/practice/index.php', 648 ga: 'ga-practice-weekly' 649 }, 650 project: { 651 shot: 'promo_project', 652 railTitle: 'Build a Project', 653 railSub: 'Step-by-step projects in browser', 654 // the HTMLBIO001 browser preview is text-heavy and unreadable at rail widths; 655 // the game template reads as a project at a glance 656 railArt: '/projects/templates/GAME001/preview.webp', 657 railArtFallback: '/projects/templates/GAME001/preview.png', 658 cta: 'Start Project', 659 eyebrow: 'W3Schools Projects', 660 title: 'Build a Personal Page', 661 desc: 'Build your first HTML project, step-by-step. No setup, everything runs in your browser.', 662 href: '/projects/index.php', 663 ga: 'ga-project-htmlbio001' 664 } 665 }; 666 667 var FRONTEND = { html: 1, css: 1, js: 1 }; 668 function promoKindForPage() { 669 var seg = (location.pathname.split('/')[1] || '').toLowerCase(); 670 return FRONTEND[seg] ? 'project' : 'practice'; 671 } 672 673 /* The weekly problem rotates on a date schedule, so its name cannot live in this 674 file. Fetch it once per page and fill the card in when it lands; until then the 675 generic copy above stands, so the slide is never blank or wrong. */ 676 var weeklyReq = null; 677 function withWeekly(cb) { 678 if (!weeklyReq) { 679 weeklyReq = fetch('/practice/weekly_json.php') 680 .then(function (r) { return r.json(); }) 681 .catch(function () { return null; }); 682 } 683 weeklyReq.then(cb); 684 } 685 686 function promoSlide(kind) { 687 var p = PROMOS[kind] || PROMOS.practice; 688 return { slug: p.shot, title: p.title, desc: p.desc, href: p.href, 689 eyebrow: p.eyebrow, header: p.header, railTitle: p.railTitle, cta: p.cta, 690 railArt: p.railArt, railArtFallback: p.railArtFallback, railSub: p.railSub, 691 ga: p.ga, promo: true, kind: kind }; 692 } 693 694 /* ââ mount âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 695 function mount(host, fmt, opts) { 696 if (!host || !TOOLS.length) return null; 697 var auto = (fmt === 'auto'); 698 fmt = auto ? autoFormat() : (FORMATS[fmt] ? fmt : 'square'); 699 opts = opts || {}; 700 injectCss(); 701 702 var a = document.createElement('a'); 703 // ga-featured / ga-right and the per-tool class are applied in apply(), which runs 704 // on every rotation - setting them here would go stale on the first swap 705 a.className = 'wts wts-' + fmt + ' wts-fade'; 706 if (opts.gaClass) a.classList.add(opts.gaClass); 707 708 // Walk a shuffled order, not the table order - TOOLS is sorted by slug, so 709 // stepping i+1 marched through the catalogue alphabetically. Shuffling per mount 710 // also stops two units on the same page showing the same run of tools. 711 // Every tool appears once before any repeats. 712 var order = [], i; 713 for (i = 0; i < TOOLS.length; i++) order.push(i); 714 for (i = order.length - 1; i > 0; i--) { 715 var r = Math.floor(Math.random() * (i + 1)), t = order[i]; 716 order[i] = order[r]; order[r] = t; 717 } 718 var p = 0; 719 // opts.slide pins the unit to one slide (promo mounts) instead of the catalogue 720 function current() { return opts.slide || TOOLS[order[p]]; } 721 apply(a, current(), fmt); 722 723 if (host.hasAttribute && host.hasAttribute('data-ws-tools-showcase')) { 724 host.innerHTML = ''; 725 host.appendChild(a); 726 } else { 727 host.appendChild(a); 728 } 729 // width is only knowable once the unit is in the document, and the hero picks its 730 // capture from that - so resolve it again now it can be measured 731 if (fmt === 'hero') apply(a, current(), fmt); 732 733 var paused = false, timer = null; 734 var still = window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches; 735 736 function next() { 737 if (paused) return; 738 p = (p + 1) % order.length; 739 if (still) { apply(a, current(), fmt); return; } 740 a.classList.add('wts-out'); 741 setTimeout(function () { 742 apply(a, current(), fmt); 743 a.classList.remove('wts-out'); 744 }, 350); 745 } 746 747 if (TOOLS.length > 1 && opts.rotate !== false) { 748 timer = setInterval(next, opts.interval || INTERVAL); 749 // don't swap the link out from under someone reading or tabbing to it 750 a.addEventListener('mouseenter', function () { paused = true; }); 751 a.addEventListener('mouseleave', function () { paused = false; }); 752 a.addEventListener('focus', function () { paused = true; }); 753 a.addEventListener('blur', function () { paused = false; }); 754 document.addEventListener('visibilitychange', function () { 755 paused = document.hidden; 756 }); 757 } 758 759 if (auto) { 760 window.addEventListener('resize', function () { 761 var f = autoFormat(); 762 if (f === fmt) return;
763 a.classList.remove('wts-' + fmt); 764 fmt = f; 765 a.classList.add('wts-' + fmt); 766 apply(a, current(), fmt); // re-stamps ga-featured/ga-right for the new format 767 }); 768 } 769 770 return { el: a, fmt: function () { return fmt; }, 771 stop: function () { if (timer) clearInterval(timer); } }; 772 } 773 774 function init() { 775 var hosts = document.querySelectorAll('[data-ws-tools-showcase]'); 776 for (var i = 0; i < hosts.length; i++) { 777 if (hosts[i].querySelector('.wts')) continue; 778 var want = hosts[i].getAttribute('data-ws-tools-showcase') || ''; 779 var m = want.match(/^(hero|square|sky|wide|auto)-(practice|project|auto)$/); 780 if (m) mountPromo(hosts[i], m[2], m[1]); 781 else mount(hosts[i], want); 782 } 783 } 784 785 /* Mount a single promo slide (no rotation). kind is 'practice', 'project', or 786 'auto' to pick by subject folder. The practice card fills in the live problem 787 name once /practice/weekly_json.php answers. */ 788 function mountPromo(host, kind, fmt, opts) { 789 if (kind === 'auto' || !kind) kind = promoKindForPage(); 790 var slide = promoSlide(kind); 791 var unit = mount(host, fmt, mergeOpts(opts, { rotate: false, slide: slide })); 792 if (unit && kind === 'practice') { 793 withWeekly(function (w) { 794 if (!w || !w.ok || !w.name) return; 795 slide.title = w.name; 796 slide.desc = w.intro || slide.desc; 797 if (w.daysLeft != null) { 798 slide.meta = w.daysLeft + ' day' + (w.daysLeft === 1 ? '' : 's') + ' left'; 799 } 800 apply(unit.el, slide, unit.fmt()); 801 }); 802 } 803 return unit; 804 } 805 806 function mergeOpts(o, extra) { 807 var out = {}, k; 808 for (k in (o || {})) if (Object.prototype.hasOwnProperty.call(o, k)) out[k] = o[k]; 809 for (k in extra) out[k] = extra[k]; 810 return out; 811 } 812 813 window.WsToolsShowcase = { 814 mount: mount, mountPromo: mountPromo, init: init, 815 tools: TOOLS, promoKindForPage: promoKindForPage 816 }; 817 818 if (document.readyState !== 'loading') init(); 819 else document.addEventListener('DOMContentLoaded', init); 820})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.