commit bd649bd5f2fe7755cc7fa5c2e51db46313219ac6 Author: Kangmin Date: Mon Jan 5 06:47:36 2026 +0700 Initial commit - CMS Gov Bapenda Garut dengan EditorJS diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f37663 --- /dev/null +++ b/.gitignore @@ -0,0 +1,131 @@ +#------------------------- +# Operating Specific Junk Files +#------------------------- + +# OS X +.DS_Store +.AppleDouble +.LSOverride + +# OS X Thumbnails +._* + +# Windows image file caches +Thumbs.db +ehthumbs.db +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Linux +*~ + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +#------------------------- +# Environment Files +#------------------------- +# These should never be under version control, +# as it poses a security risk. +.env +.vagrant +Vagrantfile + +#------------------------- +# Temporary Files +#------------------------- +writable/cache/* +!writable/cache/index.html + +writable/logs/* +!writable/logs/index.html + +writable/session/* +!writable/session/index.html + +writable/uploads/* +!writable/uploads/index.html + +writable/debugbar/* +!writable/debugbar/index.html + +php_errors.log + +#------------------------- +# User Guide Temp Files +#------------------------- +user_guide_src/build/* +user_guide_src/cilexer/build/* +user_guide_src/cilexer/dist/* +user_guide_src/cilexer/pycilexer.egg-info/* + +#------------------------- +# Test Files +#------------------------- +tests/coverage* + +# Don't save phpunit under version control. +phpunit + +#------------------------- +# Composer +#------------------------- +vendor/ + +#------------------------- +# Node.js +#------------------------- +node_modules/ + +#------------------------- +# IDE / Development Files +#------------------------- + +# Modules Testing +_modules/* + +# phpenv local config +.php-version + +# Jetbrains editors (PHPStorm, etc) +.idea/ +*.iml + +# NetBeans +/nbproject/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/nbactions.xml +/nb-configuration.xml +/.nb-gradle/ + +# Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project +.phpintel +/api/ + +# Visual Studio Code +.vscode/ + +/results/ +/phpunit*.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..24728f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2019 British Columbia Institute of Technology +Copyright (c) 2019-present CodeIgniter Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d14b4c9 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# CodeIgniter 4 Application Starter + +## What is CodeIgniter? + +CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure. +More information can be found at the [official site](https://codeigniter.com). + +This repository holds a composer-installable app starter. +It has been built from the +[development repository](https://github.com/codeigniter4/CodeIgniter4). + +More information about the plans for version 4 can be found in [CodeIgniter 4](https://forum.codeigniter.com/forumdisplay.php?fid=28) on the forums. + +You can read the [user guide](https://codeigniter.com/user_guide/) +corresponding to the latest version of the framework. + +## Installation & updates + +`composer create-project codeigniter4/appstarter` then `composer update` whenever +there is a new release of the framework. + +When updating, check the release notes to see if there are any changes you might need to apply +to your `app` folder. The affected files can be copied or merged from +`vendor/codeigniter4/framework/app`. + +## Setup + +Copy `env` to `.env` and tailor for your app, specifically the baseURL +and any database settings. + +## Important Change with index.php + +`index.php` is no longer in the root of the project! It has been moved inside the *public* folder, +for better security and separation of components. + +This means that you should configure your web server to "point" to your project's *public* folder, and +not to the project root. A better practice would be to configure a virtual host to point there. A poor practice would be to point your web server to the project root and expect to enter *public/...*, as the rest of your logic and the +framework are exposed. + +**Please** read the user guide for a better explanation of how CI4 works! + +## Repository Management + +We use GitHub issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages. +We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss +FEATURE REQUESTS. + +This repository is a "distribution" one, built by our release preparation script. +Problems with it can be raised on our forum, or as issues in the main repository. + +## Server Requirements + +PHP version 8.1 or higher is required, with the following extensions installed: + +- [intl](http://php.net/manual/en/intl.requirements.php) +- [mbstring](http://php.net/manual/en/mbstring.installation.php) + +> [!WARNING] +> - The end of life date for PHP 7.4 was November 28, 2022. +> - The end of life date for PHP 8.0 was November 26, 2023. +> - If you are still using PHP 7.4 or 8.0, you should upgrade immediately. +> - The end of life date for PHP 8.1 will be December 31, 2025. + +Additionally, make sure that the following extensions are enabled in your PHP: + +- json (enabled by default - don't turn it off) +- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) if you plan to use MySQL +- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library diff --git a/app/.htaccess b/app/.htaccess new file mode 100644 index 0000000..3462048 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/app/Common.php b/app/Common.php new file mode 100644 index 0000000..95f5544 --- /dev/null +++ b/app/Common.php @@ -0,0 +1,15 @@ + + */ + public array $allowedHostnames = []; + + /** + * -------------------------------------------------------------------------- + * Index File + * -------------------------------------------------------------------------- + * + * Typically, this will be your `index.php` file, unless you've renamed it to + * something else. If you have configured your web server to remove this file + * from your site URIs, set this variable to an empty string. + */ + public string $indexPage = ''; + + /** + * -------------------------------------------------------------------------- + * URI PROTOCOL + * -------------------------------------------------------------------------- + * + * This item determines which server global should be used to retrieve the + * URI string. The default setting of 'REQUEST_URI' works for most servers. + * If your links do not seem to work, try one of the other delicious flavors: + * + * 'REQUEST_URI': Uses $_SERVER['REQUEST_URI'] + * 'QUERY_STRING': Uses $_SERVER['QUERY_STRING'] + * 'PATH_INFO': Uses $_SERVER['PATH_INFO'] + * + * WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded! + */ + public string $uriProtocol = 'REQUEST_URI'; + + /* + |-------------------------------------------------------------------------- + | Allowed URL Characters + |-------------------------------------------------------------------------- + | + | This lets you specify which characters are permitted within your URLs. + | When someone tries to submit a URL with disallowed characters they will + | get a warning message. + | + | As a security measure you are STRONGLY encouraged to restrict URLs to + | as few characters as possible. + | + | By default, only these are allowed: `a-z 0-9~%.:_-` + | + | Set an empty string to allow all characters -- but only if you are insane. + | + | The configured value is actually a regular expression character group + | and it will be used as: '/\A[]+\z/iu' + | + | DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! + | + */ + public string $permittedURIChars = 'a-z 0-9~%.:_\-'; + + /** + * -------------------------------------------------------------------------- + * Default Locale + * -------------------------------------------------------------------------- + * + * The Locale roughly represents the language and location that your visitor + * is viewing the site from. It affects the language strings and other + * strings (like currency markers, numbers, etc), that your program + * should run under for this request. + */ + public string $defaultLocale = 'id'; + + /** + * -------------------------------------------------------------------------- + * Negotiate Locale + * -------------------------------------------------------------------------- + * + * If true, the current Request object will automatically determine the + * language to use based on the value of the Accept-Language header. + * + * If false, no automatic detection will be performed. + */ + public bool $negotiateLocale = false; + + /** + * -------------------------------------------------------------------------- + * Supported Locales + * -------------------------------------------------------------------------- + * + * If $negotiateLocale is true, this array lists the locales supported + * by the application in descending order of priority. If no match is + * found, the first locale will be used. + * + * IncomingRequest::setLocale() also uses this list. + * + * @var list + */ + public array $supportedLocales = ['id', 'en']; + + /** + * -------------------------------------------------------------------------- + * Application Timezone + * -------------------------------------------------------------------------- + * + * The default timezone that will be used in your application to display + * dates with the date helper, and can be retrieved through app_timezone() + * + * @see https://www.php.net/manual/en/timezones.php for list of timezones + * supported by PHP. + */ + public string $appTimezone = 'Asia/Jakarta'; + + /** + * -------------------------------------------------------------------------- + * Default Character Set + * -------------------------------------------------------------------------- + * + * This determines which character set is used by default in various methods + * that require a character set to be provided. + * + * @see http://php.net/htmlspecialchars for a list of supported charsets. + */ + public string $charset = 'UTF-8'; + + /** + * -------------------------------------------------------------------------- + * Force Global Secure Requests + * -------------------------------------------------------------------------- + * + * If true, this will force every request made to this application to be + * made via a secure connection (HTTPS). If the incoming request is not + * secure, the user will be redirected to a secure version of the page + * and the HTTP Strict Transport Security (HSTS) header will be set. + */ + public bool $forceGlobalSecureRequests = false; + + /** + * -------------------------------------------------------------------------- + * Reverse Proxy IPs + * -------------------------------------------------------------------------- + * + * If your server is behind a reverse proxy, you must whitelist the proxy + * IP addresses from which CodeIgniter should trust headers such as + * X-Forwarded-For or Client-IP in order to properly identify + * the visitor's IP address. + * + * You need to set a proxy IP address or IP address with subnets and + * the HTTP header for the client IP address. + * + * Here are some examples: + * [ + * '10.0.1.200' => 'X-Forwarded-For', + * '192.168.5.0/24' => 'X-Real-IP', + * ] + * + * @var array + */ + public array $proxyIPs = []; + + /** + * -------------------------------------------------------------------------- + * Content Security Policy + * -------------------------------------------------------------------------- + * + * Enables the Response's Content Secure Policy to restrict the sources that + * can be used for images, scripts, CSS files, audio, video, etc. If enabled, + * the Response object will populate default values for the policy from the + * `ContentSecurityPolicy.php` file. Controllers can always add to those + * restrictions at run time. + * + * For a better understanding of CSP, see these documents: + * + * @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/ + * @see http://www.w3.org/TR/CSP/ + */ + public bool $CSPEnabled = false; +} diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php new file mode 100644 index 0000000..9a92824 --- /dev/null +++ b/app/Config/Autoload.php @@ -0,0 +1,92 @@ +|string> + */ + public $psr4 = [ + APP_NAMESPACE => APPPATH, + ]; + + /** + * ------------------------------------------------------------------- + * Class Map + * ------------------------------------------------------------------- + * The class map provides a map of class names and their exact + * location on the drive. Classes loaded in this manner will have + * slightly faster performance because they will not have to be + * searched for within one or more directories as they would if they + * were being autoloaded through a namespace. + * + * Prototype: + * $classmap = [ + * 'MyClass' => '/path/to/class/file.php' + * ]; + * + * @var array + */ + public $classmap = []; + + /** + * ------------------------------------------------------------------- + * Files + * ------------------------------------------------------------------- + * The files array provides a list of paths to __non-class__ files + * that will be autoloaded. This can be useful for bootstrap operations + * or for loading functions. + * + * Prototype: + * $files = [ + * '/path/to/my/file.php', + * ]; + * + * @var list + */ + public $files = []; + + /** + * ------------------------------------------------------------------- + * Helpers + * ------------------------------------------------------------------- + * Prototype: + * $helpers = [ + * 'form', + * ]; + * + * @var list + */ + public $helpers = []; +} diff --git a/app/Config/Boot/development.php b/app/Config/Boot/development.php new file mode 100644 index 0000000..a868447 --- /dev/null +++ b/app/Config/Boot/development.php @@ -0,0 +1,34 @@ + WRITEPATH . 'cache/', + 'mode' => 0640, + ]; + + /** + * ------------------------------------------------------------------------- + * Memcached settings + * ------------------------------------------------------------------------- + * + * Your Memcached servers can be specified below, if you are using + * the Memcached drivers. + * + * @see https://codeigniter.com/user_guide/libraries/caching.html#memcached + * + * @var array{host?: string, port?: int, weight?: int, raw?: bool} + */ + public array $memcached = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'weight' => 1, + 'raw' => false, + ]; + + /** + * ------------------------------------------------------------------------- + * Redis settings + * ------------------------------------------------------------------------- + * + * Your Redis server can be specified below, if you are using + * the Redis or Predis drivers. + * + * @var array{host?: string, password?: string|null, port?: int, timeout?: int, database?: int} + */ + public array $redis = [ + 'host' => '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'timeout' => 0, + 'database' => 0, + ]; + + /** + * -------------------------------------------------------------------------- + * Available Cache Handlers + * -------------------------------------------------------------------------- + * + * This is an array of cache engine alias' and class names. Only engines + * that are listed here are allowed to be used. + * + * @var array> + */ + public array $validHandlers = [ + 'dummy' => DummyHandler::class, + 'file' => FileHandler::class, + 'memcached' => MemcachedHandler::class, + 'predis' => PredisHandler::class, + 'redis' => RedisHandler::class, + 'wincache' => WincacheHandler::class, + ]; + + /** + * -------------------------------------------------------------------------- + * Web Page Caching: Cache Include Query String + * -------------------------------------------------------------------------- + * + * Whether to take the URL query string into consideration when generating + * output cache files. Valid options are: + * + * false = Disabled + * true = Enabled, take all query parameters into account. + * Please be aware that this may result in numerous cache + * files generated for the same page over and over again. + * ['q'] = Enabled, but only take into account the specified list + * of query parameters. + * + * @var bool|list + */ + public $cacheQueryString = false; +} diff --git a/app/Config/Constants.php b/app/Config/Constants.php new file mode 100644 index 0000000..fb56bb1 --- /dev/null +++ b/app/Config/Constants.php @@ -0,0 +1,79 @@ +|string|null + */ + public $defaultSrc; + + /** + * Lists allowed scripts' URLs. + * + * @var list|string + */ + public $scriptSrc = 'self'; + + /** + * Lists allowed stylesheets' URLs. + * + * @var list|string + */ + public $styleSrc = 'self'; + + /** + * Defines the origins from which images can be loaded. + * + * @var list|string + */ + public $imageSrc = 'self'; + + /** + * Restricts the URLs that can appear in a page's `` element. + * + * Will default to self if not overridden + * + * @var list|string|null + */ + public $baseURI; + + /** + * Lists the URLs for workers and embedded frame contents + * + * @var list|string + */ + public $childSrc = 'self'; + + /** + * Limits the origins that you can connect to (via XHR, + * WebSockets, and EventSource). + * + * @var list|string + */ + public $connectSrc = 'self'; + + /** + * Specifies the origins that can serve web fonts. + * + * @var list|string + */ + public $fontSrc; + + /** + * Lists valid endpoints for submission from `
` tags. + * + * @var list|string + */ + public $formAction = 'self'; + + /** + * Specifies the sources that can embed the current page. + * This directive applies to ``, `'; + } + }; + const getFlashHtml = (data) => { + let html = ''; + if (data.poster) { + html += ''; + } + html += ''; + return html; + }; + const getAudioHtml = (data, audioTemplateCallback) => { + if (audioTemplateCallback) { + return audioTemplateCallback(data); + } + else { + return (''); + } + }; + const getVideoHtml = (data, videoTemplateCallback) => { + if (videoTemplateCallback) { + return videoTemplateCallback(data); + } + else { + return (''); + } + }; + const dataToHtml = (editor, dataIn) => { + const data = global$5.extend({}, dataIn); + if (!data.source) { + global$5.extend(data, htmlToData(data.embed ?? '', editor.schema)); + if (!data.source) { + return ''; + } + } + if (!data.altsource) { + data.altsource = ''; + } + if (!data.poster) { + data.poster = ''; + } + data.source = editor.convertURL(data.source, 'source'); + data.altsource = editor.convertURL(data.altsource, 'source'); + data.sourcemime = guess(data.source); + data.altsourcemime = guess(data.altsource); + data.poster = editor.convertURL(data.poster, 'poster'); + const pattern = matchPattern(data.source); + if (pattern) { + data.source = pattern.url; + data.type = pattern.type; + data.allowfullscreen = pattern.allowFullscreen; + data.width = data.width || String(pattern.w); + data.height = data.height || String(pattern.h); + } + if (data.embed) { + return updateHtml(data.embed, data, true, editor.schema); + } + else { + const audioTemplateCallback = getAudioTemplateCallback(editor); + const videoTemplateCallback = getVideoTemplateCallback(editor); + const iframeTemplateCallback = getIframeTemplateCallback(editor); + data.width = data.width || '300'; + data.height = data.height || '150'; + global$5.each(data, (value, key) => { + data[key] = editor.dom.encode('' + value); + }); + if (data.type === 'iframe') { + return getIframeHtml(data, iframeTemplateCallback); + } + else if (data.sourcemime === 'application/x-shockwave-flash') { + return getFlashHtml(data); + } + else if (data.sourcemime.indexOf('audio') !== -1) { + return getAudioHtml(data, audioTemplateCallback); + } + else { + return getVideoHtml(data, videoTemplateCallback); + } + } + }; + + const isMediaElement = (element) => element.hasAttribute('data-mce-object') || element.hasAttribute('data-ephox-embed-iri'); + const setup$2 = (editor) => { + // TINY-10774: On Safari all events bubble out even if you click on the video play button on other browsers the video element doesn't bubble the event + editor.on('mousedown', (e) => { + const previewObj = editor.dom.getParent(e.target, '.mce-preview-object'); + if (previewObj && editor.dom.getAttrib(previewObj, 'data-mce-selected') === '2') { + e.stopImmediatePropagation(); + } + }); + editor.on('click keyup touchend', () => { + const selectedNode = editor.selection.getNode(); + if (selectedNode && editor.dom.hasClass(selectedNode, 'mce-preview-object')) { + if (editor.dom.getAttrib(selectedNode, 'data-mce-selected')) { + selectedNode.setAttribute('data-mce-selected', '2'); + } + } + }); + editor.on('ObjectResized', (e) => { + const target = e.target; + if (target.getAttribute('data-mce-object')) { + let html = target.getAttribute('data-mce-html'); + if (html) { + html = unescape(html); + target.setAttribute('data-mce-html', escape(updateHtml(html, { + width: String(e.width), + height: String(e.height) + }, false, editor.schema))); + } + } + }); + }; + + const cache = {}; + const embedPromise = (data, dataToHtml, handler) => { + return new Promise((res, rej) => { + const wrappedResolve = (response) => { + if (response.html) { + cache[data.source] = response; + } + return res({ + url: data.source, + html: response.html ? response.html : dataToHtml(data) + }); + }; + if (cache[data.source]) { + wrappedResolve(cache[data.source]); + } + else { + handler({ url: data.source }).then(wrappedResolve).catch(rej); + } + }); + }; + const defaultPromise = (data, dataToHtml) => Promise.resolve({ html: dataToHtml(data), url: data.source }); + const loadedData = (editor) => (data) => dataToHtml(editor, data); + const getEmbedHtml = (editor, data) => { + const embedHandler = getUrlResolver(editor); + return embedHandler ? embedPromise(data, loadedData(editor), embedHandler) : defaultPromise(data, loadedData(editor)); + }; + const isCached = (url) => has(cache, url); + + const extractMeta = (sourceInput, data) => get$1(data, sourceInput).bind((mainData) => get$1(mainData, 'meta')); + const getValue = (data, metaData, sourceInput) => (prop) => { + // Cases: + // 1. Get the nested value prop (component is the executed urlinput) + // 2. Get from metadata (a urlinput was executed but urlinput != this component) + // 3. Not a urlinput so just get string + // If prop === sourceInput do 1, 2 then 3, else do 2 then 1 or 3 + // ASSUMPTION: we only want to get values for props that already exist in data + const getFromData = () => get$1(data, prop); + const getFromMetaData = () => get$1(metaData, prop); + const getNonEmptyValue = (c) => get$1(c, 'value').bind((v) => v.length > 0 ? Optional.some(v) : Optional.none()); + const getFromValueFirst = () => getFromData().bind((child) => isObject(child) + ? getNonEmptyValue(child).orThunk(getFromMetaData) + : getFromMetaData().orThunk(() => Optional.from(child))); + const getFromMetaFirst = () => getFromMetaData().orThunk(() => getFromData().bind((child) => isObject(child) + ? getNonEmptyValue(child) + : Optional.from(child))); + return { [prop]: (prop === sourceInput ? getFromValueFirst() : getFromMetaFirst()).getOr('') }; + }; + const getDimensions = (data, metaData) => { + const dimensions = {}; + get$1(data, 'dimensions').each((dims) => { + each$1(['width', 'height'], (prop) => { + get$1(metaData, prop).orThunk(() => get$1(dims, prop)).each((value) => dimensions[prop] = value); + }); + }); + return dimensions; + }; + const unwrap = (data, sourceInput) => { + const metaData = sourceInput && sourceInput !== 'dimensions' ? extractMeta(sourceInput, data).getOr({}) : {}; + const get = getValue(data, metaData, sourceInput); + return { + ...get('source'), + ...get('altsource'), + ...get('poster'), + ...get('embed'), + ...getDimensions(data, metaData) + }; + }; + const wrap = (data) => { + const wrapped = { + ...data, + source: { value: get$1(data, 'source').getOr('') }, + altsource: { value: get$1(data, 'altsource').getOr('') }, + poster: { value: get$1(data, 'poster').getOr('') } + }; + // Add additional size values that may or may not have been in the html + each$1(['width', 'height'], (prop) => { + get$1(data, prop).each((value) => { + const dimensions = wrapped.dimensions || {}; + dimensions[prop] = value; + wrapped.dimensions = dimensions; + }); + }); + return wrapped; + }; + const handleError = (editor) => (error) => { + const errorMessage = error && error.msg ? + 'Media embed handler error: ' + error.msg : + 'Media embed handler threw unknown error.'; + editor.notificationManager.open({ type: 'error', text: errorMessage }); + }; + const getEditorData = (editor) => { + const element = editor.selection.getNode(); + const snippet = isMediaElement(element) ? editor.serializer.serialize(element, { selection: true }) : ''; + const data = htmlToData(snippet, editor.schema); + const getDimensionsOfElement = () => { + if (isEmbedIframe(data.source, data.type)) { + const rect = editor.dom.getRect(element); + return { + width: rect.w.toString().replace(/px$/, ''), + height: rect.h.toString().replace(/px$/, ''), + }; + } + else { + return {}; + } + }; + const dimensions = getDimensionsOfElement(); + return { + embed: snippet, + ...data, + ...dimensions + }; + }; + const addEmbedHtml = (api, editor) => (response) => { + // Only set values if a URL has been defined + if (isString(response.url) && response.url.trim().length > 0) { + const html = response.html; + const snippetData = htmlToData(html, editor.schema); + const nuData = { + ...snippetData, + source: response.url, + embed: html + }; + api.setData(wrap(nuData)); + } + }; + const selectPlaceholder = (editor, beforeObjects) => { + const afterObjects = editor.dom.select('*[data-mce-object]'); + // Find new image placeholder so we can select it + for (let i = 0; i < beforeObjects.length; i++) { + for (let y = afterObjects.length - 1; y >= 0; y--) { + if (beforeObjects[i] === afterObjects[y]) { + afterObjects.splice(y, 1); + } + } + } + editor.selection.select(afterObjects[0]); + }; + const handleInsert = (editor, html) => { + const beforeObjects = editor.dom.select('*[data-mce-object]'); + editor.insertContent(html); + selectPlaceholder(editor, beforeObjects); + editor.nodeChanged(); + }; + const isEmbedIframe = (url, mediaDataType) => isNonNullable(mediaDataType) && mediaDataType === 'ephox-embed-iri' && isNonNullable(matchPattern(url)); + const shouldInsertAsNewIframe = (prevData, newData) => { + const hasDimensionsChanged = (prevData, newData) => prevData.width !== newData.width || prevData.height !== newData.height; + return hasDimensionsChanged(prevData, newData) && isEmbedIframe(newData.source, prevData.type); + }; + const submitForm = (prevData, newData, editor) => { + newData.embed = + shouldInsertAsNewIframe(prevData, newData) && hasDimensions(editor) + ? dataToHtml(editor, { ...newData, embed: '' }) + : updateHtml(newData.embed ?? '', newData, false, editor.schema); + // Only fetch the embed HTML content if the URL has changed from what it previously was + if (newData.embed && (prevData.source === newData.source || isCached(newData.source))) { + handleInsert(editor, newData.embed); + } + else { + getEmbedHtml(editor, newData) + .then((response) => { + handleInsert(editor, response.html); + }).catch(handleError(editor)); + } + }; + const showDialog = (editor) => { + const editorData = getEditorData(editor); + const currentData = Cell(editorData); + const initialData = wrap(editorData); + const handleSource = (prevData, api) => { + const serviceData = unwrap(api.getData(), 'source'); + // If a new URL is entered, then clear the embed html and fetch the new data + if (prevData.source !== serviceData.source) { + addEmbedHtml(win, editor)({ url: serviceData.source, html: '' }); + getEmbedHtml(editor, serviceData) + .then(addEmbedHtml(win, editor)) + .catch(handleError(editor)); + } + }; + const handleEmbed = (api) => { + const data = unwrap(api.getData()); + const dataFromEmbed = htmlToData(data.embed ?? '', editor.schema); + api.setData(wrap(dataFromEmbed)); + }; + const handleUpdate = (api, sourceInput, prevData) => { + const dialogData = unwrap(api.getData(), sourceInput); + const data = shouldInsertAsNewIframe(prevData, dialogData) && hasDimensions(editor) + ? { ...dialogData, embed: '' } + : dialogData; + const embed = dataToHtml(editor, data); + api.setData(wrap({ + ...data, + embed + })); + }; + const mediaInput = [{ + name: 'source', + type: 'urlinput', + filetype: 'media', + label: 'Source', + picker_text: 'Browse files' + }]; + const sizeInput = !hasDimensions(editor) ? [] : [{ + type: 'sizeinput', + name: 'dimensions', + label: 'Constrain proportions', + constrain: true + }]; + const generalTab = { + title: 'General', + name: 'general', + items: flatten([mediaInput, sizeInput]) + }; + const embedTextarea = { + type: 'textarea', + name: 'embed', + label: 'Paste your embed code below:' + }; + const embedTab = { + title: 'Embed', + items: [ + embedTextarea + ] + }; + const advancedFormItems = []; + if (hasAltSource(editor)) { + advancedFormItems.push({ + name: 'altsource', + type: 'urlinput', + filetype: 'media', + label: 'Alternative source URL' + }); + } + if (hasPoster(editor)) { + advancedFormItems.push({ + name: 'poster', + type: 'urlinput', + filetype: 'image', + label: 'Media poster (Image URL)' + }); + } + const advancedTab = { + title: 'Advanced', + name: 'advanced', + items: advancedFormItems + }; + const tabs = [ + generalTab, + embedTab + ]; + if (advancedFormItems.length > 0) { + tabs.push(advancedTab); + } + const body = { + type: 'tabpanel', + tabs + }; + const win = editor.windowManager.open({ + title: 'Insert/Edit Media', + size: 'normal', + body, + buttons: [ + { + type: 'cancel', + name: 'cancel', + text: 'Cancel' + }, + { + type: 'submit', + name: 'save', + text: 'Save', + primary: true + } + ], + onSubmit: (api) => { + const serviceData = unwrap(api.getData()); + submitForm(currentData.get(), serviceData, editor); + api.close(); + }, + onChange: (api, detail) => { + switch (detail.name) { + case 'source': + handleSource(currentData.get(), api); + break; + case 'embed': + handleEmbed(api); + break; + case 'dimensions': + case 'altsource': + case 'poster': + handleUpdate(api, detail.name, currentData.get()); + break; + } + currentData.set(unwrap(api.getData())); + }, + initialData + }); + }; + + const get = (editor) => { + const showDialog$1 = () => { + showDialog(editor); + }; + return { + showDialog: showDialog$1 + }; + }; + + const register$1 = (editor) => { + const showDialog$1 = () => { + showDialog(editor); + }; + editor.addCommand('mceMedia', showDialog$1); + }; + + var global = tinymce.util.Tools.resolve('tinymce.Env'); + + const isLiveEmbedNode = (node) => { + const name = node.name; + return name === 'iframe' || name === 'video' || name === 'audio'; + }; + const getDimension = (node, styles, dimension, defaultValue = null) => { + const value = node.attr(dimension); + if (isNonNullable(value)) { + return value; + } + else if (!has(styles, dimension)) { + return defaultValue; + } + else { + return null; + } + }; + const setDimensions = (node, previewNode, styles) => { + // Apply dimensions for video elements to maintain legacy behaviour + const useDefaults = previewNode.name === 'img' || node.name === 'video'; + // Determine the defaults + const defaultWidth = useDefaults ? '300' : null; + const fallbackHeight = node.name === 'audio' ? '30' : '150'; + const defaultHeight = useDefaults ? fallbackHeight : null; + previewNode.attr({ + width: getDimension(node, styles, 'width', defaultWidth), + height: getDimension(node, styles, 'height', defaultHeight) + }); + }; + const appendNodeContent = (editor, nodeName, previewNode, html) => { + const newNode = Parser(editor.schema).parse(html, { context: nodeName }); + while (newNode.firstChild) { + previewNode.append(newNode.firstChild); + } + }; + const createPlaceholderNode = (editor, node) => { + const name = node.name; + const placeHolder = new global$2('img', 1); + retainAttributesAndInnerHtml(editor, node, placeHolder); + setDimensions(node, placeHolder, {}); + placeHolder.attr({ + 'style': node.attr('style'), + 'src': global.transparentSrc, + 'data-mce-object': name, + 'class': 'mce-object mce-object-' + name + }); + return placeHolder; + }; + const createPreviewNode = (editor, node) => { + const name = node.name; + const previewWrapper = new global$2('span', 1); + previewWrapper.attr({ + 'contentEditable': 'false', + 'style': node.attr('style'), + 'data-mce-object': name, + 'class': 'mce-preview-object mce-object-' + name + }); + retainAttributesAndInnerHtml(editor, node, previewWrapper); + const styles = editor.dom.parseStyle(node.attr('style') ?? ''); + const previewNode = new global$2(name, 1); + setDimensions(node, previewNode, styles); + previewNode.attr({ + src: node.attr('src'), + style: node.attr('style'), + class: node.attr('class') + }); + if (name === 'iframe') { + previewNode.attr({ + allowfullscreen: node.attr('allowfullscreen'), + frameborder: '0', + sandbox: node.attr('sandbox'), + referrerpolicy: node.attr('referrerpolicy') + }); + } + else { + // Exclude autoplay as we don't want video/audio to play by default + const attrs = ['controls', 'crossorigin', 'currentTime', 'loop', 'muted', 'poster', 'preload']; + each$1(attrs, (attrName) => { + previewNode.attr(attrName, node.attr(attrName)); + }); + // Recreate the child nodes using the sanitized inner HTML + const sanitizedHtml = previewWrapper.attr('data-mce-html'); + if (isNonNullable(sanitizedHtml)) { + appendNodeContent(editor, name, previewNode, unescape(sanitizedHtml)); + } + } + const shimNode = new global$2('span', 1); + shimNode.attr('class', 'mce-shim'); + previewWrapper.append(previewNode); + previewWrapper.append(shimNode); + return previewWrapper; + }; + const retainAttributesAndInnerHtml = (editor, sourceNode, targetNode) => { + // Prefix all attributes except internal (data-mce-*), width, height and style since we + // will add these to the placeholder + const attribs = sourceNode.attributes ?? []; + let ai = attribs.length; + while (ai--) { + const attrName = attribs[ai].name; + let attrValue = attribs[ai].value; + if (attrName !== 'width' && attrName !== 'height' && attrName !== 'style' && !startsWith(attrName, 'data-mce-')) { + if (attrName === 'data' || attrName === 'src') { + attrValue = editor.convertURL(attrValue, attrName); + } + targetNode.attr('data-mce-p-' + attrName, attrValue); + } + } + // Place the inner HTML contents inside an escaped attribute + // This enables us to copy/paste the fake object + const serializer = global$1({ inner: true }, editor.schema); + const tempNode = new global$2('div', 1); + each$1(sourceNode.children(), (child) => tempNode.append(child)); + const innerHtml = serializer.serialize(tempNode); + if (innerHtml) { + targetNode.attr('data-mce-html', escape(innerHtml)); + targetNode.empty(); + } + }; + const isPageEmbedWrapper = (node) => { + const nodeClass = node.attr('class'); + return isString(nodeClass) && /\btiny-pageembed\b/.test(nodeClass); + }; + const isWithinEmbedWrapper = (node) => { + let tempNode = node; + while ((tempNode = tempNode.parent)) { + if (tempNode.attr('data-ephox-embed-iri') || isPageEmbedWrapper(tempNode)) { + return true; + } + } + return false; + }; + const placeHolderConverter = (editor) => (nodes) => { + let i = nodes.length; + let node; + while (i--) { + node = nodes[i]; + if (!node.parent) { + continue; + } + if (node.parent.attr('data-mce-object')) { + continue; + } + if (isLiveEmbedNode(node) && hasLiveEmbeds(editor)) { + if (!isWithinEmbedWrapper(node)) { + node.replace(createPreviewNode(editor, node)); + } + } + else { + if (!isWithinEmbedWrapper(node)) { + node.replace(createPlaceholderNode(editor, node)); + } + } + } + }; + + const parseAndSanitize = (editor, context, html) => { + const getEditorOption = editor.options.get; + const sanitize = getEditorOption('xss_sanitization'); + const validate = shouldFilterHtml(editor); + return Parser(editor.schema, { sanitize, validate }).parse(html, { context }); + }; + + const setup$1 = (editor) => { + editor.on('PreInit', () => { + const { schema, serializer, parser } = editor; + // Set browser specific allowFullscreen attribs as boolean + const boolAttrs = schema.getBoolAttrs(); + each$1('webkitallowfullscreen mozallowfullscreen'.split(' '), (name) => { + boolAttrs[name] = {}; + }); + // Add some non-standard attributes to the schema + each({ + embed: ['wmode'] + }, (attrs, name) => { + const rule = schema.getElementRule(name); + if (rule) { + each$1(attrs, (attr) => { + rule.attributes[attr] = {}; + rule.attributesOrder.push(attr); + }); + } + }); + // Converts iframe, video etc into placeholder images + parser.addNodeFilter('iframe,video,audio,object,embed', placeHolderConverter(editor)); + // Replaces placeholder images with real elements for video, object, iframe etc + serializer.addAttributeFilter('data-mce-object', (nodes, name) => { + let i = nodes.length; + while (i--) { + const node = nodes[i]; + if (!node.parent) { + continue; + } + const realElmName = node.attr(name); + const realElm = new global$2(realElmName, 1); + // Add width/height to everything but audio + if (realElmName !== 'audio') { + const className = node.attr('class'); + if (className && className.indexOf('mce-preview-object') !== -1 && node.firstChild) { + realElm.attr({ + width: node.firstChild.attr('width'), + height: node.firstChild.attr('height') + }); + } + else { + realElm.attr({ + width: node.attr('width'), + height: node.attr('height') + }); + } + } + realElm.attr({ + style: node.attr('style') + }); + // Unprefix all placeholder attributes + const attribs = node.attributes ?? []; + let ai = attribs.length; + while (ai--) { + const attrName = attribs[ai].name; + if (attrName.indexOf('data-mce-p-') === 0) { + realElm.attr(attrName.substr(11), attribs[ai].value); + } + } + // Inject innerhtml + const innerHtml = node.attr('data-mce-html'); + if (innerHtml) { + const fragment = parseAndSanitize(editor, realElmName, unescape(innerHtml)); + each$1(fragment.children(), (child) => realElm.append(child)); + } + node.replace(realElm); + } + }); + }); + editor.on('SetContent', () => { + // TODO: This shouldn't be needed there should be a way to mark bogus + // elements so they are never removed except external save + const dom = editor.dom; + each$1(dom.select('span.mce-preview-object'), (elm) => { + if (dom.select('span.mce-shim', elm).length === 0) { + dom.add(elm, 'span', { class: 'mce-shim' }); + } + }); + }); + }; + + const setup = (editor) => { + editor.on('ResolveName', (e) => { + let name; + if (e.target.nodeType === 1 && (name = e.target.getAttribute('data-mce-object'))) { + e.name = name; + } + }); + }; + + const onSetupEditable = (editor) => (api) => { + const nodeChanged = () => { + api.setEnabled(editor.selection.isEditable()); + }; + editor.on('NodeChange', nodeChanged); + nodeChanged(); + return () => { + editor.off('NodeChange', nodeChanged); + }; + }; + const register = (editor) => { + const onAction = () => editor.execCommand('mceMedia'); + editor.ui.registry.addToggleButton('media', { + tooltip: 'Insert/edit media', + icon: 'embed', + onAction, + onSetup: (buttonApi) => { + const selection = editor.selection; + buttonApi.setActive(isMediaElement(selection.getNode())); + const unbindSelectorChanged = selection.selectorChangedWithUnbind('img[data-mce-object],span[data-mce-object],div[data-ephox-embed-iri]', buttonApi.setActive).unbind; + const unbindEditable = onSetupEditable(editor)(buttonApi); + return () => { + unbindSelectorChanged(); + unbindEditable(); + }; + } + }); + editor.ui.registry.addMenuItem('media', { + icon: 'embed', + text: 'Media...', + onAction, + onSetup: onSetupEditable(editor) + }); + }; + + var Plugin = () => { + global$6.add('media', (editor) => { + register$2(editor); + register$1(editor); + register(editor); + setup(editor); + setup$1(editor); + setup$2(editor); + return get(editor); + }); + }; + + Plugin(); + /** ***** + * DO NOT EXPORT ANYTHING + * + * IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE + *******/ + +})(); diff --git a/public/assets/js/tinymce/plugins/media/plugin.min.js b/public/assets/js/tinymce/plugins/media/plugin.min.js new file mode 100644 index 0000000..0e1ce5e --- /dev/null +++ b/public/assets/js/tinymce/plugins/media/plugin.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(r=o=e,(a=String).prototype.isPrototypeOf(r)||o.constructor?.name===a.name)?"string":t;var r,o,a})(t)===e,r=t("string"),o=t("object"),a=t("array"),s=e=>!(e=>null==e)(e),i=e=>"function"==typeof e;class n{tag;value;static singletonNone=new n(!1);constructor(e,t){this.tag=e,this.value=t}static some(e){return new n(!0,e)}static none(){return n.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?n.some(e(this.value)):n.none()}bind(e){return this.tag?e(this.value):n.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:n.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(e??"Called getOrDie on None")}static from(e){return s(e)?n.some(e):n.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}Array.prototype.slice;const c=Array.prototype.push,l=(e,t)=>{for(let r=0,o=e.length;r{const t=[];for(let r=0,o=e.length;rp(e,t)?n.from(e[t]):n.none(),p=(e,t)=>d.call(e,t),g=e=>t=>t.options.get(e),b=g("audio_template_callback"),w=g("video_template_callback"),f=g("iframe_template_callback"),y=g("media_live_embeds"),v=g("media_filter_html"),x=g("media_url_resolver"),_=g("media_alt_source"),k=g("media_poster"),j=g("media_dimensions");var A=tinymce.util.Tools.resolve("tinymce.util.Tools"),O=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),S=tinymce.util.Tools.resolve("tinymce.html.DomParser");const $=O.DOM,C=e=>e.replace(/px$/,""),T=e=>{const t=e.attr("style"),r=t?$.parseStyle(t):{};return{type:"ephox-embed-iri",source:e.attr("data-ephox-embed-iri"),altsource:"",poster:"",width:h(r,"max-width").map(C).getOr(""),height:h(r,"max-height").map(C).getOr("")}},z=(e,t)=>{let r={};for(let o=S({validate:!1,forced_root_block:!1},t).parse(e);o;o=o.walk())if(1===o.type){const e=o.name;if(o.attr("data-ephox-embed-iri")){r=T(o);break}r.source||"param"!==e||(r.source=o.attr("movie")),"iframe"!==e&&"object"!==e&&"embed"!==e&&"video"!==e&&"audio"!==e||(r.type||(r.type=e),r=A.extend(o.attributes.map,r)),"source"===e&&(r.source?r.altsource||(r.altsource=o.attr("src")):r.source=o.attr("src")),"img"!==e||r.poster||(r.poster=o.attr("src"))}return r.source=r.source||r.src||"",r.altsource=r.altsource||"",r.poster=r.poster||"",r},D=e=>{const t=e.toLowerCase().split(".").pop()??"";return h({mp3:"audio/mpeg",m4a:"audio/x-m4a",wav:"audio/wav",mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",swf:"application/x-shockwave-flash"},t).getOr("")};var F=tinymce.util.Tools.resolve("tinymce.html.Node"),M=tinymce.util.Tools.resolve("tinymce.html.Serializer");const N=(e,t={})=>S({forced_root_block:!1,validate:!1,allow_conditional_comments:!0,...t},e),P=O.DOM,R=e=>/^[0-9.]+$/.test(e)?e+"px":e,E=(e,t)=>{const r=t.attr("style"),o=r?P.parseStyle(r):{};s(e.width)&&(o["max-width"]=R(e.width)),s(e.height)&&(o["max-height"]=R(e.height)),t.attr("style",P.serializeStyle(o))},U=["source","altsource"],L=(e,t,r,o)=>{let a=0,s=0;const i=N(o);i.addNodeFilter("source",(e=>a=e.length));const n=i.parse(e);for(let e=n;e;e=e.walk())if(1===e.type){const o=e.name;if(e.attr("data-ephox-embed-iri")){E(t,e);break}switch(o){case"video":case"object":case"embed":case"img":case"iframe":void 0!==t.height&&void 0!==t.width&&(e.attr("width",t.width),e.attr("height",t.height))}if(r)switch(o){case"video":e.attr("poster",t.poster),e.attr("src",null);for(let r=a;r<2;r++)if(t[U[r]]){const o=new F("source",1);o.attr("src",t[U[r]]),o.attr("type",t[U[r]+"mime"]||null),e.append(o)}break;case"iframe":e.attr("src",t.source);break;case"object":const r=e.getAll("img").length>0;if(t.poster&&!r){e.attr("src",t.poster);const r=new F("img",1);r.attr("src",t.poster),r.attr("width",t.width),r.attr("height",t.height),e.append(r)}break;case"source":if(s<2&&(e.attr("src",t[U[s]]),e.attr("type",t[U[s]+"mime"]||null),!t[U[s]])){e.remove();continue}s++;break;case"img":t.poster||e.remove()}}return M({},o).serialize(n)},I=[{regex:/youtu\.be\/([\w\-_\?&=.]+)/i,type:"iframe",w:560,h:314,url:"www.youtube.com/embed/$1",allowFullscreen:!0},{regex:/youtube\.com(.+)v=([^&]+)(&([a-z0-9&=\-_]+))?/i,type:"iframe",w:560,h:314,url:"www.youtube.com/embed/$2?$4",allowFullscreen:!0},{regex:/youtube.com\/embed\/([a-z0-9\?&=\-_]+)/i,type:"iframe",w:560,h:314,url:"www.youtube.com/embed/$1",allowFullscreen:!0},{regex:/vimeo\.com\/([0-9]+)\?h=(\w+)/,type:"iframe",w:425,h:350,url:"player.vimeo.com/video/$1?h=$2&title=0&byline=0&portrait=0&color=8dc7dc",allowFullscreen:!0},{regex:/vimeo\.com\/(.*)\/([0-9]+)\?h=(\w+)/,type:"iframe",w:425,h:350,url:"player.vimeo.com/video/$2?h=$3&title=0&byline=0",allowFullscreen:!0},{regex:/vimeo\.com\/([0-9]+)/,type:"iframe",w:425,h:350,url:"player.vimeo.com/video/$1?title=0&byline=0&portrait=0&color=8dc7dc",allowFullscreen:!0},{regex:/vimeo\.com\/(.*)\/([0-9]+)/,type:"iframe",w:425,h:350,url:"player.vimeo.com/video/$2?title=0&byline=0",allowFullscreen:!0},{regex:/maps\.google\.([a-z]{2,3})\/maps\/(.+)msid=(.+)/,type:"iframe",w:425,h:350,url:'maps.google.com/maps/ms?msid=$2&output=embed"',allowFullscreen:!1},{regex:/dailymotion\.com\/video\/([^_]+)/,type:"iframe",w:480,h:270,url:"www.dailymotion.com/embed/video/$1",allowFullscreen:!0},{regex:/dai\.ly\/([^_]+)/,type:"iframe",w:480,h:270,url:"www.dailymotion.com/embed/video/$1",allowFullscreen:!0}],B=(e,t)=>{const r=(e=>{const t=e.match(/^(https?:\/\/|www\.)(.+)$/i);return t&&t.length>1?"www."===t[1]?"https://":t[1]:"https://"})(t),o=e.regex.exec(t);let a=r+e.url;if(s(o))for(let e=0;eo[e]?o[e]:""));return a.replace(/\?$/,"")},G=e=>{const t=I.filter((t=>t.regex.test(e)));return t.length>0?A.extend({},t[0],{url:B(t[0],e)}):null},W=(e,t)=>{const r=A.extend({},t);if(!r.source&&(A.extend(r,z(r.embed??"",e.schema)),!r.source))return"";r.altsource||(r.altsource=""),r.poster||(r.poster=""),r.source=e.convertURL(r.source,"source"),r.altsource=e.convertURL(r.altsource,"source"),r.sourcemime=D(r.source),r.altsourcemime=D(r.altsource),r.poster=e.convertURL(r.poster,"poster");const o=G(r.source);if(o&&(r.source=o.url,r.type=o.type,r.allowfullscreen=o.allowFullscreen,r.width=r.width||String(o.w),r.height=r.height||String(o.h)),r.embed)return L(r.embed,r,!0,e.schema);{const t=b(e),o=w(e),a=f(e);return r.width=r.width||"300",r.height=r.height||"150",A.each(r,((t,o)=>{r[o]=e.dom.encode(""+t)})),"iframe"===r.type?((e,t)=>{if(t)return t(e);{const t=e.allowfullscreen?' allowFullscreen="1"':"";return'"}})(r,a):"application/x-shockwave-flash"===r.sourcemime?(e=>{let t='';return e.poster&&(t+=''),t+="",t})(r):-1!==r.sourcemime.indexOf("audio")?((e,t)=>t?t(e):'")(r,t):((e,t)=>t?t(e):'")(r,o)}},q=e=>e.hasAttribute("data-mce-object")||e.hasAttribute("data-ephox-embed-iri"),H={},J=e=>t=>W(e,t),K=(e,t)=>{const r=x(e);return r?((e,t,r)=>new Promise(((o,a)=>{const s=r=>(r.html&&(H[e.source]=r),o({url:e.source,html:r.html?r.html:t(e)}));H[e.source]?s(H[e.source]):r({url:e.source}).then(s).catch(a)})))(t,J(e),r):((e,t)=>Promise.resolve({html:t(e),url:e.source}))(t,J(e))},Q=(e,t)=>{const r={};return h(e,"dimensions").each((e=>{l(["width","height"],(o=>{h(t,o).orThunk((()=>h(e,o))).each((e=>r[o]=e))}))})),r},V=(e,t)=>{const r=t&&"dimensions"!==t?((e,t)=>h(t,e).bind((e=>h(e,"meta"))))(t,e).getOr({}):{},a=((e,t,r)=>a=>{const s=()=>h(e,a),i=()=>h(t,a),c=e=>h(e,"value").bind((e=>e.length>0?n.some(e):n.none()));return{[a]:(a===r?s().bind((e=>o(e)?c(e).orThunk(i):i().orThunk((()=>n.from(e))))):i().orThunk((()=>s().bind((e=>o(e)?c(e):n.from(e)))))).getOr("")}})(e,r,t);return{...a("source"),...a("altsource"),...a("poster"),...a("embed"),...Q(e,r)}},X=e=>{const t={...e,source:{value:h(e,"source").getOr("")},altsource:{value:h(e,"altsource").getOr("")},poster:{value:h(e,"poster").getOr("")}};return l(["width","height"],(r=>{h(e,r).each((e=>{const o=t.dimensions||{};o[r]=e,t.dimensions=o}))})),t},Y=e=>t=>{const r=t&&t.msg?"Media embed handler error: "+t.msg:"Media embed handler threw unknown error.";e.notificationManager.open({type:"error",text:r})},Z=(e,t)=>o=>{if(r(o.url)&&o.url.trim().length>0){const r=o.html,a={...z(r,t.schema),source:o.url,embed:r};e.setData(X(a))}},ee=(e,t)=>{const r=e.dom.select("*[data-mce-object]");e.insertContent(t),((e,t)=>{const r=e.dom.select("*[data-mce-object]");for(let e=0;e=0;o--)t[e]===r[o]&&r.splice(o,1);e.selection.select(r[0])})(e,r),e.nodeChanged()},te=(e,t)=>s(t)&&"ephox-embed-iri"===t&&s(G(e)),re=(e,t)=>((e,t)=>e.width!==t.width||e.height!==t.height)(e,t)&&te(t.source,e.type),oe=e=>{const t=(e=>{const t=e.selection.getNode(),r=q(t)?e.serializer.serialize(t,{selection:!0}):"",o=z(r,e.schema),a=(()=>{if(te(o.source,o.type)){const r=e.dom.getRect(t);return{width:r.w.toString().replace(/px$/,""),height:r.h.toString().replace(/px$/,"")}}return{}})();return{embed:r,...o,...a}})(e),r=(e=>{let t=e;return{get:()=>t,set:e=>{t=e}}})(t),o=X(t),a=j(e)?[{type:"sizeinput",name:"dimensions",label:"Constrain proportions",constrain:!0}]:[],s={title:"General",name:"general",items:m([[{name:"source",type:"urlinput",filetype:"media",label:"Source",picker_text:"Browse files"}],a])},i=[];_(e)&&i.push({name:"altsource",type:"urlinput",filetype:"media",label:"Alternative source URL"}),k(e)&&i.push({name:"poster",type:"urlinput",filetype:"image",label:"Media poster (Image URL)"});const n={title:"Advanced",name:"advanced",items:i},c=[s,{title:"Embed",items:[{type:"textarea",name:"embed",label:"Paste your embed code below:"}]}];i.length>0&&c.push(n);const l={type:"tabpanel",tabs:c},u=e.windowManager.open({title:"Insert/Edit Media",size:"normal",body:l,buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],onSubmit:t=>{const o=V(t.getData());((e,t,r)=>{var o;t.embed=re(e,t)&&j(r)?W(r,{...t,embed:""}):L(t.embed??"",t,!1,r.schema),t.embed&&(e.source===t.source||(o=t.source,p(H,o)))?ee(r,t.embed):K(r,t).then((e=>{ee(r,e.html)})).catch(Y(r))})(r.get(),o,e),t.close()},onChange:(t,o)=>{switch(o.name){case"source":((t,r)=>{const o=V(r.getData(),"source");t.source!==o.source&&(Z(u,e)({url:o.source,html:""}),K(e,o).then(Z(u,e)).catch(Y(e)))})(r.get(),t);break;case"embed":(t=>{const r=V(t.getData()),o=z(r.embed??"",e.schema);t.setData(X(o))})(t);break;case"dimensions":case"altsource":case"poster":((t,r,o)=>{const a=V(t.getData(),r),s=re(o,a)&&j(e)?{...a,embed:""}:a,i=W(e,s);t.setData(X({...s,embed:i}))})(t,o.name,r.get())}r.set(V(t.getData()))},initialData:o})};var ae=tinymce.util.Tools.resolve("tinymce.Env");const se=e=>{const t=e.name;return"iframe"===t||"video"===t||"audio"===t},ie=(e,t,r,o=null)=>{const a=e.attr(r);return s(a)?a:p(t,r)?null:o},ne=(e,t,r)=>{const o="img"===t.name||"video"===e.name,a=o?"300":null,s="audio"===e.name?"30":"150",i=o?s:null;t.attr({width:ie(e,r,"width",a),height:ie(e,r,"height",i)})},ce=(e,t)=>{const r=t.name,o=new F("img",1);return me(e,t,o),ne(t,o,{}),o.attr({style:t.attr("style"),src:ae.transparentSrc,"data-mce-object":r,class:"mce-object mce-object-"+r}),o},le=(e,t)=>{const r=t.name,o=new F("span",1);o.attr({contentEditable:"false",style:t.attr("style"),"data-mce-object":r,class:"mce-preview-object mce-object-"+r}),me(e,t,o);const a=e.dom.parseStyle(t.attr("style")??""),i=new F(r,1);if(ne(t,i,a),i.attr({src:t.attr("src"),style:t.attr("style"),class:t.attr("class")}),"iframe"===r)i.attr({allowfullscreen:t.attr("allowfullscreen"),frameborder:"0",sandbox:t.attr("sandbox"),referrerpolicy:t.attr("referrerpolicy")});else{l(["controls","crossorigin","currentTime","loop","muted","poster","preload"],(e=>{i.attr(e,t.attr(e))}));const a=o.attr("data-mce-html");s(a)&&((e,t,r,o)=>{const a=N(e.schema).parse(o,{context:t});for(;a.firstChild;)r.append(a.firstChild)})(e,r,i,unescape(a))}const n=new F("span",1);return n.attr("class","mce-shim"),o.append(i),o.append(n),o},me=(e,t,r)=>{const o=t.attributes??[];let a=o.length;for(;a--;){const t=o[a].name;let n=o[a].value;"width"===t||"height"===t||"style"===t||(i="data-mce-",(s=t).length>=9&&s.substr(0,9)===i)||("data"!==t&&"src"!==t||(n=e.convertURL(n,t)),r.attr("data-mce-p-"+t,n))}var s,i;const n=M({inner:!0},e.schema),c=new F("div",1);l(t.children(),(e=>c.append(e)));const m=n.serialize(c);m&&(r.attr("data-mce-html",escape(m)),r.empty())},ue=e=>{const t=e.attr("class");return r(t)&&/\btiny-pageembed\b/.test(t)},de=e=>{let t=e;for(;t=t.parent;)if(t.attr("data-ephox-embed-iri")||ue(t))return!0;return!1},he=(e,t,r)=>{const o=(0,e.options.get)("xss_sanitization"),a=v(e);return N(e.schema,{sanitize:o,validate:a}).parse(r,{context:t})},pe=e=>t=>{const r=()=>{t.setEnabled(e.selection.isEditable())};return e.on("NodeChange",r),r(),()=>{e.off("NodeChange",r)}};e.add("media",(e=>((e=>{const t=e.options.register;t("audio_template_callback",{processor:"function"}),t("video_template_callback",{processor:"function"}),t("iframe_template_callback",{processor:"function"}),t("media_live_embeds",{processor:"boolean",default:!0}),t("media_filter_html",{processor:"boolean",default:!0}),t("media_url_resolver",{processor:"function"}),t("media_alt_source",{processor:"boolean",default:!0}),t("media_poster",{processor:"boolean",default:!0}),t("media_dimensions",{processor:"boolean",default:!0})})(e),(e=>{e.addCommand("mceMedia",(()=>{oe(e)}))})(e),(e=>{const t=()=>e.execCommand("mceMedia");e.ui.registry.addToggleButton("media",{tooltip:"Insert/edit media",icon:"embed",onAction:t,onSetup:t=>{const r=e.selection;t.setActive(q(r.getNode()));const o=r.selectorChangedWithUnbind("img[data-mce-object],span[data-mce-object],div[data-ephox-embed-iri]",t.setActive).unbind,a=pe(e)(t);return()=>{o(),a()}}}),e.ui.registry.addMenuItem("media",{icon:"embed",text:"Media...",onAction:t,onSetup:pe(e)})})(e),(e=>{e.on("ResolveName",(e=>{let t;1===e.target.nodeType&&(t=e.target.getAttribute("data-mce-object"))&&(e.name=t)}))})(e),(e=>{e.on("PreInit",(()=>{const{schema:t,serializer:r,parser:o}=e,a=t.getBoolAttrs();l("webkitallowfullscreen mozallowfullscreen".split(" "),(e=>{a[e]={}})),((e,t)=>{const r=u(e);for(let o=0,a=r.length;o{const o=t.getElementRule(r);o&&l(e,(e=>{o.attributes[e]={},o.attributesOrder.push(e)}))})),o.addNodeFilter("iframe,video,audio,object,embed",(e=>t=>{let r,o=t.length;for(;o--;)r=t[o],r.parent&&(r.parent.attr("data-mce-object")||(se(r)&&y(e)?de(r)||r.replace(le(e,r)):de(r)||r.replace(ce(e,r))))})(e)),r.addAttributeFilter("data-mce-object",((t,r)=>{let o=t.length;for(;o--;){const a=t[o];if(!a.parent)continue;const s=a.attr(r),i=new F(s,1);if("audio"!==s){const e=a.attr("class");e&&-1!==e.indexOf("mce-preview-object")&&a.firstChild?i.attr({width:a.firstChild.attr("width"),height:a.firstChild.attr("height")}):i.attr({width:a.attr("width"),height:a.attr("height")})}i.attr({style:a.attr("style")});const n=a.attributes??[];let c=n.length;for(;c--;){const e=n[c].name;0===e.indexOf("data-mce-p-")&&i.attr(e.substr(11),n[c].value)}const m=a.attr("data-mce-html");if(m){const t=he(e,s,unescape(m));l(t.children(),(e=>i.append(e)))}a.replace(i)}}))})),e.on("SetContent",(()=>{const t=e.dom;l(t.select("span.mce-preview-object"),(e=>{0===t.select("span.mce-shim",e).length&&t.add(e,"span",{class:"mce-shim"})}))}))})(e),(e=>{e.on("mousedown",(t=>{const r=e.dom.getParent(t.target,".mce-preview-object");r&&"2"===e.dom.getAttrib(r,"data-mce-selected")&&t.stopImmediatePropagation()})),e.on("click keyup touchend",(()=>{const t=e.selection.getNode();t&&e.dom.hasClass(t,"mce-preview-object")&&e.dom.getAttrib(t,"data-mce-selected")&&t.setAttribute("data-mce-selected","2")})),e.on("ObjectResized",(t=>{const r=t.target;if(r.getAttribute("data-mce-object")){let o=r.getAttribute("data-mce-html");o&&(o=unescape(o),r.setAttribute("data-mce-html",escape(L(o,{width:String(t.width),height:String(t.height)},!1,e.schema))))}}))})(e),(e=>({showDialog:()=>{oe(e)}}))(e))))}(); \ No newline at end of file diff --git a/public/assets/js/tinymce/plugins/nonbreaking/index.js b/public/assets/js/tinymce/plugins/nonbreaking/index.js new file mode 100644 index 0000000..b38ef5e --- /dev/null +++ b/public/assets/js/tinymce/plugins/nonbreaking/index.js @@ -0,0 +1,7 @@ +// Exports the "nonbreaking" plugin for usage with module loaders +// Usage: +// CommonJS: +// require('tinymce/plugins/nonbreaking') +// ES2015: +// import 'tinymce/plugins/nonbreaking' +require('./plugin.js'); \ No newline at end of file diff --git a/public/assets/js/tinymce/plugins/nonbreaking/plugin.js b/public/assets/js/tinymce/plugins/nonbreaking/plugin.js new file mode 100644 index 0000000..482ccb3 --- /dev/null +++ b/public/assets/js/tinymce/plugins/nonbreaking/plugin.js @@ -0,0 +1,128 @@ +/** + * TinyMCE version 8.3.1 (2025-12-17) + */ + +(function () { + 'use strict'; + + var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager'); + + /* eslint-disable @typescript-eslint/no-wrapper-object-types */ + const isSimpleType = (type) => (value) => typeof value === type; + const isBoolean = isSimpleType('boolean'); + const isNumber = isSimpleType('number'); + + const option = (name) => (editor) => editor.options.get(name); + const register$2 = (editor) => { + const registerOption = editor.options.register; + registerOption('nonbreaking_force_tab', { + processor: (value) => { + if (isBoolean(value)) { + return { value: value ? 3 : 0, valid: true }; + } + else if (isNumber(value)) { + return { value, valid: true }; + } + else { + return { valid: false, message: 'Must be a boolean or number.' }; + } + }, + default: false + }); + registerOption('nonbreaking_wrap', { + processor: 'boolean', + default: true + }); + }; + const getKeyboardSpaces = option('nonbreaking_force_tab'); + const wrapNbsps = option('nonbreaking_wrap'); + + const stringRepeat = (string, repeats) => { + let str = ''; + for (let index = 0; index < repeats; index++) { + str += string; + } + return str; + }; + const isVisualCharsEnabled = (editor) => editor.plugins.visualchars ? editor.plugins.visualchars.isEnabled() : false; + const insertNbsp = (editor, times) => { + const classes = () => isVisualCharsEnabled(editor) ? 'mce-nbsp-wrap mce-nbsp' : 'mce-nbsp-wrap'; + const nbspSpan = () => `${stringRepeat(' ', times)}`; + const shouldWrap = wrapNbsps(editor); + const html = shouldWrap || editor.plugins.visualchars ? nbspSpan() : stringRepeat(' ', times); + editor.undoManager.transact(() => editor.insertContent(html)); + }; + + const register$1 = (editor) => { + editor.addCommand('mceNonBreaking', () => { + insertNbsp(editor, 1); + }); + }; + + var global = tinymce.util.Tools.resolve('tinymce.util.VK'); + + const setup = (editor) => { + const spaces = getKeyboardSpaces(editor); + if (spaces > 0) { + editor.on('keydown', (e) => { + if (e.keyCode === global.TAB && !e.isDefaultPrevented()) { + if (e.shiftKey) { + return; + } + e.preventDefault(); + e.stopImmediatePropagation(); + insertNbsp(editor, spaces); + } + }); + } + }; + + const onSetupEditable = (editor) => (api) => { + const nodeChanged = () => { + api.setEnabled(editor.selection.isEditable()); + }; + editor.on('NodeChange', nodeChanged); + nodeChanged(); + return () => { + editor.off('NodeChange', nodeChanged); + }; + }; + const register = (editor) => { + const onAction = () => editor.execCommand('mceNonBreaking'); + editor.ui.registry.addButton('nonbreaking', { + icon: 'non-breaking', + tooltip: 'Nonbreaking space', + onAction, + onSetup: onSetupEditable(editor) + }); + editor.ui.registry.addMenuItem('nonbreaking', { + icon: 'non-breaking', + text: 'Nonbreaking space', + onAction, + onSetup: onSetupEditable(editor) + }); + }; + + /** + * This class contains all core logic for the nonbreaking plugin. + * + * @class tinymce.nonbreaking.Plugin + * @private + */ + var Plugin = () => { + global$1.add('nonbreaking', (editor) => { + register$2(editor); + register$1(editor); + register(editor); + setup(editor); + }); + }; + + Plugin(); + /** ***** + * DO NOT EXPORT ANYTHING + * + * IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE + *******/ + +})(); diff --git a/public/assets/js/tinymce/plugins/nonbreaking/plugin.min.js b/public/assets/js/tinymce/plugins/nonbreaking/plugin.min.js new file mode 100644 index 0000000..c33bce1 --- /dev/null +++ b/public/assets/js/tinymce/plugins/nonbreaking/plugin.min.js @@ -0,0 +1 @@ +!function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=n=>e=>typeof e===n,o=e("boolean"),a=e("number"),t=n=>e=>e.options.get(n),i=t("nonbreaking_force_tab"),s=t("nonbreaking_wrap"),r=(n,e)=>{let o="";for(let a=0;a{const o=s(n)||n.plugins.visualchars?`${r(" ",e)}`:r(" ",e);n.undoManager.transact((()=>n.insertContent(o)))};var l=tinymce.util.Tools.resolve("tinymce.util.VK");const u=n=>e=>{const o=()=>{e.setEnabled(n.selection.isEditable())};return n.on("NodeChange",o),o(),()=>{n.off("NodeChange",o)}};n.add("nonbreaking",(n=>{(n=>{const e=n.options.register;e("nonbreaking_force_tab",{processor:n=>o(n)?{value:n?3:0,valid:!0}:a(n)?{value:n,valid:!0}:{valid:!1,message:"Must be a boolean or number."},default:!1}),e("nonbreaking_wrap",{processor:"boolean",default:!0})})(n),(n=>{n.addCommand("mceNonBreaking",(()=>{c(n,1)}))})(n),(n=>{const e=()=>n.execCommand("mceNonBreaking");n.ui.registry.addButton("nonbreaking",{icon:"non-breaking",tooltip:"Nonbreaking space",onAction:e,onSetup:u(n)}),n.ui.registry.addMenuItem("nonbreaking",{icon:"non-breaking",text:"Nonbreaking space",onAction:e,onSetup:u(n)})})(n),(n=>{const e=i(n);e>0&&n.on("keydown",(o=>{if(o.keyCode===l.TAB&&!o.isDefaultPrevented()){if(o.shiftKey)return;o.preventDefault(),o.stopImmediatePropagation(),c(n,e)}}))})(n)}))}(); \ No newline at end of file diff --git a/public/assets/js/tinymce/plugins/pagebreak/index.js b/public/assets/js/tinymce/plugins/pagebreak/index.js new file mode 100644 index 0000000..0ff6162 --- /dev/null +++ b/public/assets/js/tinymce/plugins/pagebreak/index.js @@ -0,0 +1,7 @@ +// Exports the "pagebreak" plugin for usage with module loaders +// Usage: +// CommonJS: +// require('tinymce/plugins/pagebreak') +// ES2015: +// import 'tinymce/plugins/pagebreak' +require('./plugin.js'); \ No newline at end of file diff --git a/public/assets/js/tinymce/plugins/pagebreak/plugin.js b/public/assets/js/tinymce/plugins/pagebreak/plugin.js new file mode 100644 index 0000000..cf28deb --- /dev/null +++ b/public/assets/js/tinymce/plugins/pagebreak/plugin.js @@ -0,0 +1,123 @@ +/** + * TinyMCE version 8.3.1 (2025-12-17) + */ + +(function () { + 'use strict'; + + var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager'); + + var global = tinymce.util.Tools.resolve('tinymce.Env'); + + const option = (name) => (editor) => editor.options.get(name); + const register$2 = (editor) => { + const registerOption = editor.options.register; + registerOption('pagebreak_separator', { + processor: 'string', + default: '' + }); + registerOption('pagebreak_split_block', { + processor: 'boolean', + default: false + }); + }; + const getSeparatorHtml = option('pagebreak_separator'); + const shouldSplitBlock = option('pagebreak_split_block'); + + const pageBreakClass = 'mce-pagebreak'; + const getPlaceholderHtml = (shouldSplitBlock) => { + const html = ``; + return shouldSplitBlock ? `

${html}

` : html; + }; + const setup$1 = (editor) => { + const separatorHtml = getSeparatorHtml(editor); + const shouldSplitBlock$1 = () => shouldSplitBlock(editor); + const pageBreakSeparatorRegExp = new RegExp(separatorHtml.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g, (a) => { + return '\\' + a; + }), 'gi'); + editor.on('BeforeSetContent', (e) => { + e.content = e.content.replace(pageBreakSeparatorRegExp, getPlaceholderHtml(shouldSplitBlock$1())); + }); + editor.on('PreInit', () => { + editor.serializer.addNodeFilter('img', (nodes) => { + let i = nodes.length, node, className; + while (i--) { + node = nodes[i]; + className = node.attr('class'); + if (className && className.indexOf(pageBreakClass) !== -1) { + // Replace parent block node if pagebreak_split_block is enabled + const parentNode = node.parent; + if (parentNode && editor.schema.getBlockElements()[parentNode.name] && shouldSplitBlock$1()) { + parentNode.type = 3; + parentNode.value = separatorHtml; + parentNode.raw = true; + node.remove(); + continue; + } + node.type = 3; + node.value = separatorHtml; + node.raw = true; + } + } + }); + }); + }; + + const register$1 = (editor) => { + editor.addCommand('mcePageBreak', () => { + editor.insertContent(getPlaceholderHtml(shouldSplitBlock(editor))); + }); + }; + + const setup = (editor) => { + editor.on('ResolveName', (e) => { + if (e.target.nodeName === 'IMG' && editor.dom.hasClass(e.target, pageBreakClass)) { + e.name = 'pagebreak'; + } + }); + }; + + const onSetupEditable = (editor) => (api) => { + const nodeChanged = () => { + api.setEnabled(editor.selection.isEditable()); + }; + editor.on('NodeChange', nodeChanged); + nodeChanged(); + return () => { + editor.off('NodeChange', nodeChanged); + }; + }; + const register = (editor) => { + const onAction = () => editor.execCommand('mcePageBreak'); + editor.ui.registry.addButton('pagebreak', { + icon: 'page-break', + tooltip: 'Page break', + onAction, + onSetup: onSetupEditable(editor) + }); + editor.ui.registry.addMenuItem('pagebreak', { + text: 'Page break', + icon: 'page-break', + onAction, + onSetup: onSetupEditable(editor) + }); + }; + + var Plugin = () => { + global$1.add('pagebreak', (editor) => { + register$2(editor); + register$1(editor); + register(editor); + setup$1(editor); + setup(editor); + }); + }; + + Plugin(); + /** ***** + * DO NOT EXPORT ANYTHING + * + * IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE + *******/ + +})(); diff --git a/public/assets/js/tinymce/plugins/pagebreak/plugin.min.js b/public/assets/js/tinymce/plugins/pagebreak/plugin.min.js new file mode 100644 index 0000000..8cf44f2 --- /dev/null +++ b/public/assets/js/tinymce/plugins/pagebreak/plugin.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env");const t=e=>a=>a.options.get(e),n=t("pagebreak_separator"),o=t("pagebreak_split_block"),r="mce-pagebreak",s=e=>{const t=``;return e?`

${t}

`:t},c=e=>a=>{const t=()=>{a.setEnabled(e.selection.isEditable())};return e.on("NodeChange",t),t(),()=>{e.off("NodeChange",t)}};e.add("pagebreak",(e=>{(e=>{const a=e.options.register;a("pagebreak_separator",{processor:"string",default:"\x3c!-- pagebreak --\x3e"}),a("pagebreak_split_block",{processor:"boolean",default:!1})})(e),(e=>{e.addCommand("mcePageBreak",(()=>{e.insertContent(s(o(e)))}))})(e),(e=>{const a=()=>e.execCommand("mcePageBreak");e.ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:a,onSetup:c(e)}),e.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:a,onSetup:c(e)})})(e),(e=>{const a=n(e),t=()=>o(e),c=new RegExp(a.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,(e=>"\\"+e)),"gi");e.on("BeforeSetContent",(e=>{e.content=e.content.replace(c,s(t()))})),e.on("PreInit",(()=>{e.serializer.addNodeFilter("img",(n=>{let o,s,c=n.length;for(;c--;)if(o=n[c],s=o.attr("class"),s&&-1!==s.indexOf(r)){const n=o.parent;if(n&&e.schema.getBlockElements()[n.name]&&t()){n.type=3,n.value=a,n.raw=!0,o.remove();continue}o.type=3,o.value=a,o.raw=!0}}))}))})(e),(e=>{e.on("ResolveName",(a=>{"IMG"===a.target.nodeName&&e.dom.hasClass(a.target,r)&&(a.name="pagebreak")}))})(e)}))}(); \ No newline at end of file diff --git a/public/assets/js/tinymce/plugins/preview/index.js b/public/assets/js/tinymce/plugins/preview/index.js new file mode 100644 index 0000000..4e8e817 --- /dev/null +++ b/public/assets/js/tinymce/plugins/preview/index.js @@ -0,0 +1,7 @@ +// Exports the "preview" plugin for usage with module loaders +// Usage: +// CommonJS: +// require('tinymce/plugins/preview') +// ES2015: +// import 'tinymce/plugins/preview' +require('./plugin.js'); \ No newline at end of file diff --git a/public/assets/js/tinymce/plugins/preview/plugin.js b/public/assets/js/tinymce/plugins/preview/plugin.js new file mode 100644 index 0000000..82e4294 --- /dev/null +++ b/public/assets/js/tinymce/plugins/preview/plugin.js @@ -0,0 +1,843 @@ +/** + * TinyMCE version 8.3.1 (2025-12-17) + */ + +(function () { + 'use strict'; + + var global$2 = tinymce.util.Tools.resolve('tinymce.PluginManager'); + + /* eslint-disable @typescript-eslint/no-wrapper-object-types */ + const isSimpleType = (type) => (value) => typeof value === type; + const eq = (t) => (a) => t === a; + const isUndefined = eq(undefined); + const isNullable = (a) => a === null || a === undefined; + const isNonNullable = (a) => !isNullable(a); + const isFunction = isSimpleType('function'); + + const constant = (value) => { + return () => { + return value; + }; + }; + const identity = (x) => { + return x; + }; + const never = constant(false); + + /** + * The `Optional` type represents a value (of any type) that potentially does + * not exist. Any `Optional` can either be a `Some` (in which case the + * value does exist) or a `None` (in which case the value does not exist). This + * module defines a whole lot of FP-inspired utility functions for dealing with + * `Optional` objects. + * + * Comparison with null or undefined: + * - We don't get fancy null coalescing operators with `Optional` + * - We do get fancy helper functions with `Optional` + * - `Optional` support nesting, and allow for the type to still be nullable (or + * another `Optional`) + * - There is no option to turn off strict-optional-checks like there is for + * strict-null-checks + */ + class Optional { + tag; + value; + // Sneaky optimisation: every instance of Optional.none is identical, so just + // reuse the same object + static singletonNone = new Optional(false); + // The internal representation has a `tag` and a `value`, but both are + // private: able to be console.logged, but not able to be accessed by code + constructor(tag, value) { + this.tag = tag; + this.value = value; + } + // --- Identities --- + /** + * Creates a new `Optional` that **does** contain a value. + */ + static some(value) { + return new Optional(true, value); + } + /** + * Create a new `Optional` that **does not** contain a value. `T` can be + * any type because we don't actually have a `T`. + */ + static none() { + return Optional.singletonNone; + } + /** + * Perform a transform on an `Optional` type. Regardless of whether this + * `Optional` contains a value or not, `fold` will return a value of type `U`. + * If this `Optional` does not contain a value, the `U` will be created by + * calling `onNone`. If this `Optional` does contain a value, the `U` will be + * created by calling `onSome`. + * + * For the FP enthusiasts in the room, this function: + * 1. Could be used to implement all of the functions below + * 2. Forms a catamorphism + */ + fold(onNone, onSome) { + if (this.tag) { + return onSome(this.value); + } + else { + return onNone(); + } + } + /** + * Determine if this `Optional` object contains a value. + */ + isSome() { + return this.tag; + } + /** + * Determine if this `Optional` object **does not** contain a value. + */ + isNone() { + return !this.tag; + } + // --- Functor (name stolen from Haskell / maths) --- + /** + * Perform a transform on an `Optional` object, **if** there is a value. If + * you provide a function to turn a T into a U, this is the function you use + * to turn an `Optional` into an `Optional`. If this **does** contain + * a value then the output will also contain a value (that value being the + * output of `mapper(this.value)`), and if this **does not** contain a value + * then neither will the output. + */ + map(mapper) { + if (this.tag) { + return Optional.some(mapper(this.value)); + } + else { + return Optional.none(); + } + } + // --- Monad (name stolen from Haskell / maths) --- + /** + * Perform a transform on an `Optional` object, **if** there is a value. + * Unlike `map`, here the transform itself also returns an `Optional`. + */ + bind(binder) { + if (this.tag) { + return binder(this.value); + } + else { + return Optional.none(); + } + } + // --- Traversable (name stolen from Haskell / maths) --- + /** + * For a given predicate, this function finds out if there **exists** a value + * inside this `Optional` object that meets the predicate. In practice, this + * means that for `Optional`s that do not contain a value it returns false (as + * no predicate-meeting value exists). + */ + exists(predicate) { + return this.tag && predicate(this.value); + } + /** + * For a given predicate, this function finds out if **all** the values inside + * this `Optional` object meet the predicate. In practice, this means that + * for `Optional`s that do not contain a value it returns true (as all 0 + * objects do meet the predicate). + */ + forall(predicate) { + return !this.tag || predicate(this.value); + } + filter(predicate) { + if (!this.tag || predicate(this.value)) { + return this; + } + else { + return Optional.none(); + } + } + // --- Getters --- + /** + * Get the value out of the inside of the `Optional` object, using a default + * `replacement` value if the provided `Optional` object does not contain a + * value. + */ + getOr(replacement) { + return this.tag ? this.value : replacement; + } + /** + * Get the value out of the inside of the `Optional` object, using a default + * `replacement` value if the provided `Optional` object does not contain a + * value. Unlike `getOr`, in this method the `replacement` object is also + * `Optional` - meaning that this method will always return an `Optional`. + */ + or(replacement) { + return this.tag ? this : replacement; + } + /** + * Get the value out of the inside of the `Optional` object, using a default + * `replacement` value if the provided `Optional` object does not contain a + * value. Unlike `getOr`, in this method the `replacement` value is + * "thunked" - that is to say that you don't pass a value to `getOrThunk`, you + * pass a function which (if called) will **return** the `value` you want to + * use. + */ + getOrThunk(thunk) { + return this.tag ? this.value : thunk(); + } + /** + * Get the value out of the inside of the `Optional` object, using a default + * `replacement` value if the provided Optional object does not contain a + * value. + * + * Unlike `or`, in this method the `replacement` value is "thunked" - that is + * to say that you don't pass a value to `orThunk`, you pass a function which + * (if called) will **return** the `value` you want to use. + * + * Unlike `getOrThunk`, in this method the `replacement` value is also + * `Optional`, meaning that this method will always return an `Optional`. + */ + orThunk(thunk) { + return this.tag ? this : thunk(); + } + /** + * Get the value out of the inside of the `Optional` object, throwing an + * exception if the provided `Optional` object does not contain a value. + * + * WARNING: + * You should only be using this function if you know that the `Optional` + * object **is not** empty (otherwise you're throwing exceptions in production + * code, which is bad). + * + * In tests this is more acceptable. + * + * Prefer other methods to this, such as `.each`. + */ + getOrDie(message) { + if (!this.tag) { + throw new Error(message ?? 'Called getOrDie on None'); + } + else { + return this.value; + } + } + // --- Interop with null and undefined --- + /** + * Creates an `Optional` value from a nullable (or undefined-able) input. + * Null, or undefined, is converted to `None`, and anything else is converted + * to `Some`. + */ + static from(value) { + return isNonNullable(value) ? Optional.some(value) : Optional.none(); + } + /** + * Converts an `Optional` to a nullable type, by getting the value if it + * exists, or returning `null` if it does not. + */ + getOrNull() { + return this.tag ? this.value : null; + } + /** + * Converts an `Optional` to an undefined-able type, by getting the value if + * it exists, or returning `undefined` if it does not. + */ + getOrUndefined() { + return this.value; + } + // --- Utilities --- + /** + * If the `Optional` contains a value, perform an action on that value. + * Unlike the rest of the methods on this type, `.each` has side-effects. If + * you want to transform an `Optional` **into** something, then this is not + * the method for you. If you want to use an `Optional` to **do** + * something, then this is the method for you - provided you're okay with not + * doing anything in the case where the `Optional` doesn't have a value inside + * it. If you're not sure whether your use-case fits into transforming + * **into** something or **doing** something, check whether it has a return + * value. If it does, you should be performing a transform. + */ + each(worker) { + if (this.tag) { + worker(this.value); + } + } + /** + * Turn the `Optional` object into an array that contains all of the values + * stored inside the `Optional`. In practice, this means the output will have + * either 0 or 1 elements. + */ + toArray() { + return this.tag ? [this.value] : []; + } + /** + * Turn the `Optional` object into a string for debugging or printing. Not + * recommended for production code, but good for debugging. Also note that + * these days an `Optional` object can be logged to the console directly, and + * its inner value (if it exists) will be visible. + */ + toString() { + return this.tag ? `some(${this.value})` : 'none()'; + } + } + + const nativeSlice = Array.prototype.slice; + const nativeIndexOf = Array.prototype.indexOf; + const rawIndexOf = (ts, t) => nativeIndexOf.call(ts, t); + const contains$1 = (xs, x) => rawIndexOf(xs, x) > -1; + const exists = (xs, pred) => { + for (let i = 0, len = xs.length; i < len; i++) { + const x = xs[i]; + if (pred(x, i)) { + return true; + } + } + return false; + }; + const map = (xs, f) => { + // pre-allocating array size when it's guaranteed to be known + // http://jsperf.com/push-allocated-vs-dynamic/22 + const len = xs.length; + const r = new Array(len); + for (let i = 0; i < len; i++) { + const x = xs[i]; + r[i] = f(x, i); + } + return r; + }; + const findUntil = (xs, pred, until) => { + for (let i = 0, len = xs.length; i < len; i++) { + const x = xs[i]; + if (pred(x, i)) { + return Optional.some(x); + } + else if (until(x, i)) { + break; + } + } + return Optional.none(); + }; + const find$1 = (xs, pred) => { + return findUntil(xs, pred, never); + }; + isFunction(Array.from) ? Array.from : (x) => nativeSlice.call(x); + const findMap = (arr, f) => { + for (let i = 0; i < arr.length; i++) { + const r = f(arr[i], i); + if (r.isSome()) { + return r; + } + } + return Optional.none(); + }; + const unique = (xs, comparator) => { + const r = []; + const isDuplicated = isFunction(comparator) ? + (x) => exists(r, (i) => comparator(i, x)) : + (x) => contains$1(r, x); + for (let i = 0, len = xs.length; i < len; i++) { + const x = xs[i]; + if (!isDuplicated(x)) { + r.push(x); + } + } + return r; + }; + + // There are many variations of Object iteration that are faster than the 'for-in' style: + // http://jsperf.com/object-keys-iteration/107 + // + // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering + const keys = Object.keys; + const each = (obj, f) => { + const props = keys(obj); + for (let k = 0, len = props.length; k < len; k++) { + const i = props[k]; + const x = obj[i]; + f(x, i); + } + }; + const mapToArray = (obj, f) => { + const r = []; + each(obj, (value, name) => { + r.push(f(value, name)); + }); + return r; + }; + const values = (obj) => { + return mapToArray(obj, identity); + }; + + const contains = (str, substr, start = 0, end) => { + const idx = str.indexOf(substr, start); + if (idx !== -1) { + return isUndefined(end) ? true : idx + substr.length <= end; + } + else { + return false; + } + }; + + const cached = (f) => { + let called = false; + let r; + return (...args) => { + if (!called) { + called = true; + r = f.apply(null, args); + } + return r; + }; + }; + + const DeviceType = (os, browser, userAgent, mediaMatch) => { + const isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; + const isiPhone = os.isiOS() && !isiPad; + const isMobile = os.isiOS() || os.isAndroid(); + const isTouch = isMobile || mediaMatch('(pointer:coarse)'); + const isTablet = isiPad || !isiPhone && isMobile && mediaMatch('(min-device-width:768px)'); + const isPhone = isiPhone || isMobile && !isTablet; + const iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; + const isDesktop = !isPhone && !isTablet && !iOSwebview; + return { + isiPad: constant(isiPad), + isiPhone: constant(isiPhone), + isTablet: constant(isTablet), + isPhone: constant(isPhone), + isTouch: constant(isTouch), + isAndroid: os.isAndroid, + isiOS: os.isiOS, + isWebView: constant(iOSwebview), + isDesktop: constant(isDesktop) + }; + }; + + const firstMatch = (regexes, s) => { + for (let i = 0; i < regexes.length; i++) { + const x = regexes[i]; + if (x.test(s)) { + return x; + } + } + return undefined; + }; + const find = (regexes, agent) => { + const r = firstMatch(regexes, agent); + if (!r) { + return { major: 0, minor: 0 }; + } + const group = (i) => { + return Number(agent.replace(r, '$' + i)); + }; + return nu$2(group(1), group(2)); + }; + const detect$3 = (versionRegexes, agent) => { + const cleanedAgent = String(agent).toLowerCase(); + if (versionRegexes.length === 0) { + return unknown$2(); + } + return find(versionRegexes, cleanedAgent); + }; + const unknown$2 = () => { + return nu$2(0, 0); + }; + const nu$2 = (major, minor) => { + return { major, minor }; + }; + const Version = { + nu: nu$2, + detect: detect$3, + unknown: unknown$2 + }; + + const detectBrowser$1 = (browsers, userAgentData) => { + return findMap(userAgentData.brands, (uaBrand) => { + const lcBrand = uaBrand.brand.toLowerCase(); + return find$1(browsers, (browser) => lcBrand === browser.brand?.toLowerCase()) + .map((info) => ({ + current: info.name, + version: Version.nu(parseInt(uaBrand.version, 10), 0) + })); + }); + }; + + const detect$2 = (candidates, userAgent) => { + const agent = String(userAgent).toLowerCase(); + return find$1(candidates, (candidate) => { + return candidate.search(agent); + }); + }; + // They (browser and os) are the same at the moment, but they might + // not stay that way. + const detectBrowser = (browsers, userAgent) => { + return detect$2(browsers, userAgent).map((browser) => { + const version = Version.detect(browser.versionRegexes, userAgent); + return { + current: browser.name, + version + }; + }); + }; + const detectOs = (oses, userAgent) => { + return detect$2(oses, userAgent).map((os) => { + const version = Version.detect(os.versionRegexes, userAgent); + return { + current: os.name, + version + }; + }); + }; + + const normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; + const checkContains = (target) => { + return (uastring) => { + return contains(uastring, target); + }; + }; + const browsers = [ + // This is legacy Edge + { + name: 'Edge', + versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], + search: (uastring) => { + return contains(uastring, 'edge/') && contains(uastring, 'chrome') && contains(uastring, 'safari') && contains(uastring, 'applewebkit'); + } + }, + // This is Google Chrome and Chromium Edge + { + name: 'Chromium', + brand: 'Chromium', + versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], + search: (uastring) => { + return contains(uastring, 'chrome') && !contains(uastring, 'chromeframe'); + } + }, + { + name: 'IE', + versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], + search: (uastring) => { + return contains(uastring, 'msie') || contains(uastring, 'trident'); + } + }, + // INVESTIGATE: Is this still the Opera user agent? + { + name: 'Opera', + versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], + search: checkContains('opera') + }, + { + name: 'Firefox', + versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], + search: checkContains('firefox') + }, + { + name: 'Safari', + versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], + search: (uastring) => { + return (contains(uastring, 'safari') || contains(uastring, 'mobile/')) && contains(uastring, 'applewebkit'); + } + } + ]; + const oses = [ + { + name: 'Windows', + search: checkContains('win'), + versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] + }, + { + name: 'iOS', + search: (uastring) => { + return contains(uastring, 'iphone') || contains(uastring, 'ipad'); + }, + versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] + }, + { + name: 'Android', + search: checkContains('android'), + versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] + }, + { + name: 'macOS', + search: checkContains('mac os x'), + versionRegexes: [/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/] + }, + { + name: 'Linux', + search: checkContains('linux'), + versionRegexes: [] + }, + { name: 'Solaris', + search: checkContains('sunos'), + versionRegexes: [] + }, + { + name: 'FreeBSD', + search: checkContains('freebsd'), + versionRegexes: [] + }, + { + name: 'ChromeOS', + search: checkContains('cros'), + versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/] + } + ]; + const PlatformInfo = { + browsers: constant(browsers), + oses: constant(oses) + }; + + const edge = 'Edge'; + const chromium = 'Chromium'; + const ie = 'IE'; + const opera = 'Opera'; + const firefox = 'Firefox'; + const safari = 'Safari'; + const unknown$1 = () => { + return nu$1({ + current: undefined, + version: Version.unknown() + }); + }; + const nu$1 = (info) => { + const current = info.current; + const version = info.version; + const isBrowser = (name) => () => current === name; + return { + current, + version, + isEdge: isBrowser(edge), + isChromium: isBrowser(chromium), + // NOTE: isIe just looks too weird + isIE: isBrowser(ie), + isOpera: isBrowser(opera), + isFirefox: isBrowser(firefox), + isSafari: isBrowser(safari) + }; + }; + const Browser = { + unknown: unknown$1, + nu: nu$1, + edge: constant(edge), + chromium: constant(chromium), + ie: constant(ie), + opera: constant(opera), + firefox: constant(firefox), + safari: constant(safari) + }; + + const windows = 'Windows'; + const ios = 'iOS'; + const android = 'Android'; + const linux = 'Linux'; + const macos = 'macOS'; + const solaris = 'Solaris'; + const freebsd = 'FreeBSD'; + const chromeos = 'ChromeOS'; + // Though there is a bit of dupe with this and Browser, trying to + // reuse code makes it much harder to follow and change. + const unknown = () => { + return nu({ + current: undefined, + version: Version.unknown() + }); + }; + const nu = (info) => { + const current = info.current; + const version = info.version; + const isOS = (name) => () => current === name; + return { + current, + version, + isWindows: isOS(windows), + // TODO: Fix capitalisation + isiOS: isOS(ios), + isAndroid: isOS(android), + isMacOS: isOS(macos), + isLinux: isOS(linux), + isSolaris: isOS(solaris), + isFreeBSD: isOS(freebsd), + isChromeOS: isOS(chromeos) + }; + }; + const OperatingSystem = { + unknown, + nu, + windows: constant(windows), + ios: constant(ios), + android: constant(android), + linux: constant(linux), + macos: constant(macos), + solaris: constant(solaris), + freebsd: constant(freebsd), + chromeos: constant(chromeos) + }; + + const detect$1 = (userAgent, userAgentDataOpt, mediaMatch) => { + const browsers = PlatformInfo.browsers(); + const oses = PlatformInfo.oses(); + const browser = userAgentDataOpt.bind((userAgentData) => detectBrowser$1(browsers, userAgentData)) + .orThunk(() => detectBrowser(browsers, userAgent)) + .fold(Browser.unknown, Browser.nu); + const os = detectOs(oses, userAgent).fold(OperatingSystem.unknown, OperatingSystem.nu); + const deviceType = DeviceType(os, browser, userAgent, mediaMatch); + return { + browser, + os, + deviceType + }; + }; + const PlatformDetection = { + detect: detect$1 + }; + + const mediaMatch = (query) => window.matchMedia(query).matches; + // IMPORTANT: Must be in a thunk, otherwise rollup thinks calling this immediately + // causes side effects and won't tree shake this away + // Note: navigator.userAgentData is not part of the native typescript types yet + let platform = cached(() => PlatformDetection.detect(window.navigator.userAgent, Optional.from((window.navigator.userAgentData)), mediaMatch)); + const detect = () => platform(); + + const isMacOS = () => detect().os.isMacOS(); + const isiOS = () => detect().os.isiOS(); + + const getPreventClicksOnLinksScript = () => { + const isMacOSOrIOS = isMacOS() || isiOS(); + const fn = (isMacOSOrIOS) => { + document.addEventListener('click', (e) => { + for (let elm = e.target; elm; elm = elm.parentNode) { + if (elm.nodeName === 'A') { + const anchor = elm; + const href = anchor.getAttribute('href'); + if (href && href.startsWith('#')) { + e.preventDefault(); + const targetElement = document.getElementById(href.substring(1)); + if (targetElement) { + targetElement.scrollIntoView({ behavior: 'smooth' }); + } + return; + } + const isMetaKeyPressed = isMacOSOrIOS ? e.metaKey : e.ctrlKey && !e.altKey; + if (!isMetaKeyPressed) { + e.preventDefault(); + } + } + } + }, false); + }; + return ``; + }; + + var global$1 = tinymce.util.Tools.resolve('tinymce.dom.ScriptLoader'); + + var global = tinymce.util.Tools.resolve('tinymce.util.Tools'); + + const option = (name) => (editor) => editor.options.get(name); + const getContentStyle = option('content_style'); + const shouldUseContentCssCors = option('content_css_cors'); + const getBodyClass = option('body_class'); + const getBodyId = option('body_id'); + + const getComponentScriptsHtml = (editor) => { + const urls = unique(values(editor.schema.getComponentUrls())); + return map(urls, (url) => { + const attrs = mapToArray(global$1.ScriptLoader.getScriptAttributes(url), (v, k) => ` ${editor.dom.encode(k)}="${editor.dom.encode(v)}"`); + return ``; + }).join(''); + }; + const getPreviewHtml = (editor) => { + let headHtml = ''; + const encode = editor.dom.encode; + const contentStyle = getContentStyle(editor) ?? ''; + headHtml += ``; + const cors = shouldUseContentCssCors(editor) ? ' crossorigin="anonymous"' : ''; + global.each(editor.contentCSS, (url) => { + headHtml += ''; + }); + if (contentStyle) { + headHtml += ''; + } + headHtml += getComponentScriptsHtml(editor); + const bodyId = getBodyId(editor); + const bodyClass = getBodyClass(editor); + const directionality = editor.getBody().dir; + const dirAttr = directionality ? ' dir="' + encode(directionality) + '"' : ''; + const previewHtml = ('' + + '' + + '' + + headHtml + + '' + + '' + + editor.getContent() + + getPreventClicksOnLinksScript() + + '' + + ''); + return previewHtml; + }; + + const open = (editor) => { + const content = getPreviewHtml(editor); + const dataApi = editor.windowManager.open({ + title: 'Preview', + size: 'large', + body: { + type: 'panel', + items: [ + { + name: 'preview', + type: 'iframe', + sandboxed: true, + transparent: false + } + ] + }, + buttons: [ + { + type: 'cancel', + name: 'close', + text: 'Close', + primary: true + } + ], + initialData: { + preview: content + } + }); + // Focus the close button, as by default the first element in the body is selected + // which we don't want to happen here since the body only has the iframe content + dataApi.focus('close'); + }; + + const register$1 = (editor) => { + editor.addCommand('mcePreview', () => { + open(editor); + }); + }; + + const register = (editor) => { + const onAction = () => editor.execCommand('mcePreview'); + editor.ui.registry.addButton('preview', { + icon: 'preview', + tooltip: 'Preview', + onAction, + context: 'any' + }); + editor.ui.registry.addMenuItem('preview', { + icon: 'preview', + text: 'Preview', + onAction, + context: 'any' + }); + }; + + var Plugin = () => { + global$2.add('preview', (editor) => { + register$1(editor); + register(editor); + }); + }; + + Plugin(); + /** ***** + * DO NOT EXPORT ANYTHING + * + * IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE + *******/ + +})(); diff --git a/public/assets/js/tinymce/plugins/preview/plugin.min.js b/public/assets/js/tinymce/plugins/preview/plugin.min.js new file mode 100644 index 0000000..a9d8d88 --- /dev/null +++ b/public/assets/js/tinymce/plugins/preview/plugin.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>undefined===e;const r=e=>"function"==typeof e;const n=e=>()=>e,s=e=>e,o=n(!1);class i{tag;value;static singletonNone=new i(!1);constructor(e,t){this.tag=e,this.value=t}static some(e){return new i(!0,e)}static none(){return i.singletonNone}fold(e,t){return this.tag?t(this.value):e()}isSome(){return this.tag}isNone(){return!this.tag}map(e){return this.tag?i.some(e(this.value)):i.none()}bind(e){return this.tag?e(this.value):i.none()}exists(e){return this.tag&&e(this.value)}forall(e){return!this.tag||e(this.value)}filter(e){return!this.tag||e(this.value)?this:i.none()}getOr(e){return this.tag?this.value:e}or(e){return this.tag?this:e}getOrThunk(e){return this.tag?this.value:e()}orThunk(e){return this.tag?this:e()}getOrDie(e){if(this.tag)return this.value;throw new Error(e??"Called getOrDie on None")}static from(e){return null==e?i.none():i.some(e)}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(e){this.tag&&e(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}Array.prototype.slice;const a=Array.prototype.indexOf,c=(e,t)=>((e,t,r)=>{for(let n=0,s=e.length;n{const r=[];return((e,t)=>{const r=u(e);for(let n=0,s=r.length;n{r.push(t(e,n))})),r},d=(e,r,n=0,s)=>{const o=e.indexOf(r,n);return-1!==o&&(!!t(s)||o+r.length<=s)},m=()=>h(0,0),h=(e,t)=>({major:e,minor:t}),g={nu:h,detect:(e,t)=>{const r=String(t).toLowerCase();return 0===e.length?m():((e,t)=>{const r=((e,t)=>{for(let r=0;rNumber(t.replace(r,"$"+e));return h(n(1),n(2))})(e,r)},unknown:m},v=(e,t)=>{const r=String(t).toLowerCase();return c(e,(e=>e.search(r)))},p=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,f=e=>t=>d(t,e),w=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:e=>d(e,"edge/")&&d(e,"chrome")&&d(e,"safari")&&d(e,"applewebkit")},{name:"Chromium",brand:"Chromium",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,p],search:e=>d(e,"chrome")&&!d(e,"chromeframe")},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:e=>d(e,"msie")||d(e,"trident")},{name:"Opera",versionRegexes:[p,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:f("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:f("firefox")},{name:"Safari",versionRegexes:[p,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:e=>(d(e,"safari")||d(e,"mobile/"))&&d(e,"applewebkit")}],y=[{name:"Windows",search:f("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:e=>d(e,"iphone")||d(e,"ipad"),versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:f("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"macOS",search:f("mac os x"),versionRegexes:[/.*?mac\ os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:f("linux"),versionRegexes:[]},{name:"Solaris",search:f("sunos"),versionRegexes:[]},{name:"FreeBSD",search:f("freebsd"),versionRegexes:[]},{name:"ChromeOS",search:f("cros"),versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/]}],x={browsers:n(w),oses:n(y)},S="Edge",b="Chromium",O="Opera",A="Firefox",C="Safari",R=e=>{const t=e.current,r=e.version,n=e=>()=>t===e;return{current:t,version:r,isEdge:n(S),isChromium:n(b),isIE:n("IE"),isOpera:n(O),isFirefox:n(A),isSafari:n(C)}},k=()=>R({current:void 0,version:g.unknown()}),D=R,E=(n(S),n(b),n("IE"),n(O),n(A),n(C),"Windows"),I="Android",T="Linux",L="macOS",P="Solaris",$="FreeBSD",_="ChromeOS",B=e=>{const t=e.current,r=e.version,n=e=>()=>t===e;return{current:t,version:r,isWindows:n(E),isiOS:n("iOS"),isAndroid:n(I),isMacOS:n(L),isLinux:n(T),isSolaris:n(P),isFreeBSD:n($),isChromeOS:n(_)}},N=()=>B({current:void 0,version:g.unknown()}),F=B,M=(n(E),n("iOS"),n(I),n(T),n(L),n(P),n($),n(_),(e,t,r)=>{const s=x.browsers(),o=x.oses(),a=t.bind((e=>((e,t)=>((e,t)=>{for(let r=0;r{const r=t.brand.toLowerCase();return c(e,(e=>r===e.brand?.toLowerCase())).map((e=>({current:e.name,version:g.nu(parseInt(t.version,10),0)})))})))(s,e))).orThunk((()=>((e,t)=>v(e,t).map((e=>{const r=g.detect(e.versionRegexes,t);return{current:e.name,version:r}})))(s,e))).fold(k,D),u=((e,t)=>v(e,t).map((e=>{const r=g.detect(e.versionRegexes,t);return{current:e.name,version:r}})))(o,e).fold(N,F),l=((e,t,r,s)=>{const o=e.isiOS()&&!0===/ipad/i.test(r),i=e.isiOS()&&!o,a=e.isiOS()||e.isAndroid(),c=a||s("(pointer:coarse)"),u=o||!i&&a&&s("(min-device-width:768px)"),l=i||a&&!u,d=t.isSafari()&&e.isiOS()&&!1===/safari/i.test(r),m=!l&&!u&&!d;return{isiPad:n(o),isiPhone:n(i),isTablet:n(u),isPhone:n(l),isTouch:n(c),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:n(d),isDesktop:n(m)}})(u,a,e,r);return{browser:a,os:u,deviceType:l}}),j=e=>window.matchMedia(e).matches;let U=(e=>{let t,r=!1;return(...n)=>(r||(r=!0,t=e.apply(null,n)),t)})((()=>M(window.navigator.userAgent,i.from(window.navigator.userAgentData),j)));const W=()=>U();var K=tinymce.util.Tools.resolve("tinymce.dom.ScriptLoader"),V=tinymce.util.Tools.resolve("tinymce.util.Tools");const z=e=>t=>t.options.get(e),Y=z("content_style"),q=z("content_css_cors"),G=z("body_class"),H=z("body_id"),J=e=>{var t;return((e,t)=>{const r=e.length,n=new Array(r);for(let s=0;s{const n=[],s=r(t)?e=>((e,t)=>{for(let r=0,n=e.length;rt(r,e))):e=>((e,t)=>((e,t)=>a.call(e,t))(e,t)>-1)(n,e);for(let t=0,r=e.length;t{const r=l(K.ScriptLoader.getScriptAttributes(t),((t,r)=>` ${e.dom.encode(r)}="${e.dom.encode(t)}"`));return`