From 43208907a274375e171cc97262fcef5357291c5a Mon Sep 17 00:00:00 2001 From: "Krzysztof Gacek (Harvel)" <59796176+Harvel218@users.noreply.github.com> Date: Tue, 11 Jan 2022 11:14:36 +0100 Subject: [PATCH] Feature/video media read (#410) * v0.0.0 * add media button to ckeditor * fix js * final cleanup and fixes * CR feedback --- .../js/bitbag/bitbag-media-autocomplete.js | 4 +- src/Resources/assets/admin/scss/_css.scss | 5 +- .../config/fos_ck_editor/fos_ck_editor.yml | 12 +- src/Resources/public/.DS_Store | Bin 0 -> 6148 bytes .../js/ckeditor-plugins/dialogs/index.js | 401 ++++++++++++++++++ .../js/ckeditor-plugins/image/.DS_Store | Bin 0 -> 6148 bytes .../media.png => image/icons/mediaimage.png} | Bin .../{media => image}/plugin.js | 14 +- .../ckeditor-plugins/media/dialogs/media.js | 221 ---------- .../video/icons/mediavideo.png | Bin 0 -> 9542 bytes .../js/ckeditor-plugins/video/plugin.js | 18 + tests/Application/package.json | 3 +- 12 files changed, 441 insertions(+), 237 deletions(-) create mode 100644 src/Resources/public/.DS_Store create mode 100644 src/Resources/public/js/ckeditor-plugins/dialogs/index.js create mode 100644 src/Resources/public/js/ckeditor-plugins/image/.DS_Store rename src/Resources/public/js/ckeditor-plugins/{media/icons/media.png => image/icons/mediaimage.png} (100%) rename src/Resources/public/js/ckeditor-plugins/{media => image}/plugin.js (50%) delete mode 100644 src/Resources/public/js/ckeditor-plugins/media/dialogs/media.js create mode 100644 src/Resources/public/js/ckeditor-plugins/video/icons/mediavideo.png create mode 100644 src/Resources/public/js/ckeditor-plugins/video/plugin.js diff --git a/src/Resources/assets/admin/js/bitbag/bitbag-media-autocomplete.js b/src/Resources/assets/admin/js/bitbag/bitbag-media-autocomplete.js index 77dece0b9..35f1e60ce 100644 --- a/src/Resources/assets/admin/js/bitbag/bitbag-media-autocomplete.js +++ b/src/Resources/assets/admin/js/bitbag/bitbag-media-autocomplete.js @@ -79,7 +79,7 @@ export class HandleAutoComplete { const data = await res.json(); this._addToSelectMenu(data); - this.selectMenu.firstChild.click(); + this.selectMenu?.firstChild?.click(); triggerCustomEvent(this.mediaContainer, 'cms.media.saved.reload.completed', data); } catch (error) { @@ -127,7 +127,7 @@ export class HandleAutoComplete { _addToSelectMenu(arr) { triggerCustomEvent(this.mediaContainer, 'cms.media.display.update.start'); this.selectMenu.innerHTML = ''; - arr.forEach((item) => { + arr?.forEach((item) => { this.selectMenu.insertAdjacentHTML('beforeend', this._itemTemplate(item.path, item.code.trim())); }); triggerCustomEvent(this.mediaContainer, 'cms.media.display.update.end'); diff --git a/src/Resources/assets/admin/scss/_css.scss b/src/Resources/assets/admin/scss/_css.scss index e25531c83..2489ccbb6 100644 --- a/src/Resources/assets/admin/scss/_css.scss +++ b/src/Resources/assets/admin/scss/_css.scss @@ -1,10 +1,10 @@ #bitbag-cms-resource-preview-modal { - height: 90%; + height: 100%; } #bitbag-cms-resource-preview-modal iframe { width: 100%; - height: 600px; + height: 100vh; } #bitbag-cms-resource-preview-modal .ui.header { @@ -52,6 +52,7 @@ } .media-list__item__input { + z-index: 99; position: absolute !important; bottom: 10px; right: 10px; diff --git a/src/Resources/config/fos_ck_editor/fos_ck_editor.yml b/src/Resources/config/fos_ck_editor/fos_ck_editor.yml index 293f9f23b..5b83b4efd 100644 --- a/src/Resources/config/fos_ck_editor/fos_ck_editor.yml +++ b/src/Resources/config/fos_ck_editor/fos_ck_editor.yml @@ -7,10 +7,14 @@ fos_ck_editor: forcePasteAsPlainText: 'allow-word' allowedContent: true extraPlugins: - - 'media' + - 'mediaVideo' + - 'mediaImage' plugins: - media: - path: '/bundles/bitbagsyliuscmsplugin/js/ckeditor-plugins/media/' + mediaVideo: + path: '/bundles/bitbagsyliuscmsplugin/js/ckeditor-plugins/video/' + filename: 'plugin.js' + mediaImage: + path: '/bundles/bitbagsyliuscmsplugin/js/ckeditor-plugins/image/' filename: 'plugin.js' toolbars: configs: @@ -26,4 +30,4 @@ fos_ck_editor: - '@standard.paragraph' - '@standard.about' items: - standard.insert: ['Image', 'Media', 'Table', 'HorizontalRule', 'SpecialChar'] + standard.insert: ['Image', 'MediaVideo', 'MediaImage', 'Table', 'HorizontalRule', 'SpecialChar'] diff --git a/src/Resources/public/.DS_Store b/src/Resources/public/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..dee0e3ef6901846f5ae79a82155c4009b8f6a42b GIT binary patch literal 6148 zcmeH~u?oUK42Bc!P;lw!c#99<8yrQSpfBJc2qF~}o$t~8lgnatmLh*3`7XJJw*Sy; zL`3_?aVye^NDDX0#=^uD`9jWee-5|tave{|t!B2U@Eo;HCi}5XPys4H1*iZOpaL^e zAkOi2HKS+Zqfh}VFb@Un`;g$q94wXo=|J!i0Blfp!`f#FFj)c2!BU9|Ok;Y`XjLCW ztnTe#itBQ)RMvLU7(O)atTx5Kw6=>TBrwem1}Z=WCJHQT-r4!Thku*@CoN2=02TN% z1+>`>yB;qUXY0r7S^W@ITNgO!mm|FW1R$}acnx>MezFCagQXG`7=Hv@1_mndQw3g) C!VpCO literal 0 HcmV?d00001 diff --git a/src/Resources/public/js/ckeditor-plugins/dialogs/index.js b/src/Resources/public/js/ckeditor-plugins/dialogs/index.js new file mode 100644 index 000000000..f5cbafb3f --- /dev/null +++ b/src/Resources/public/js/ckeditor-plugins/dialogs/index.js @@ -0,0 +1,401 @@ +/* + This file was created by developers working at BitBag + Do you need more information about us and what we do? Visit our https://bitbag.io website! + We are hiring developers from all over the world. Join us and start your new, exciting adventure and become part of us: https://bitbag.io/career +*/ + +let oldValue = null; +let phrase = ''; +let currentPage = 1; +let totalPages = null; +let limit = 12; +const mediaCodeLength = 20; + +function trimValue(item) { + return item.length > mediaCodeLength ? item.substring(0, mediaCodeLength) + '...' : item; +} + +function htmlToString(item) { + return String(item).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +} + +function checkName(item) { + if (item) return item; + + return 'Empty name'; +} + +function insertImageHtml(data) { + const output = data + .map((media) => { + return `
+ + + +
`; + }) + .join(''); + + return output; +} + +function insertVideoHtml(data) { + const output = data + .map((media) => { + return `
+ + + +
`; + }) + .join(''); + + return output; +} + +function numPages(totalResults) { + return Math.ceil(totalResults / limit); +} + +function prevImagesPage() { + if (currentPage > 1) { + currentPage--; + changePage(currentPage, 'image-btn-next', 'image-btn-prev', 'image-page-number'); + } + + showMediaImages(phrase, currentPage); +} + +function nextImagesPage() { + if (currentPage < totalPages) { + currentPage++; + changePage(currentPage, 'image-btn-next', 'image-btn-prev', 'image-page-number'); + } + + showMediaImages(phrase, currentPage); +} + +function prevVideosPage() { + if (currentPage > 1) { + currentPage--; + changePage(currentPage, 'video-btn-next', 'video-btn-prev', 'video-page-number'); + } + + showMediaVideos(phrase, currentPage); +} + +function nextVideosPage() { + if (currentPage < totalPages) { + currentPage++; + changePage(currentPage, 'video-btn-next', 'video-btn-prev', 'video-page-number'); + } + + showMediaVideos(phrase, currentPage); +} + +function changePage(page, next, prev, number) { + const btn_next = document.getElementById(next); + const btn_prev = document.getElementById(prev); + const pageNumber = document.getElementById(number); + + if (page < 1) page = 1; + if (page > totalPages) page = totalPages; + + pageNumber.innerHTML = page; + + if (page == 1) { + btn_prev.style.visibility = 'hidden'; + } else { + btn_prev.style.visibility = 'visible'; + } + + if (page == totalPages) { + btn_next.style.visibility = 'hidden'; + } else { + btn_next.style.visibility = 'visible'; + } +} + +function showMediaImages(phrase, pageNumber) { + const myObject = { + criteria: { + search: { + type: 'contains', + value: phrase, + }, + type: 'image', + }, + page: pageNumber, + limit: limit, + }; + + const shallowEncoded = $.param(myObject); + const shallowDecoded = decodeURIComponent(shallowEncoded); + + $.ajax({ + type: 'GET', + url: `${route}?${shallowDecoded}`, + dataType: 'JSON', + success(data) { + totalPages = numPages(data.total); + changePage(currentPage, 'image-btn-next', 'image-btn-prev', 'image-page-number'); + const element = CKEDITOR.document.getById('media-image-list'); + + if (element) { + element.setHtml(insertImageHtml(data._embedded.items)); + } + }, + error(jqXHR, textStatus, errorThrown) { + console.error(`ajax error ${textStatus} ${errorThrown}`); + }, + }); +} + +function showMediaVideos(phrase, pageNumber) { + const myObject = { + criteria: { + search: { + type: 'contains', + value: phrase, + }, + type: 'video', + }, + page: pageNumber, + limit: limit, + }; + + const shallowEncoded = $.param(myObject); + const shallowDecoded = decodeURIComponent(shallowEncoded); + + $.ajax({ + type: 'GET', + url: `${route}?${shallowDecoded}`, + dataType: 'JSON', + success(data) { + totalPages = numPages(data.total); + changePage(currentPage, 'video-btn-next', 'video-btn-prev', 'video-page-number'); + const element = CKEDITOR.document.getById('media-video-list'); + + if (element) { + element.setHtml(insertVideoHtml(data._embedded.items)); + } + }, + error(jqXHR, textStatus, errorThrown) { + console.error(`ajax error ${textStatus} ${errorThrown}`); + }, + }); +} + +CKEDITOR.dialog.add('videoDialog', (editor) => ({ + title: 'Choose media', + minWidth: 1000, + minHeight: 600, + resizable: CKEDITOR.DIALOG_RESIZE_NONE, + onShow() { + phrase = ''; + showMediaVideos(phrase, currentPage); + }, + + contents: [ + { + id: 'media-video-content', + elements: [ + { + type: 'text', + id: 'phrase', + label: 'Search by phrase', + inputStyle: 'text-align: left', + controlStyle: 'width: 100%', + onKeyUp: function () { + phrase = this.getValue(); + + if (oldValue === phrase) { + return; + } + + oldValue = this.getValue(); + changePage(currentPage, 'video-btn-next', 'video-btn-prev', 'video-page-number'); + showMediaVideos(phrase, currentPage); + }, + }, + { + type: 'hbox', + widths: ['25%', '25%'], + style: 'width: 10em', + align: 'left', + children: [ + { + type: 'text', + id: 'imageWidth', + label: 'Image width', + inputStyle: 'text-align: center', + controlStyle: 'width: 4em', + inputStyle: 'width: 4em', + }, + { + type: 'text', + id: 'imageHeight', + default: '200', + label: 'Image height', + inputStyle: 'text-align: center', + controlStyle: 'width: 4em', + }, + ], + }, + + { + type: 'html', + id: 'media-video-list', + label: 'Media found:', + html: '
', + }, + { + type: 'hbox', + widths: ['25%', '25%', '25%'], + style: 'width: 10em', + align: 'center', + children: [ + { + type: 'html', + id: 'video-btn-prev', + html: '', + }, + { + type: 'html', + id: 'video-page-number', + html: '', + }, + { + type: 'html', + id: 'video-btn-next', + html: '', + }, + ], + }, + ], + }, + ], + onOk() { + const dialog = this; + const document = CKEDITOR.document; + const element = document.find('#media-video-list .media-list__item__input:checked'); + const imagePath = element.getItem(0).getAttribute('image-path'); + const imageWidth = dialog.getContentElement('media-video-content', 'imageWidth').getValue(); + const imageHeight = dialog.getContentElement('media-video-content', 'imageHeight').getValue(); + + editor.insertHtml( + `` + ); + }, +})); + +CKEDITOR.dialog.add('imageDialog', (editor) => ({ + title: 'Choose media', + minWidth: 1000, + minHeight: 600, + resizable: CKEDITOR.DIALOG_RESIZE_NONE, + onShow() { + phrase = ''; + showMediaImages(phrase, currentPage); + }, + + contents: [ + { + id: 'media-image-content', + elements: [ + { + type: 'text', + id: 'phrase', + label: 'Search by phrase', + inputStyle: 'text-align: left', + controlStyle: 'width: 100%', + onKeyUp: function () { + phrase = this.getValue(); + + if (oldValue === phrase) { + return; + } + + oldValue = this.getValue(); + changePage(currentPage, 'image-btn-next', 'image-btn-prev', 'image-page-number'); + showMediaImages(phrase, currentPage); + }, + }, + { + type: 'hbox', + widths: ['25%', '25%'], + style: 'width: 10em', + align: 'left', + children: [ + { + type: 'text', + id: 'imageWidth', + label: 'Image width', + inputStyle: 'text-align: center', + controlStyle: 'width: 4em', + inputStyle: 'width: 4em', + }, + { + type: 'text', + id: 'imageHeight', + default: '200', + label: 'Image height', + inputStyle: 'text-align: center', + controlStyle: 'width: 4em', + }, + ], + }, + + { + type: 'html', + id: 'media-image-list', + label: 'Media found:', + html: '
', + }, + { + type: 'hbox', + widths: ['25%', '25%', '25%'], + style: 'width: 10em', + align: 'center', + children: [ + { + type: 'html', + id: 'mage-btn-prev', + html: '', + }, + { + type: 'html', + id: 'image-page-number', + html: '', + }, + { + type: 'html', + id: 'image-btn-next', + html: '', + }, + ], + }, + ], + }, + ], + onOk() { + const dialog = this; + const document = CKEDITOR.document; + const element = document.find('#media-image-list .media-list__item__input:checked'); + const imagePath = element.getItem(0).getAttribute('image-path'); + const imageWidth = dialog.getContentElement('media-image-content', 'imageWidth').getValue(); + const imageHeight = dialog.getContentElement('media-image-content', 'imageHeight').getValue(); + + editor.insertHtml( + `cms plugin media image` + ); + }, +})); diff --git a/src/Resources/public/js/ckeditor-plugins/image/.DS_Store b/src/Resources/public/js/ckeditor-plugins/image/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d8d844af3d0e5ad8739cc0c55cf1aa5c1b2c9724 GIT binary patch literal 6148 zcmeHKI|>3Z5S{S@f{mqRuHX%V=n1@lpdf;*plH37=kjR2`83O7r-kwcCNG)HOUNsB zc0@#%xBWt7CL$BKp*(Epo9&zTY>*KJ!g0p+zPU7~b93nBz6%(4EJxYNS(*;tc4$<9 z3Qz$mKn1A4rxnNwJDGm^V4g<>sKC!FVBd!VH>`Bxq5@RluN2VHYO`A6Nm*NakF#1^;9I!mJmF@TI|YN6W1yE~EUX;QJt^{v&9Pq- U+d!uy?sOo3222+k75KISFBrfS)c^nh literal 0 HcmV?d00001 diff --git a/src/Resources/public/js/ckeditor-plugins/media/icons/media.png b/src/Resources/public/js/ckeditor-plugins/image/icons/mediaimage.png similarity index 100% rename from src/Resources/public/js/ckeditor-plugins/media/icons/media.png rename to src/Resources/public/js/ckeditor-plugins/image/icons/mediaimage.png diff --git a/src/Resources/public/js/ckeditor-plugins/media/plugin.js b/src/Resources/public/js/ckeditor-plugins/image/plugin.js similarity index 50% rename from src/Resources/public/js/ckeditor-plugins/media/plugin.js rename to src/Resources/public/js/ckeditor-plugins/image/plugin.js index 5add22b22..d992f2be3 100644 --- a/src/Resources/public/js/ckeditor-plugins/media/plugin.js +++ b/src/Resources/public/js/ckeditor-plugins/image/plugin.js @@ -4,15 +4,15 @@ We are hiring developers from all over the world. Join us and start your new, exciting adventure and become part of us: https://bitbag.io/career */ -CKEDITOR.plugins.add('media', { - icons: 'media', +CKEDITOR.plugins.add('mediaImage', { + icons: 'mediaimage', init(editor) { - editor.addCommand('media', new CKEDITOR.dialogCommand('mediaDialog')); - editor.ui.addButton('Media', { - label: 'Insert media', - command: 'media', + editor.addCommand('mediaImage', new CKEDITOR.dialogCommand('imageDialog')); + editor.ui.addButton('MediaImage', { + label: 'Insert media image', + command: 'mediaImage', toolbar: 'insert', }); - CKEDITOR.dialog.add('mediaDialog', `${this.path}dialogs/media.js`); + CKEDITOR.dialog.add('imageDialog', `${this.path.replace('image/', '')}dialogs/index.js`); }, }); diff --git a/src/Resources/public/js/ckeditor-plugins/media/dialogs/media.js b/src/Resources/public/js/ckeditor-plugins/media/dialogs/media.js deleted file mode 100644 index da7aabecf..000000000 --- a/src/Resources/public/js/ckeditor-plugins/media/dialogs/media.js +++ /dev/null @@ -1,221 +0,0 @@ -/* - This file was created by developers working at BitBag - Do you need more information about us and what we do? Visit our https://bitbag.io website! - We are hiring developers from all over the world. Join us and start your new, exciting adventure and become part of us: https://bitbag.io/career -*/ - -let oldValue = null; -let phrase = ''; -let currentPage = 1; -let totalPages = null; -let limit = 12; -const mediaCodeLength = 20; - -function trimValue(item) { - return item.length > mediaCodeLength ? - item.substring(0, mediaCodeLength) + '...' : - item; -} - -function htmlToString(item) { - return String(item).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); -}; - -function checkName(item) { - if (item) return item; - else return 'Empty name'; -}; - -function insertHtml (data) { - const output = data.map(media => - `
- - - -
` - ).join(''); - return output; -} - -function refreshMedia () { - showMedia(phrase, currentPage); -} - -function numPages(totalResults){ - return Math.ceil(totalResults/limit); -} - -function prevPage(){ - if (currentPage > 1) { - currentPage--; - changePage(currentPage); - } - refreshMedia(); - -} - -function nextPage(){ - if (currentPage < totalPages) { - currentPage++; - changePage(currentPage); - } - refreshMedia(); -} - -function changePage(page) -{ - const btn_next = document.getElementById("btn-next"); - const btn_prev = document.getElementById("btn-prev"); - const pageNumber = document.getElementById("page-number"); - - if (page < 1) page = 1; - if (page > totalPages) page = totalPages; - - pageNumber.innerHTML = page; - - if (page == 1) { - btn_prev.style.visibility = "hidden"; - } - else { - btn_prev.style.visibility = "visible"; - } - - if (page == totalPages) { - btn_next.style.visibility = "hidden"; - } - else { - btn_next.style.visibility = "visible"; - } -} - -function showMedia (phrase, pageNumber) { - const myObject = { - criteria: { - search: { - type: 'contains', - value: phrase - }, - type: 'image', - }, - page: pageNumber, - limit: limit, - }; - const shallowEncoded = $.param( myObject ); - const shallowDecoded = decodeURIComponent( shallowEncoded ); - - $.ajax({ - type: 'GET', - url: route + '?' + shallowDecoded, - dataType: 'JSON', - success(data) { - totalPages = numPages(data.total); - changePage(currentPage); - const element = CKEDITOR.document.getById('media-list'); - if (element) { - element.setHtml(insertHtml(data._embedded.items)); - } - }, - error(jqXHR, textStatus, errorThrown) { - console.log(`ajax error ${textStatus} ${errorThrown}`); - }, - }); -} - -CKEDITOR.dialog.add ('mediaDialog', editor => ({ - title: 'Choose media', - minWidth: 1000, - minHeight: 600, - resizable: CKEDITOR.DIALOG_RESIZE_NONE, - onShow() { - phrase = ''; - showMedia(phrase, currentPage); - }, - - contents: [ - { - id: "media-content", - elements: [ - { - type: 'text', - id: 'phrase', - label: 'Search by phrase', - inputStyle: 'text-align: left', - controlStyle: 'width: 100%', - onKeyUp: function() { - phrase = this.getValue(); - if(oldValue === phrase){ - return; - } - oldValue = this.getValue(); - changePage(currentPage); - showMedia(phrase, currentPage); - } - }, - { - type: 'hbox', - widths: [ '25%', '25%' ], - style: 'width: 10em', - align: 'left', - children: [ - { - type: 'text', - id: 'imageWidth', - label: 'Image width', - inputStyle: 'text-align: center', - controlStyle: 'width: 4em', - inputStyle: 'width: 4em', - }, - { - type: 'text', - id: 'imageHeight', - default: '200', - label: 'Image height', - inputStyle: 'text-align: center', - controlStyle: 'width: 4em' - }, - ] - }, - - { - type: 'html', - id: 'media-list', - label: 'Media found:', - html: '
', - }, - { - type: 'hbox', - widths: [ '25%', '25%', '25%' ], - style: 'width: 10em', - align: 'center', - children: [ - { - type: 'html', - id: 'btn-prev', - html: '', - }, - { - type: 'html', - id: 'page-number', - html: '', - }, - { - type: 'html', - id: 'btn-next', - html: '', - }, - ] - }, - ] - }, - ], - onOk() { - const dialog = this; - const document = CKEDITOR.document; - const element = document.find( '.media-list__item__input:checked'); - const imagePath = element.getItem(0).getAttribute( 'image-path' ); - const imageWidth = dialog.getContentElement('media-content','imageWidth').getValue(); - const imageHeight = dialog.getContentElement('media-content','imageHeight').getValue(); - - editor.insertHtml(`media-img`); - }, -})); diff --git a/src/Resources/public/js/ckeditor-plugins/video/icons/mediavideo.png b/src/Resources/public/js/ckeditor-plugins/video/icons/mediavideo.png new file mode 100644 index 0000000000000000000000000000000000000000..8c2b6f72bbd8ecd31baae700b3f5505b61c1aeb5 GIT binary patch literal 9542 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