UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

891 lines (774 loc) 31.4 kB
import { PluginModal } from '../../../interfaces'; import { Modal, Figure } from '../../../modules/contract'; import { FileManager } from '../../../modules/manager'; import { dom, numbers, env, converter } from '../../../helper'; const { _w, NO_EVENT } = env; import VideoSizeService from './services/video.size'; import VideoUploadService from './services/video.upload'; import { CreateHTML_modal } from './render/video.html'; /** * @typedef {Object} VideoPluginOptions * @property {boolean} [canResize=true] - Whether the video element can be resized. * @property {boolean} [showHeightInput=true] - Whether to display the height input field. * @property {string} [defaultWidth] - The default width of the video element. If a number is provided, `"px"` will be appended. * @property {string} [defaultHeight] - The default height of the video element. If a number is provided, `"px"` will be appended. * @property {boolean} [percentageOnlySize=false] - Whether to allow only percentage-based sizing. * @property {boolean} [createFileInput=false] - Whether to create a file input element for video uploads. * @property {boolean} [createUrlInput] - Whether to create a URL input element for video embedding. * - Defaults to `true`. Always `true` when `createFileInput` is `false`. * @property {string} [uploadUrl] - The URL endpoint for video file uploads. * - The server must return: * ```js * { * "result": [ * { * "url": "https://example.com/video.mp4", * "name": "video.mp4", * "size": 5242880 * } * ] * } * ``` * @property {Object<string, string>} [uploadHeaders] - Additional headers to include in the video upload request. * @property {number} [uploadSizeLimit] - The total upload size limit for videos in bytes. * @property {number} [uploadSingleSizeLimit] - The single file upload size limit for videos in bytes. * @property {boolean} [allowMultiple=false] - Whether multiple video uploads are allowed. * @property {string} [acceptedFormats="video/*"] - Accepted file formats for video uploads (`"video/*"`). * @property {number} [defaultRatio=0.5625] - The default aspect ratio for the video (height/width, e.g. 16:9 → `9/16 = 0.5625`). * @property {boolean} [showRatioOption=true] - Whether to display the ratio option in the modal. * @property {Array<{name: string, value: number}>} [ratioOptions] - Custom ratio options for video resizing (value = height/width). * ```js * // ratioOptions * [{ name: '16:9', value: 0.5625 }, { name: '4:3', value: 0.75 }] * ``` * @property {Object<string, string>} [videoTagAttributes] - Additional attributes to set on the `VIDEO` tag. * ```js * { videoTagAttributes: { controls: 'true', muted: 'true', playsinline: '' } } * ``` * @property {Object<string, string>} [iframeTagAttributes] - Additional attributes to set on the `IFRAME` tag. * ```js * { iframeTagAttributes: { allowfullscreen: 'true', loading: 'lazy' } } * ``` * @property {string} [query_youtube=""] - Additional query parameters for YouTube embedding. * ```js * { query_youtube: 'autoplay=1&mute=1' } * ``` * @property {string} [query_vimeo=""] - Additional query parameters for Vimeo embedding. * ```js * { query_vimeo: 'autoplay=1' } * ``` * @property {Object<string, {pattern: RegExp, action: (url: string) => string, tag: string}>} [embedQuery] - Custom embed service definitions (see `EmbedPluginOptions.embedQuery`). * @property {Array<RegExp>} [urlPatterns] - Additional URL patterns for video embedding. * @property {Array<string>} [extensions] - Additional file extensions to be recognized for video uploads. * @property {SunEditor.Module.Figure.Controls} [controls] - Figure controls. * @property {SunEditor.ComponentInsertType} [insertBehavior] - Component insertion behavior for selection and cursor placement. * - [default: `options.get('componentInsertBehavior')`] * - `auto`: Move cursor to the next line if possible, otherwise select the component. * - `select`: Always select the inserted component. * - `line`: Move cursor to the next line if possible, or create a new line and move there. * - `none`: Do nothing. */ /** * @typedef {Object} VideoState * @property {string} sizeUnit * @property {boolean} onlyPercentage * @property {string} defaultRatio */ /** * @class * @description Video plugin. * - This plugin provides video embedding functionality within the editor. * - It also supports embedding from popular video services */ class Video extends PluginModal { static key = 'video'; static className = ''; /** * @param {HTMLElement} node - The node to check. * @returns {HTMLElement|null} Returns a node if the node is a valid component. */ static component(node) { if (/^(VIDEO)$/i.test(node?.nodeName)) { return node; } else if (/^(IFRAME)$/i.test(node?.nodeName)) { return this.#checkContentType(/** @type {HTMLIFrameElement} */ (node).src) ? node : null; } return null; } /** * @description Checks if the given URL matches any of the defined URL patterns. * @param {string} url - The URL to check. * @returns {boolean} `true` if the URL matches a known pattern; otherwise, `false`. */ static #checkContentType(url) { url = url?.toLowerCase() || ''; if (this.#extensions.some((ext) => url.endsWith(ext)) || this.#urlPatterns.some((pattern) => pattern.test(url))) { return true; } return false; } static #extensions = ['.mp4', '.avi', '.mov', '.webm', '.flv', '.mkv', '.m4v', '.ogv']; static #urlPatterns = [ /youtu\.?be/, /vimeo\.com\//, /dailymotion\.com\/video\//, /facebook\.com\/.+\/videos\//, /facebook\.com\/watch\/\?v=/, /twitter\.com\/.+\/status\//, /twitch\.tv\/videos\//, /twitch\.tv\/[^/]+$/, /tiktok\.com\/@[^/]+\/video\//, /instagram\.com\/p\//, /instagram\.com\/tv\//, /instagram\.com\/reel\//, /linkedin\.com\/posts\//, /\.(wistia\.com|wi\.st)\/(medias|embed)\//, /loom\.com\/share\//, ]; #resizing; #nonResizing; #linkValue = ''; #align = 'none'; #element = null; #container = null; /** * @constructor * @param {SunEditor.Kernel} kernel - The Kernel instance * @param {VideoPluginOptions} pluginOptions */ constructor(kernel, pluginOptions) { // plugin basic properties super(kernel); this.title = this.$.lang.video; this.icon = 'video'; // define plugin options this.pluginOptions = { canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize, showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput, defaultWidth: !pluginOptions.defaultWidth || !numbers.get(pluginOptions.defaultWidth, 0) ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth, defaultHeight: !pluginOptions.defaultHeight || !numbers.get(pluginOptions.defaultHeight, 0) ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight, percentageOnlySize: !!pluginOptions.percentageOnlySize, createFileInput: !!pluginOptions.createFileInput, createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput, uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null, uploadHeaders: pluginOptions.uploadHeaders || null, uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0), uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0), allowMultiple: !!pluginOptions.allowMultiple, acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'video/*' : pluginOptions.acceptedFormats.trim() || 'video/*', defaultRatio: numbers.get(pluginOptions.defaultRatio, 4) || 0.5625, showRatioOption: pluginOptions.showRatioOption === undefined ? true : !!pluginOptions.showRatioOption, ratioOptions: !pluginOptions.ratioOptions ? null : pluginOptions.ratioOptions, videoTagAttributes: pluginOptions.videoTagAttributes || null, iframeTagAttributes: pluginOptions.iframeTagAttributes || null, query_youtube: pluginOptions.query_youtube || '', query_vimeo: pluginOptions.query_vimeo || '', insertBehavior: pluginOptions.insertBehavior, }; // create HTML const sizeUnit = this.pluginOptions.percentageOnlySize ? '%' : 'px'; const modalEl = CreateHTML_modal(this.$, this.pluginOptions); const figureControls = pluginOptions.controls || (!this.pluginOptions.canResize ? [['align', 'edit', 'copy', 'remove']] : [['resize_auto,75,50', 'align', 'edit', 'revert', 'copy', 'remove']]); // show align if (!figureControls.some((subArray) => subArray.includes('align'))) modalEl.alignForm.style.display = 'none'; // modules const defaultRatio = this.pluginOptions.defaultRatio * 100 + '%'; this.modal = new Modal(this, this.$, modalEl.html); this.figure = new Figure(this, this.$, figureControls, { sizeUnit: sizeUnit, autoRatio: { current: defaultRatio, default: defaultRatio } }); this.fileManager = new FileManager(this, this.$, { query: 'iframe, video', loadEventName: 'onVideoLoad', actionEventName: 'onVideoAction', }); // members this.fileModalWrapper = modalEl.fileModalWrapper; this.videoInputFile = modalEl.videoInputFile; this.videoUrlFile = modalEl.videoUrlFile; this.focusElement = this.videoUrlFile || this.videoInputFile; this.previewSrc = modalEl.previewSrc; this.#resizing = this.pluginOptions.canResize; this.#nonResizing = !this.#resizing || !this.pluginOptions.showHeightInput || this.pluginOptions.percentageOnlySize; this.query = { youtube: { pattern: /youtu\.?be/i, action: (url) => { url = this.convertUrlYoutube(url); return converter.addUrlQuery(url, this.pluginOptions.query_youtube); }, tag: 'iframe', }, vimeo: { pattern: /vimeo\.com/i, action: (url) => { url = this.convertUrlVimeo(url); return converter.addUrlQuery(url, this.pluginOptions.query_vimeo); }, tag: 'iframe', }, ...pluginOptions.embedQuery, }; const urlPatterns = []; for (const key in this.query) { urlPatterns.push(this.query[key].pattern); } Video.#extensions = Video.#extensions.concat(this.pluginOptions.extensions || []); Video.#urlPatterns = Video.#urlPatterns.concat(pluginOptions.urlPatterns || []); /** @type {VideoState} */ this.state = { onlyPercentage: this.pluginOptions.percentageOnlySize, sizeUnit: sizeUnit, defaultRatio: this.pluginOptions.defaultRatio * 100 + '%', }; this.sizeService = new VideoSizeService(this, modalEl); this.uploadService = new VideoUploadService(this); // init const galleryButton = modalEl.galleryButton; if (galleryButton) this.$.eventManager.addEvent(galleryButton, 'click', this.#OpenGallery.bind(this)); if (this.videoInputFile) this.$.eventManager.addEvent(modalEl.fileRemoveBtn, 'click', this.#RemoveSelectedFiles.bind(this)); if (this.videoUrlFile) this.$.eventManager.addEvent(this.videoUrlFile, 'input', this.#OnLinkPreview.bind(this)); if (this.videoInputFile && this.videoUrlFile) this.$.eventManager.addEvent(this.videoInputFile, 'change', this.#OnfileInputChange.bind(this)); } /** * @template {keyof VideoState} K * @param {K} key * @param {VideoState[K]} value */ setState(key, value) { this.state[key] = value; } /** * @override * @type {PluginModal['open']} */ open() { this.modal.open(); } /** * @hook Editor.Core * @type {SunEditor.Hook.Core.RetainFormat} */ retainFormat() { return { query: 'iframe, video', /** @param {HTMLIFrameElement|HTMLVideoElement} element */ method: async (element) => { if (/^(iframe)$/i.test(element?.nodeName)) { if (!Video.#checkContentType(element.src)) return; } const figureInfo = Figure.GetContainer(element); if (figureInfo && figureInfo.container && figureInfo.cover) return; this.#ready(element, true); const line = this.$.format.getLine(element); if (line) this.#align = line.style.textAlign || line.style.float; this.#fixTagStructure(element); }, }; } /** * @hook Editor.EventManager * @type {SunEditor.Hook.Event.OnFilePasteAndDrop} */ onFilePasteAndDrop({ file }) { if (!/^video/.test(file.type)) return; this.submitFile([file]); this.$.focusManager.focus(); } /** * @hook Modules.Modal * @type {SunEditor.Hook.Modal.On} */ modalOn(isUpdate) { if (!isUpdate) { if (this.videoInputFile && this.pluginOptions.allowMultiple) this.videoInputFile.setAttribute('multiple', 'multiple'); } else { if (this.videoInputFile && this.pluginOptions.allowMultiple) this.videoInputFile.removeAttribute('multiple'); } this.sizeService.on(isUpdate); } /** * @hook Modules.Modal * @type {SunEditor.Hook.Modal.Action} */ async modalAction() { this.#align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_video_radio"]:checked')).value; let result = false; if (this.videoInputFile && this.videoInputFile.files.length > 0) { result = await this.submitFile(this.videoInputFile.files); } else if (this.videoUrlFile && this.#linkValue.length > 0) { result = await this.submitURL(this.#linkValue); } if (result) _w.setTimeout(this.$.component.select.bind(this.$.component, this.#element, Video.key), 0); return result; } /** * @hook Modules.Modal * @type {SunEditor.Hook.Modal.Init} */ modalInit() { Modal.OnChangeFile(this.fileModalWrapper, []); if (this.videoInputFile) this.videoInputFile.value = ''; if (this.videoUrlFile) this.#linkValue = this.previewSrc.textContent = this.videoUrlFile.value = ''; if (this.videoInputFile && this.videoUrlFile) { this.videoUrlFile.disabled = false; this.previewSrc.style.textDecoration = ''; } /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]')).checked = true; this.#nonResizing = false; this.sizeService.init(); } /** * @hook Editor.Component * @type {SunEditor.Hook.Component.Select} * @param {HTMLIFrameElement|HTMLVideoElement} target */ componentSelect(target) { this.#ready(target); } /** * @hook Component * @type {SunEditor.Hook.Component.Edit} */ componentEdit() { this.modal.open(); } /** * @hook Editor.Component * @type {SunEditor.Hook.Component.Destroy} */ async componentDestroy(target) { const targetEl = target || this.#element; const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl; const focusEl = container.previousElementSibling || container.nextElementSibling; const emptyDiv = container.parentNode; const message = await this.$.eventManager.triggerEvent('onVideoDeleteBefore', { element: targetEl, container, align: this.#align, url: this.#linkValue }); if (message === false) return; dom.utils.removeItem(container); this.modalInit(); if (emptyDiv !== this.$.frameContext.get('wysiwyg')) { this.$.nodeTransform.removeAllParents( emptyDiv, function (current) { return current.childNodes.length === 0; }, null, ); } // focus this.$.focusManager.focusEdge(focusEl); this.$.history.push(false); } /** * @description Finds and processes the URL for video by matching it against known service patterns. * @param {string} url - The original URL. * @returns {{origin: string, url: string, tag: string}|null} An object containing the original URL, the processed URL, and the tag type (e.g., `iframe`), * or `null` if no matching pattern is found. */ findProcessUrl(url) { const query = this.query; for (const key in query) { const service = query[key]; if (service.pattern.test(url)) { return { origin: url, url: service.action(url), tag: service.tag, }; } } return null; } /** * @description Converts a YouTube URL into an embeddable URL. * - If the URL does not start with `"http"`, it prepends `"https://"`. * - It also replaces `"watch?v="` with the embed path. * @param {string} url - The original YouTube URL. * @returns {string} The converted YouTube embed URL. */ convertUrlYoutube(url) { if (!/^http/.test(url)) url = 'https://' + url; url = url.replace('watch?v=', ''); if (!/^\/\/.+\/embed\//.test(url)) { url = url.replace(url.match(/\/\/.+\//)[0], '//www.youtube.com/embed/').replace('&', '?&'); } return url; } /** * @description Converts a Vimeo URL into an embeddable URL. * - Removes any trailing slash and extracts the video ID from the URL. * @param {string} url - The original Vimeo URL. * @returns {string} The converted Vimeo embed URL. */ convertUrlVimeo(url) { if (url.endsWith('/')) { url = url.slice(0, -1); } url = 'https://player.vimeo.com/video/' + url.slice(url.lastIndexOf('/') + 1); return url; } /** * @description Adds query parameters to a URL. * - If the URL already contains a query string, the provided query is appended with an `"&"`. * @param {string} url - The original URL. * @param {string} query - The query string to append. * @returns {string} The URL with the appended query parameters. */ addQuery(url, query) { if (query.length > 0) { if (/\?/.test(url)) { const splitUrl = url.split('?'); url = splitUrl[0] + '?' + query + '&' + splitUrl[1]; } else { url += '?' + query; } } return url; } /** * @description Creates or updates a video embed component. * - When updating, it replaces the existing element if necessary * - and applies the new source, size, and alignment. * - When creating, it wraps the provided element in a figure container. * @param {HTMLIFrameElement|HTMLVideoElement} oFrame - The existing video element (for update) or a newly created one. * @param {string} src - The source URL for the video. * @param {string} width - The desired width for the video element. * @param {string} height - The desired height for the video element. * @param {string} align - The alignment to apply to the video element (e.g., 'left', 'center', 'right'). * @param {boolean} isUpdate - Indicates whether this is an update to an existing component (`true`) or a new creation (`false`). * @param {{name: string, size: number}} file - File metadata associated with the video * @param {boolean} isLast - Indicates whether this is the last file in the batch (used for scroll and insert actions). */ create(oFrame, src, width, height, align, isUpdate, file, isLast) { let container = null; /** update */ if (isUpdate) { oFrame = this.#element; if (oFrame.src !== src) { const processUrl = this.findProcessUrl(src); if (/^iframe$/i.test(processUrl?.tag) && !/^iframe$/i.test(oFrame.nodeName)) { const newTag = this.createIframeTag(); newTag.src = src; oFrame.replaceWith(newTag); this.#element = oFrame = newTag; } else if (/^video$/i.test(processUrl?.tag) && !/^video$/i.test(oFrame.nodeName)) { const newTag = this.createVideoTag(); newTag.src = src; oFrame.replaceWith(newTag); this.#element = oFrame = newTag; } else { oFrame.src = src; } } container = this.#container; } else { /** create */ oFrame.src = src; this.#element = oFrame; const figure = Figure.CreateContainer(oFrame, 'se-video-container'); container = figure.container; } /** rendering */ this.#element = oFrame; this.#container = container; this.figure.open(oFrame, { nonResizing: this.#nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, infoOnly: true }); // set size const resolved = this.sizeService.resolveSize(width, height, oFrame, isUpdate); width = resolved.width; height = resolved.height; // align this.figure.setAlign(oFrame, align); // select figure // oFrame.onload = OnloadVideo.bind(this, oFrame); this.fileManager.setFileData(oFrame, file); if (!isUpdate) { this.$.component.insert(container, { scrollTo: isLast ? true : false, insertBehavior: isLast ? this.pluginOptions.insertBehavior : 'line' }); return; } if (!this.#resizing || !resolved.isChanged || !this.figure.isVertical) this.figure.setTransform(oFrame, width, height, 0); this.$.history.push(false); } /** * @description Creates a new `IFRAME` element for video embedding. * - Applies any additional properties provided and sets the necessary attributes for embedding. * @param {Object<string, string>} [props] - An optional object containing properties to assign to the `IFRAME`. * @returns {HTMLIFrameElement} The newly created `IFRAME` element. */ createIframeTag(props) { /** @type {HTMLIFrameElement} */ const iframeTag = dom.utils.createElement('IFRAME'); if (props) { for (const key in props) { iframeTag[key] = props[key]; } } this.#setIframeAttrs(iframeTag); return iframeTag; } /** * @description Creates a new `VIDEO` element for video embedding. * - Applies any additional properties provided and sets the necessary attributes. * @param {Object<string, string>} [props] - An optional object containing properties to assign to the `VIDEO` element. * @returns {HTMLVideoElement} The newly created `VIDEO` element. */ createVideoTag(props) { /** @type {HTMLVideoElement} */ const videoTag = dom.utils.createElement('VIDEO'); if (props) { for (const key in props) { videoTag[key] = props[key]; } } this.#setTagAttrs(videoTag); return videoTag; } /** * @description Create a `video` component using the provided files. * @param {FileList|File[]} fileList File object list * @returns {Promise<boolean>} If return `false`, the file upload will be canceled */ async submitFile(fileList) { if (fileList.length === 0) return; let fileSize = 0; const files = []; const singleSizeLimit = this.pluginOptions.uploadSingleSizeLimit; for (let i = 0, len = fileList.length, f, s; i < len; i++) { f = fileList[i]; if (!/video/i.test(f.type)) continue; s = f.size; if (singleSizeLimit > 0 && s > singleSizeLimit) { const err = '[SUNEDITOR.videoUpload.fail] Size of uploadable single file: ' + singleSizeLimit / 1000 + 'KB'; const message = await this.$.eventManager.triggerEvent('onVideoUploadError', { error: err, limitSize: singleSizeLimit, uploadSize: s, file: f, }); this.$.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error'); return false; } files.push(f); fileSize += s; } const limitSize = this.pluginOptions.uploadSizeLimit; const currentSize = this.fileManager.getSize(); if (limitSize > 0 && fileSize + currentSize > limitSize) { const err = '[SUNEDITOR.videoUpload.fail] Size of uploadable total videos: ' + limitSize / 1000 + 'KB'; const message = await this.$.eventManager.triggerEvent('onVideoUploadError', { error: err, limitSize, currentSize, uploadSize: fileSize }); this.$.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error'); return false; } const videoInfo = { url: null, files, ...this.#getInfo(), }; const handler = function (uploadCallback, infos, newInfos) { infos = newInfos || infos; uploadCallback(infos, infos.files); }.bind(this, this.uploadService.serverUpload.bind(this.uploadService), videoInfo); const result = await this.$.eventManager.triggerEvent('onVideoUploadBefore', { info: videoInfo, handler, }); if (result === undefined) return true; if (result === false) return false; if (result !== null && typeof result === 'object') handler(result); if (result === true || result === NO_EVENT) handler(null); } /** * @description Create a `video` component using the provided url. * @param {string} url File url * @returns {Promise<boolean>} If return `false`, the file upload will be canceled */ async submitURL(url) { if (!(url = this.#linkValue)) return false; /** iframe source */ if (/^<iframe.*\/iframe>$/.test(url)) { const oIframe = new DOMParser().parseFromString(url, 'text/html').querySelector('iframe'); url = oIframe.src; if (url.length === 0) return false; } const processUrl = this.findProcessUrl(url); if (processUrl) { url = processUrl.url; } const file = { name: url.split('/').pop(), size: 0 }; const videoInfo = { url, files: file, ...this.#getInfo(), process: processUrl }; const handler = function (infos, newInfos) { infos = newInfos || infos; this.create(this[/^iframe$/i.test(infos.process?.tag) ? 'createIframeTag' : 'createVideoTag'](), infos.url, infos.inputWidth, infos.inputHeight, infos.align, infos.isUpdate, infos.files, true); }.bind(this, videoInfo); const result = await this.$.eventManager.triggerEvent('onVideoUploadBefore', { info: videoInfo, handler, }); if (result === undefined) return true; if (result === false) return false; if (result !== null && typeof result === 'object') handler(result); if (result === true || result === NO_EVENT) handler(null); return true; } /** * @description Prepares the component for selection. * - Ensures that the controller is properly positioned and initialized. * - Prevents duplicate event handling if the component is already selected. * @param {HTMLIFrameElement|HTMLVideoElement} target - The selected element. * @param {boolean} [infoOnly=false] - If `true`, only retrieves information without opening the controller. */ #ready(target, infoOnly = false) { if (!target) return; const figureInfo = this.figure.open(target, { nonResizing: this.#nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, infoOnly }); this.#element = target; this.#container = figureInfo.container; this.#align = figureInfo.align; target.style.float = ''; const originWidth = String(figureInfo.width || figureInfo.originWidth || figureInfo.w || ''); const originHeight = String(figureInfo.height || figureInfo.originHeight || figureInfo.h || ''); this.sizeService.setOriginSize(originWidth, originHeight); if (this.videoUrlFile) this.#linkValue = this.previewSrc.textContent = this.videoUrlFile.value = this.#element.src || this.#element.querySelector('source')?.src || ''; /** @type {HTMLInputElement} */ const activeAlign = this.modal.form.querySelector('input[name="suneditor_video_radio"][value="' + this.#align + '"]') || this.modal.form.querySelector('input[name="suneditor_video_radio"][value="none"]'); activeAlign.checked = true; if (!this.#resizing) return; this.sizeService.ready(figureInfo, target); } /** * @description Retrieves video information including size and alignment. * @returns {*} Video information object. */ #getInfo() { const { w, h } = this.sizeService.getInputSize(); return { inputWidth: w, inputHeight: h, align: this.#align, isUpdate: this.modal.isUpdate, element: this.#element, }; } /** * @description Updates the video component within the editor. * @param {HTMLIFrameElement|HTMLVideoElement} oFrame - The video element to update. */ #fixTagStructure(oFrame) { if (!oFrame) return; const isVideoTag = /^video$/i.test(oFrame.nodeName); if (isVideoTag) { this.#setTagAttrs(/** @type {HTMLVideoElement} */ (oFrame)); } else if (/^iframe$/i.test(oFrame.nodeName)) { this.#setIframeAttrs(/** @type {HTMLIFrameElement} */ (oFrame)); } const prevFrame = oFrame; const cloneFrame = /** @type {HTMLIFrameElement|HTMLVideoElement} */ (oFrame.cloneNode(true)); const figure = Figure.CreateContainer(cloneFrame, 'se-video-container'); const container = figure.container; const figcaption = Figure.GetContainer(prevFrame)?.container?.querySelector('figcaption'); let caption = null; if (figcaption) { caption = dom.utils.createElement('figcaption'); caption.innerHTML = figcaption.innerHTML; dom.utils.removeItem(figcaption); figure.cover.appendChild(caption); } // size this.figure.open(cloneFrame, { nonResizing: this.#nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, infoOnly: true }); const size = (cloneFrame.getAttribute('data-se-size') || ',').split(','); const width = size[0] || prevFrame.width || ''; const height = size[1] || prevFrame.height || this.state.defaultRatio || ''; this.sizeService.applySize(width, height); // align const format = this.$.format.getLine(prevFrame); if (format) this.#align = format.style.textAlign || format.style.float; this.figure.setAlign(cloneFrame, this.#align); this.figure.retainFigureFormat(container, this.#element, null, this.fileManager); return cloneFrame; } /** * @description Sets attributes for the `VIDEO` tag. * @param {HTMLVideoElement} element - The `VIDEO` element. */ #setTagAttrs(element) { element.setAttribute('controls', 'true'); const attrs = this.pluginOptions.videoTagAttributes; if (!attrs) return; for (const key in attrs) { element.setAttribute(key, attrs[key]); } } /** * @description Sets attributes for the `IFRAME` tag. * @param {HTMLIFrameElement} element - The `IFRAME` element. */ #setIframeAttrs(element) { element.frameBorder = '0'; element.allowFullscreen = true; const attrs = this.pluginOptions.iframeTagAttributes; if (!attrs) return; for (const key in attrs) { element.setAttribute(key, attrs[key]); } } /** * @description Removes selected files from the file input. */ #RemoveSelectedFiles() { this.videoInputFile.value = ''; if (this.videoUrlFile) { this.videoUrlFile.disabled = false; this.previewSrc.style.textDecoration = ''; } // inputFile check Modal.OnChangeFile(this.fileModalWrapper, []); } /** * @description Handles link preview input changes. * @param {InputEvent} e - Event object */ #OnLinkPreview(e) { /** @type {HTMLInputElement} */ const eventTarget = dom.query.getEventTarget(e); const value = eventTarget.value.trim(); if (/^<iframe.*\/iframe>$/.test(value)) { this.#linkValue = value; this.previewSrc.textContent = '<IFrame :src=".."></IFrame>'; } else { this.#linkValue = this.previewSrc.textContent = !value ? '' : this.$.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0 ? this.$.options.get('defaultUrlProtocol') + value : !value.includes('://') ? '/' + value : value; } } /** * @description Opens the video gallery. */ #OpenGallery() { this.$.plugins.videoGallery.open(this.#SetUrlInput.bind(this)); } /** * @description Sets the URL input value when selecting from the gallery. * @param {HTMLInputElement} target - The selected video element. */ #SetUrlInput(target) { this.#linkValue = this.previewSrc.textContent = this.videoUrlFile.value = target.getAttribute('data-command') || target.src; this.videoUrlFile.focus(); } /** * @param {InputEvent} e - Event object */ #OnfileInputChange(e) { if (!this.videoInputFile.value) { this.videoUrlFile.disabled = false; this.previewSrc.style.textDecoration = ''; } else { this.videoUrlFile.disabled = true; this.previewSrc.style.textDecoration = 'line-through'; } // inputFile check /** @type {HTMLInputElement} */ const eventTarget = dom.query.getEventTarget(e); Modal.OnChangeFile(this.fileModalWrapper, eventTarget.files); } } export default Video;