From a401fc5f3870fdc9113918c96da074ba083653e7 Mon Sep 17 00:00:00 2001 From: snow flurry Date: Fri, 25 Apr 2025 23:52:18 -0700 Subject: [PATCH] add spoiler_bbc --- plugin/Makefile | 3 +- plugin/f9_theme/src/css/colors.css | 9 + plugin/f9_theme/src/css/dark_colors.css | 7 + .../src/css/jquery.sceditor.default.css | 2 + plugin/spoiler_bbc/Makefile | 3 + plugin/spoiler_bbc/src/Subs-SpoilerCode.php | 66 +++++ plugin/spoiler_bbc/src/asset/icon_expand.svg | 175 ++++++++++++ .../spoiler_bbc/src/asset/icon_minimize.svg | 229 +++++++++++++++ .../src/asset/icon_page_shield.svg | 191 +++++++++++++ .../spoiler_bbc/src/asset/icon_page_warn.svg | 1 + plugin/spoiler_bbc/src/package-info.xml | 41 +++ plugin/spoiler_bbc/src/readme.txt | 25 ++ .../spoiler_bbc/src/sceditor.spoilerbbc.css | 11 + plugin/spoiler_bbc/src/sceditor.spoilerbbc.js | 263 ++++++++++++++++++ plugin/spoiler_bbc/src/spoiler_bbc.css | 66 +++++ 15 files changed, 1091 insertions(+), 1 deletion(-) create mode 100644 plugin/spoiler_bbc/Makefile create mode 100644 plugin/spoiler_bbc/src/Subs-SpoilerCode.php create mode 100644 plugin/spoiler_bbc/src/asset/icon_expand.svg create mode 100644 plugin/spoiler_bbc/src/asset/icon_minimize.svg create mode 100644 plugin/spoiler_bbc/src/asset/icon_page_shield.svg create mode 100644 plugin/spoiler_bbc/src/asset/icon_page_warn.svg create mode 100644 plugin/spoiler_bbc/src/package-info.xml create mode 100644 plugin/spoiler_bbc/src/readme.txt create mode 100644 plugin/spoiler_bbc/src/sceditor.spoilerbbc.css create mode 100644 plugin/spoiler_bbc/src/sceditor.spoilerbbc.js create mode 100644 plugin/spoiler_bbc/src/spoiler_bbc.css diff --git a/plugin/Makefile b/plugin/Makefile index 02c17a8..a5951e3 100644 --- a/plugin/Makefile +++ b/plugin/Makefile @@ -5,7 +5,8 @@ MODS := \ media_bbc \ yt_nocookie \ topic_mute \ - dragdrop_attach + dragdrop_attach \ + spoiler_bbc PKGDIR := $(PWD)/../packages diff --git a/plugin/f9_theme/src/css/colors.css b/plugin/f9_theme/src/css/colors.css index 618d59d..fc891ca 100644 --- a/plugin/f9_theme/src/css/colors.css +++ b/plugin/f9_theme/src/css/colors.css @@ -168,4 +168,13 @@ /* ajax notification bar */ --notify-bar-background: var(--main-background); --notify-bar-color: #f96f00; + + /* spoilers (requires spoilers_bbc) */ + --spoiler-border: #757; + --spoiler-head-background: #dcd; + --spoiler-main-background: #fff9ff; + + --noguest-border: #557; + --noguest-head-background: #dde; + --noguest-main-background: #f9f9ff; } diff --git a/plugin/f9_theme/src/css/dark_colors.css b/plugin/f9_theme/src/css/dark_colors.css index 8ff1a3f..8e309ca 100644 --- a/plugin/f9_theme/src/css/dark_colors.css +++ b/plugin/f9_theme/src/css/dark_colors.css @@ -66,4 +66,11 @@ --input-valid: #242; --input-invalid: #422; + + /* spoilers (requires spoilers_bbc) */ + --spoiler-head-background: #3f2a3f; + --spoiler-main-background: #3a333a; + + --noguest-head-background: #2a2a3f; + --noguest-main-background: #33333f; } diff --git a/plugin/f9_theme/src/css/jquery.sceditor.default.css b/plugin/f9_theme/src/css/jquery.sceditor.default.css index b7d019d..e700c7d 100644 --- a/plugin/f9_theme/src/css/jquery.sceditor.default.css +++ b/plugin/f9_theme/src/css/jquery.sceditor.default.css @@ -1,4 +1,6 @@ /*! SCEditor | (C) 2011-2013, Sam Clarke | sceditor.com/license */ +@import url("../../default/css/spoiler_bbc.css"); + html, p, code::before, div, table { margin: 0; padding: 0; diff --git a/plugin/spoiler_bbc/Makefile b/plugin/spoiler_bbc/Makefile new file mode 100644 index 0000000..95f9fba --- /dev/null +++ b/plugin/spoiler_bbc/Makefile @@ -0,0 +1,3 @@ +PROJECT := spoiler_bbc + +include ../plugin.mk diff --git a/plugin/spoiler_bbc/src/Subs-SpoilerCode.php b/plugin/spoiler_bbc/src/Subs-SpoilerCode.php new file mode 100644 index 0000000..c5265da --- /dev/null +++ b/plugin/spoiler_bbc/src/Subs-SpoilerCode.php @@ -0,0 +1,66 @@ + 'spoiler', + 'before' => '
Spoiler
', + 'after' => '
', + 'block_level' => true, + ); + $codes[] = array( + 'tag' => 'spoiler', + 'type' => 'parsed_equals', + 'before' => '
$1
', + 'after' => '
', + 'block_level' => true, + ); + $codes[] = array( + 'tag' => 'noguest', + // 'parsed_content' works by writing 'before' when it encounters the + // beginning tag, and eventually replacing the end tag with 'after'. We + // need to "parse" the content to perform the is_guest validation, + // though! So our compromise is to not allow BBCode within [noguest]. + // + // *Theoretically*, we could recursively call parse_bbc, but that is a + // can of worms I do not wish to open today. + 'type' => 'unparsed_content', + 'content' => '
Logged-in users only
$1
', + 'validate' => function(&$tag, &$data, $disabled, $params) use ($modSettings, $context, $user_info) + { + if ($user_info['is_guest']) { + $data = 'Sorry, this content is only available for logged-in users.'; + } + }, + 'block_level' => true, + ); +} + +function hook_spoiler_bbcode_buttons(&$bbc_tags) { + $bbc_tags[] = array( + array( + 'code' => 'spoiler', + 'description' => 'Insert Spoiler block', + ), + array( + 'code' => 'noguest', + 'description' => 'Insert block for logged-in users only', + ), + ); +} + +function spoiler_sce_options(&$sce_options) { + loadJavaScriptFile('sceditor.spoilerbbc.js', array('minimize' => true), 'smf_sceditor_spoiler'); + loadCSSFile('sceditor.spoilerbbc.css', array('minimize' => true)); +} + +function spoiler_bbc_credits() { + global $context; + $context['credits_modifications'][] = 'Silk SVG icons by @frhun, licensed under CC-BY-SA'; +} + +function hook_spoiler_load_theme() { + loadCSSFile('spoiler_bbc.css', array('minimize' => true)); +} + +?> diff --git a/plugin/spoiler_bbc/src/asset/icon_expand.svg b/plugin/spoiler_bbc/src/asset/icon_expand.svg new file mode 100644 index 0000000..22fa64d --- /dev/null +++ b/plugin/spoiler_bbc/src/asset/icon_expand.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/spoiler_bbc/src/asset/icon_minimize.svg b/plugin/spoiler_bbc/src/asset/icon_minimize.svg new file mode 100644 index 0000000..f66dd88 --- /dev/null +++ b/plugin/spoiler_bbc/src/asset/icon_minimize.svg @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/spoiler_bbc/src/asset/icon_page_shield.svg b/plugin/spoiler_bbc/src/asset/icon_page_shield.svg new file mode 100644 index 0000000..cc57199 --- /dev/null +++ b/plugin/spoiler_bbc/src/asset/icon_page_shield.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/plugin/spoiler_bbc/src/asset/icon_page_warn.svg b/plugin/spoiler_bbc/src/asset/icon_page_warn.svg new file mode 100644 index 0000000..dd1da12 --- /dev/null +++ b/plugin/spoiler_bbc/src/asset/icon_page_warn.svg @@ -0,0 +1 @@ +image/svg+xmlimage/svg+xml \ No newline at end of file diff --git a/plugin/spoiler_bbc/src/package-info.xml b/plugin/spoiler_bbc/src/package-info.xml new file mode 100644 index 0000000..2f8efc1 --- /dev/null +++ b/plugin/spoiler_bbc/src/package-info.xml @@ -0,0 +1,41 @@ + + + + @flurry:spoiler_bbc + Spoiler BBCode Tags + 1.0 + modification + + + readme.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/spoiler_bbc/src/readme.txt b/plugin/spoiler_bbc/src/readme.txt new file mode 100644 index 0000000..acb561e --- /dev/null +++ b/plugin/spoiler_bbc/src/readme.txt @@ -0,0 +1,25 @@ +[size=x-large][b]Spoiler and Hidden Tags[/b][/size] + +This adds the following BBCode tags to SMF: + +[size=large][b]Spoilers[/b][/size] + +Hides a block of text from users behind a warning of potential spoilers. +Follows the somewhat-standard spoiler tag definition. + +By default, the following uses the subject "[b]Spoiler Text[/b]": + +[nobbc][spoiler]Example Spoiler[/spoiler][/nobbc] + +To set the subject text for the spoiler, use the following: + +[nobbc][spoiler=Super Fantasy RPG XXV Spoilers]Example Spoiler[/spoiler][/nobbc] + +[size=large][b]Login-Restricted Text[/b][/size] + +This is useful for content users want to keep restricted to logged-in users. +For example, a game server's password, or a link to an unlisted YouTube video. + +[nobbc][noguest]The password is "haggis"[/noguest][/nobbc] + +No subject can be set here, it will always appear as "Logged-in users only". diff --git a/plugin/spoiler_bbc/src/sceditor.spoilerbbc.css b/plugin/spoiler_bbc/src/sceditor.spoilerbbc.css new file mode 100644 index 0000000..b1aa235 --- /dev/null +++ b/plugin/spoiler_bbc/src/sceditor.spoilerbbc.css @@ -0,0 +1,11 @@ +.sceditor-button-spoiler div { + background-image: url('../../default/images/icon_page_warn.svg'); + background-size: contain; + background-repeat: no-repeat; +} + +.sceditor-button-noguest div { + background-image: url('../../default/images/icon_page_shield.svg'); + background-size: contain; + background-repeat: no-repeat; +} diff --git a/plugin/spoiler_bbc/src/sceditor.spoilerbbc.js b/plugin/spoiler_bbc/src/sceditor.spoilerbbc.js new file mode 100644 index 0000000..8233ae7 --- /dev/null +++ b/plugin/spoiler_bbc/src/sceditor.spoilerbbc.js @@ -0,0 +1,263 @@ +(function (sceditor) { + const DEFAULT_SPOILER_TEXT = 'Spoiler'; + const DEFAULT_NOGUEST_TEXT = 'Logged-in users only'; + + function isArray(x) { + return (x != null && 'constructor' in x && x.constructor === Array); + } + + /** + * Creates an HTML element. + * + * This can set not only element properties, but also children and events, + * using their respectively-named properties. For example, using a button: + * + * ```js + * createElement('button', { + * innerText: 'Button!', + * events: { + * 'click': function() { alert('Button clicked!'); }, + * }, + * }); + * ``` + * + * And an example div with children: + * + * ```js + * createElement('div', { + * children: [ + * createElement('h1', { + * innerText: 'Header', + * }), + * createElement('p', { + * innerText: 'Paragraph 1', + * }), + * createElement('p', { + * innerText: 'Paragraph 2', + * }), + * ], + * }); + * ``` + * + * @param tag The HTML tag name. For example, 'input' for an `` element. + * @param params The element's parameters (e.g. `id`, `innerText`, etc). + * @return The HTML element. + */ + function createElement(tag, params) { + const elem = document.createElement(tag); + if (params != null && typeof params === 'object') { + for (const key of Object.keys(params)) { + if (key === 'children' && isArray(params['children'])) { + for (const child of params['children']) { + elem.appendChild(child); + } + } else if (key === 'events' && params != null && typeof params['events'] === 'object') { + for (const evkey of Object.keys(params['events'])) { + elem.addEventListener(evkey, params['events'][evkey]); + } + } else { + elem[key] = params[key]; + } + } + } + + return elem; + } + + /** + * BBCode-to-HTML for Spoiler-adjacent tags. + */ + function makeSpoilerHtml(tagname, title, content) { + let isOpen = false; + if (tagname === 'noguest') { + title = DEFAULT_NOGUEST_TEXT; + isOpen = true; + } else if (tagname === 'spoiler') { + if (title == null || title.trim() === '') { + title = DEFAULT_SPOILER_TEXT; + } + } + + return '
' + + '' + title + '' + ' ' + // <-- Load-bearing space?? + '
' + content + '
' + + '
'; + } + + // This creates the dropdown for the user to provide a spoiler subject + function spoilerDropDown(editor, caller, callback) { + const clickAction = function (ev) { + const spoilSubject = document.getElementById('spoilsubject'); + + // The spoiler title must a) exist, and b) be a non-empty string + let title = null; + if (spoilSubject && spoilSubject.value) { + const val = spoilSubject.value.trim(); + if (val !== '') { + title = val; + } + } + + callback(title); + editor.closeDropDown(true); + ev.preventDefault(); + }; + const dropDown = createElement('div', { + children: [ + // Text input + createElement('form', { + children: [ + createElement('label', { + htmlFor: 'spoilsubject', + innerText: 'Spoiler Subject:', + }), + createElement('input', { + id: 'spoilsubject', + name: 'spoilsubject', + type: 'text', + dir: 'ltr', + placeholder: DEFAULT_SPOILER_TEXT, + }), + ], + events: { + 'submit': clickAction, + }, + }), + // Submit button + createElement('div', { + children: [ + createElement('input', { + className: 'button', + type: 'button', + value: 'Insert', + events: { + 'click': clickAction, + }, + }), + ], + }), + ], + }); + + editor.createDropDown(caller, 'insertspoiler', dropDown); + } + + /** + * HTML-to-BBCode for Spoiler-adjacent tags. + */ + function makeSpoilerBBCode(editor, tagname, element) { + let title = null; + let contentNodes = []; + let content = null; + let summaryNode = null; + + for (const child of Array.from(element.children)) { + if (child.tagName === 'SUMMARY') { + summaryNode = child; + } else { + contentNodes.push(child); + } + } + + if (summaryNode) { + element.removeChild(summaryNode); + } + + if (tagname === 'spoiler' && summaryNode != null) { + // Get the custom spoiler text, if any + const summary = summaryNode.textContent.trim(); + if (summary !== '' && summary !== 'Spoiler') { + title = '=' + summary; + } + } + + if (contentNodes.length > 1) { + // Wrap the content in a
to be safe + content = createElement('div', { + children: contentNodes, + }); + } else if (contentNodes.length === 1) { + content = contentNodes[0]; + } + + return '[' + tagname + (title ?? '') + ']' + + editor.elementToBbcode(content) + + '[/' + tagname + ']'; + } + + sceditor.formats.bbcode.set( + 'spoiler', { + tags: { + details: { + class: 'collapsible spoiler' + } + }, + isInline: false, + breakBefore: true, + breakStart: true, + breakEnd: true, + breakAfter: true, + skipLastLineBreak: true, + format: function (element, content) { + return makeSpoilerBBCode(this, 'spoiler', element); + }, + html: function (token, attrs, content) { + return makeSpoilerHtml('spoiler', attrs.defaultattr, content); + } + }); + + sceditor.formats.bbcode.set( + 'noguest', { + tags: { + details: { + class: 'collapsible noguest' + } + }, + isInline: false, + breakBefore: true, + breakStart: true, + breakEnd: true, + breakAfter: true, + skipLastLineBreak: true, + format: function (element, content) { + return makeSpoilerBBCode(this, 'noguest', element); + }, + html: function (token, attrs, content) { + return makeSpoilerHtml('noguest', null, content); + } + }); + + sceditor.command.set( + 'spoiler', { + exec: function (caller) { + const editor = this; + spoilerDropDown(this, caller, function (title) { + // TODO: can we include the selection like txtExec here? + editor.wysiwygEditorInsertHtml(makeSpoilerHtml('spoiler', title, '')); + }); + }, + txtExec: function (caller, selected) { + const editor = this; + spoilerDropDown(this, caller, function (title) { + editor.insertText( + '[spoiler' + (title ? '=' + title : '') + ']' + + selected + + '[/spoiler]' + ); + }); + } + } + ); + + sceditor.command.set( + 'noguest', { + exec: function (caller) { + // TODO: can we include the selection like txtExec here? + this.wysiwygEditorInsertHtml(makeSpoilerHtml('noguest', null, '')); + }, + txtExec: function (caller, selected) { + this.insertText('[noguest]' + selected + '[/noguest]'); + } + } + ); +})(sceditor); diff --git a/plugin/spoiler_bbc/src/spoiler_bbc.css b/plugin/spoiler_bbc/src/spoiler_bbc.css new file mode 100644 index 0000000..15abf60 --- /dev/null +++ b/plugin/spoiler_bbc/src/spoiler_bbc.css @@ -0,0 +1,66 @@ +.collapsible { + border-width: 1px 1px 1px 3px; + border-style: solid; + margin: 0 auto 8px; +} + +.collapsible > summary, +.collapsible > div { + padding: 4px; + margin: 0; +} + +.collapsible > summary { + list-style-type: none; + font-weight: bold; +} + +.collapsible > summary::before { + display: inline-block; + width: 0.8em; + height: 0.8em; + margin-right: 0.25em; + margin-top: auto; + margin-bottom: auto; + background: url('../images/icon_expand.svg') no-repeat 0 0 / contain; + content: ''; +} + +.collapsible > summary::-webkit-details-marker { + display: none; +} + +.collapsible[open] > summary { + border-bottom-width: 1px; + border-bottom-style: solid; +} + +.collapsible[open] > summary::before { + background-image: url('../images/icon_minimize.svg'); +} + +.spoiler > summary { + background-color: var(--spoiler-head-background, #dcd); + border-color: var(--spoiler-border, #969); +} + +.spoiler > div { + background-color: var(--spoiler-main-background, #fff9ff); +} + +.spoiler { + border-color: var(--spoiler-border, #969); +} + +.noguest > summary { + background-color: var(--noguest-head-background, #cce); + border-color: var(--noguest-border, #66a); +} + +.noguest { + border-color: var(--noguest-border, #66a); +} + +.noguest > div { + background-color: var(--noguest-main-background, #f9f9ff); +}