Created
August 11, 2020 13:06
-
-
Save sambarrowclough/27f9321ae4fb6b521268d00cf53db29e to your computer and use it in GitHub Desktop.
Revisions
-
sambarrowclough created this gist
Aug 11, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,2272 @@ import tippy, { createSingleton } from "tippy.js"; import "tippy.js/themes/light-border.css"; import "tippy.js/dist/tippy.css"; import "@babel/polyfill"; import Swal from "sweetalert2/dist/sweetalert2.js"; import "sweetalert2/src/sweetalert2.scss"; import "loaders.css" // Local import * as _ from "./utils"; import "./Editor.scss"; import "./libs/pickr/src/scss/themes/nano.scss"; import Pickr from "./libs/pickr"; import Croppie from "./libs/Croppie/croppie"; import "./libs/Croppie/croppie.css"; import Spinner from "./Spinner"; import Logger from "./Logger"; // import data from "./data.json"; let isDev = __ENV__ === "dev"; window.log = { debug: () => {}, error: () => {}, warn: () => {}, info: () => {} } class Editor { static _croppie = Croppie; static _utils = _; static _pickr = Pickr; static _tippy = tippy; static EDITOR_ACTIONS = { DONE: "DONE", CANCEL: "CANCEL", ZOOM: "ZOOM", SELECT: "SELECT", REPLACE: "REPLACE", SETTINGS_DURATION:"SETTINGS_DURATION", SETTINGS_START: "SETTINGS_START", SETTINGS_RANDOM: "SETTINGS_RANDOM", SETTINGS_ARROWS: "SETTINGS_ARROWS", SETTINGS_HOVER_PAUSE: "SETTINGS_HOVER_PAUSE", SETTINGS_NUMBERED: "SETTINGS_NUMBERED", SETTINGS_EFFECT: "SETTINGS_EFFECT", SETTINGS_VISIBLE: "SETTINGS_VISIBLE", SETTINGS: "SETTINGS", URL: "URL", CAPTION: "CAPTION", PREVIEW: "PREVIEW", DELETE: "DELETE", ADD: "ADD", } // Evenlistener name: [callbacks] _eventListener = { // Actions init: [], move: [], edit: [], add: [], delete: [], select: [], publish: [], settings:[], replace:[], url:[], caption:[], preview:[], back: [], done:[], cancel:[], // Logs error: [], debug: [], warn: [] }; static DEFAULT_OPTIONS = { boundaryHeight: 500, boundaryWidth: 1200, animSpeed: 0, effect: 8, nav: true, nav2: false, pauseOnHover: true, pauseTime: 3000, next: "Next", previous: "Prev", randomStart: true, startSlide: 0, visible: true, forceManual: false, slides: [ { text: null, imageUrl: "https://d13z1xw8270sfc.cloudfront.net/origin/477866/my-img-1595420917353.png", url: null, linkMode: null } ] } _slides = []; _slide = null; _slideLimit = 10; _logo = null; _sortables = []; _style = {}; pickr = null; _slideInitIndex = 0; constructor(opt) { log.debug("Begin initialisation"); log.debug("Resolving options"); this._resolveOptions(opt) .then(() => { log.debug("Resolved options"); log.debug("Binding components"); return this._bindComponents(); }) .then(() => { log.debug("Bound component"); this._emit("init", this); }) .catch((e) => { this._emit("error", e); }); } _resolveOptions(opt) { return new Promise((resolve, reject) => { this.namespace = "designeditor"; // Assign default values this.options = this.opt = Object.assign( {}, Editor.DEFAULT_OPTIONS, opt ); log.debug("Options:", this.options); this._root = { editorActions: { select: [], }, }; // Slideshow if (opt.slides?.length) { log.debug("Setting up slideshow controls"); this._slides = opt.slides } else { log.debug("No slides given in options, setting up with default image") this._slides = [{ imageUrl: "https://d13z1xw8270sfc.cloudfront.net/origin/477866/plain-white-background_1592919904955.jpg" }] } if (!opt?.boundaryHeight) { log.warn(`Using default boundary width ${Editor.DEFAULT_OPTIONS.boundaryHeight}`) } if (!opt?.boundaryWidth) { log.warn(`Using default boundary width ${Editor.DEFAULT_OPTIONS.boundaryWidth}`) } resolve(); }); } _flexLoaded(){ const url = "javascripts/flexslider/jquery.flexslider-min_v1.js" return document.body.innerHTML.search(url) > -1 } async _bindComponents() { const { flex, slideshowTarget } = this.options // TODO: resolveOptions should set the this.startImageUrl so we are not doing checks like this // Set default image as white-background if (this._slides && this._slides.length && this._activeSlide() && this._activeSlide().imageUrl) { const defaultImage = "https://d13z1xw8270sfc.cloudfront.net/origin/477866/plain-white-background_1592919904955.jpg"; const url = this._activeSlide().imageUrl var isImg = await _.isImg(url); if (!isImg) { this._activeSlide().imageUrl = defaultImage log.warn(`Image [${url}] is not an image. Binding default image to the editor.`) } } else { this._emit("warn", "Found no image in options, binding default image.") } if (flex) { flex.remove(); log.debug("Removed flex slider"); await this._buildImageEditorShell().catch(err => this._emit("error", err)); log.debug("Built image editor shell"); } else if (flex && !isImg) { log.error("Failed to build shideshow. No flex object and startImage is not an image."); this._emit("error", err) } else { await this._buildImageEditorShell().catch(err => this._emit("error", err)); log.debug("Built image editor shell"); } // Remove default spacing on buttons for editor var btns = document.querySelectorAll(".ql-toolbar button"); for (let index = 0; index < btns.length; index++) { const btn = btns[index]; if (btn instanceof HTMLElement) { btn.style.marginBottom = "0px"; } } // Styles if (this._style.enabled) { // Disable main style sheet document.querySelector("#design_css_master").disabled = true; this._buildStyles(); } // Spotlight slideshowTarget.style.zIndex = "1002"; const overlay = _.createFromTemplate(`<div :ref=overlay id="overlay"></div>`); _.inBefore(overlay.overlay, document.body); } // blob -> https://d13z1xw8270sfc.cloudfront.net/origin/{storeId}/{file} async url(blob, storeId) { var time = new Date().getTime(); if (/Edge/.test(navigator.userAgent) || /Trident/.test(navigator.userAgent)) { var file = new Blob([blob], { type: "image/png" }); file.name = "my-img-" + time + ".png"; } else { var file = new File([blob], "my-img-" + time + ".png", { type: "image/png", }); } const signed = await this.sign(storeId, file.name, file.type, "prodimg") return this.upload(file, signed.url, storeId); } async sign(shopkeeper, fileName, contentType, type = "prodimg") { var url = "https://2iiejpzs1a.execute-api.eu-west-1.amazonaws.com/v1/geturl"; var opts = { headers: { "content-type": "application/json", }, body: JSON.stringify({ shopkeeper: shopkeeper, fileName: fileName, contentType: contentType, type: type, }), method: "POST", }; return fetch(url, opts).then(function (res) { if (res.status === 200) { return res.json(); } else { throw new Error(res.statusText); } }); } async upload(file, url, storeId) { var opts = { headers: { "content-type": file.type, "x-amz-acl": "public-read", }, body: file, method: "PUT", }; return fetch(url, opts).then(function (res) { if (res.status === 200) { return ( "https://d13z1xw8270sfc.cloudfront.net/origin/" + storeId + "/" + file.name ); } else { throw new Error(res.statusText); } }); } async _skip(e) { log.debug("SKIP:START", this._activeSlide()); // Get the original image this._activeSlide().imageUrl = this._slide.data.url // Hide done, cancel actions this._hideCropActions() // Wait between 0.6s-1.0s await new Promise(r => setTimeout(r, Math.floor(Math.random()*300+200))) // Reset points if (this._slide.backup) { const { url } = this._slide.data const { points, zoom, orientation } = this._slide.backup await this._slide.bind({ url, points, orientation, zoom }) } // Show toolbar, publish settings tools this._showEditorActions() log.debug("SKIP:END", this._activeSlide()); } _preview(e){ log.debug("PREVIEW:START", $("#slider1")) const { captionInput, urlInput, preview } = this._root.editorActions if (preview.classList.contains("disabled")) return const slides = this._slides.map((x) => x.imageUrl).filter(Boolean) const editor = this._slide.elements.boundary; if (captionInput) { this._saveCaptionText() _.rm(captionInput) this._root.editorActions.captionInput = null } if (urlInput) { this._saveUrlText() _.rm(urlInput) this._root.editorActions.urlInput = null } if (slides.length === 0) { this._emit("error", new Error("PREVIEW_UNAVAILABLE: Slide list is empty. Please Add a slide to use the Preview")) return } if (!this._flexLoaded()) { this._emit("error", new Error("PREVIEW_UNAVAILABLE: flex.jquery dependency not loaded. Note: We only load this dependency if you have slides on initialisation")) return } const html = _.createFromTemplate(` <div :ref=flexslider class="slider-wrapper theme-default"> <div id="slider1" class="flexslider"> <div :ref=back class="btn-group settings slider-prev"> <button class="btn"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-down-left"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg> </button> </div> <ul class="slides"> ${ this._slides && this._slides.length ? this._slides.map( (x) => ` <li> ${ x.url ? `<a target="_blank" href="${x.url}"> <img data-src="${x.imageUrl}" class="lazyload" /> ${ x.text ? `<p class="flex-caption">${x.text}</p>` : "" } </a>` : `<img data-src="${x.imageUrl}" class="lazyload" /> ${ x.text ? `<p class="flex-caption">${x.text}</p>` : "" }` } </li> ` ) : "" } </ul> </div> </div> `); // Bind flexslider for preview editor.insertAdjacentElement("beforebegin", html.flexslider); let animation = document.querySelector(".effect").value; if (animation === "8") { animation = "fade"; } else if (animation === "1") { animation = "slide"; } const opts = { useCSS: true, touch: true, animation: animation, animationSpeed: document.querySelector(".speed").value, slideshowSpeed: document.querySelector(".duration").value, // startAt: parseInt(document.querySelector(".start").value), directionNav: document.querySelector("#arrows").checked, prevText: "<", nextText: ">", pauseOnHover: document.querySelector("#pause").checked, randomize: document.querySelector("#random").checked, start: (e) => { window.flexslider = e; log.debug("START") }, }; $("#slider1").flexslider(opts); editor.style.display = "none"; // Preview events this._on(html.back, "click", (e) => { log.debug("BACK:START", this._slide) editor.style.display = "block"; $("#slider1").remove(); _.rm(html.flexslider) log.debug("BACK:END", this._slide) }); tippy(html.back, { content: "Back", delay: 350 }); // Send event log.debug("PREVIEW:END", $("#slider1")) } spinner = { start: () => { const { editorSpinner } = this._root.editorActions editorSpinner.style.opacity="1"; editorSpinner.style.zIndex="1000"; }, stop: () => { const { editorSpinner } = this._root.editorActions editorSpinner.style.opacity="0"; editorSpinner.style.zIndex="0"; } } async _done(e) { log.debug("DONE:START", this._activeSlide()) const { zoomerWrap, overlay, boundary } = this._slide.elements; const { _slideInitIndex, _slides } = this; const { editorActions } = this._root; const { storeId, publishPosition } = this.options; this._slide.elements.img.classList.add('sleep') this.spinner.start() zoomerWrap.style.zIndex="0"; // Save edited photo const blob = await this._slide.result({ type: "blob" }); const url = window.URL.createObjectURL(blob); _slides[_slideInitIndex].imageUrl = url; // Hide loader await new Promise(r => setTimeout(r, Math.floor(Math.random()*700+400))) zoomerWrap.style.zIndex="100"; this.spinner.stop() await new Promise(r => setTimeout(r, 50)) this._slide.elements.img.classList.remove('sleep') this._hideCropActions() this._showEditorActions() log.debug("DONE:END", this._activeSlide()) } async _replace(e){ log.debug("REPLACE:START", e) if (e.target.files?.length === 0) return; const { left, right, selectContainer, edit, rm, preview, URL, caption } = this._root.editorActions // Loader this._slide.elements.img.classList.add('sleep') this.spinner.start() // Bind replacing image to the Editor const file = e.target.files[0]; const url = window.URL.createObjectURL(file); // less than 1mb? wait a bit if (file.size < 1000000) { await new Promise(r => setTimeout(r, Math.floor(Math.random()*500+300))) } await this._slide.bind(url) // Enable editor actions if (this._isDefaultImage()) { [edit, preview, rm, URL, caption, left, right, selectContainer].forEach(x => x.classList.remove("disabled")) } // Crop image and save it const blob = await this._slide.result({ type: "blob" }); const img = window.URL.createObjectURL(blob); this._slides[this._slideInitIndex].imageUrl = img; // Hide loader this._slide.elements.img.classList.remove('sleep') this.spinner.stop() log.debug("REPLACE:END", this._activeSlide()) } async _publish(e) { log.debug("PUBLISH:START", this); const { storeId } = this.options const { publish, publishSpinner, publishText } = this._root.editorActions publish.classList.remove("active") publish.classList.add("active"); publishSpinner.style.display="block" publishText.style.opacity="0" try { // Get urls from blobs const { err, slides } = await (async () => { let slides = [] let err for(const slide of this._slides) { const { imageUrl } = slide // Ignore the default image if (imageUrl.indexOf('plain-white-background') > -1) { continue; } if (imageUrl && imageUrl.startsWith("blob")) { const blob = await fetch(imageUrl).then(r => r.blob()) const url = await this.url(blob, storeId).catch(err => { acc.err += err }) if (url) { slide.imageUrl = url } } slides.push(slide) } return { err, slides } })() if (err) { throw new Error(err) } const set = this._saveSettings() const cp = _.shallow(set) const parsed = _.parse(cp) const slide = _.assign(parsed, {slides}) // Save slides let request = { uri: "Editor/svc/Services.svc/UpdateSlideShow", method: "POST", body: _.str({ storeId: storeId, debugId: log._uuid, slide: _.str(slide) }), headers: { "content-type": "application/json", }, }; await fetch(request.uri, request).then((response) => handleResponse({ request, response })).then(() => { // Success! Swal.fire({ icon: "success", animation: false, title: "Your work has been saved", showConfirmButton: false, timer: 1500, }); }).catch((err) => { Swal.fire({ icon: "error", title: "Oops...", html: err, animation: false, confirmButtonColor: "#f054a7", }); log.error("Failed to save SlideShow. Request: " + _.str(request) + ". Error: " + err); }).then(async () => { // Stop spinner publish.classList.remove("active"); publishSpinner.style.display="none" publishText.style.opacity="1" log.debug("PUBLISH:END", this); }) } catch (err) { Swal.fire({ icon: "error", title: "Oops...", html: err, animation: false, confirmButtonColor: "#f054a7", }); log.error("Something went wrong prepparing the slideshow for save. Error:", err); publish.classList.remove("active"); publishSpinner.style.display="none" publishText.style.opacity="1" } } activeSlideIndex() { let { select } = this._root.editorActions; select = Array.from(select).filter(Boolean); const active = select.findIndex((x) => x.classList.contains("active")); return active; } async _buildImageEditorShell() { const { slideshowTarget } = this.options; const { _slideInitIndex, namespace } = this; const show = this.options?.visible const slideSettings = this.options; // Destory any previous croppie as we can only init one at a time if (this._slide) { this._slide.destroy(); this._slide = null; log.debug("Destoryed croppie instance"); } // Build slide editor // Default image // TODO: resolveOptions should set the this.startImageUrl so we are not doing checks like this let url = this._activeSlide().imageUrl if (!url) { url = "https://d13z1xw8270sfc.cloudfront.net/origin/477866/plain-white-background_1592919904955.jpg"; } var { offsetWidth } = slideshowTarget.parentElement; const { width, height } = await _.dimen(url); const aspect = _.aspect(width, height, offsetWidth, offsetWidth); const opt = { width: aspect.width, height: aspect.height, target: slideshowTarget, url, mouseWheelZoom: false, disabled: !show }; log.debug("Building croppie instance with options: ", opt); this._slide = await this._buildCroppieInstance(opt); log.debug("Built croppie instance with options: ", opt); const { zoomerWrap, overlay, boundary } = this._slide.elements; // Disable interaction zoomerWrap.style.opacity = "0"; overlay.style.pointerEvents = "none"; log.debug(`Building editor html`) // Paint shell actions over editor const html = _.createFromTemplate(` <div :ref=slideEditorShellContainer style="width:${ boundary.offsetWidthh }px; height:${ boundary.offsetHeight }px" class="${namespace}-slide-editor-shell-container"> <div class="${namespace}-editor-toolbar" :ref=container> <!-- SPINER --> <div :ref=editorSpinner style="top:50%;left:50%;z-index:0;" class="spinner"> <span style="border-right-color:black;"></span> </div> <!-- SPINER --> <!-- CROP ACTIONS --> <div style="top:2.25em; visibility:hidden; opacity:0;" :ref=cropActions class="btn-group shell-actions"> <button :ref=cancel class="btn"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> </button> <button :ref=skip class="btn"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-skip-forward"><polygon points="5 4 15 12 5 20 5 4"></polygon><line x1="19" y1="5" x2="19" y2="19"></line></svg> </button> <button :ref=done class="btn"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg> </button> </div> <!-- CROP ACTIONS --> <div :ref=toolbarContainer class="btn-group shell-actions"> <!-- Add --> <button :ref=add class="btn"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> </button> <input :ref=addInput type="file" class="shell-input" style="display:none;"> <!-- Add --> <div class="de-toolbar-divide"></div> <!-- Crop --> <button :ref=edit class="btn ${this._isDefaultImage() ? "disabled": ""}"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crop"><path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></path><path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></path></svg> </button> <!-- Crop --> <!-- Order slides --> <div :ref=moveContainer class="btn-group de-order-slides"> <!-- Left --> <div :ref=left class="btn ${this._isDefaultImage() || this._slides.length === 1 ? "disabled" : ""}"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg> </div> <!-- Left --> <!-- Right --> <div :ref=right class="btn ${this._isDefaultImage() || this._slides.length === 1 ? "disabled" : ""}"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg> </div> <!-- Right --> </div> <!-- Order slides --> <!-- URL --> <button :ref=URL class="btn ${this._isDefaultImage() ? "disabled": ""}"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link-2"><path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path><line x1="8" y1="12" x2="16" y2="12"></line></svg> </button> <!-- URL --> <!-- Caption --> <button :ref=caption class="btn ${this._isDefaultImage() ? "disabled": ""}"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-type"><polyline points="4 7 4 4 20 4 20 7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line></svg> </button> <!-- Delete --> <button :ref=rm class="btn ${this._isDefaultImage() ? "disabled": ""}"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg> </button> <!-- Delete --> </div> <!-- shell actions --> <!-- Select Actions --> <div :ref=selectMoveContainer class="select-actions"> <!-- Select --> <div :ref=selectContainer class="btn-group select-container ${this._isDefaultImage() || this._slides.length === 1 ? "disabled" : ""}"> ${ this._slides && this._slides.length ? this._slides .map((slide) => `<div :arr=select class="select-item"></div>` ) .join("") : "" } </div> <!-- Select --> </div> <!-- Select Actions --> <!-- settings --> <div :ref=settingsPanel class='slideshow-settings-panel'> <div class='setting'> <div class='setting-header'> <svg :ref=visibleHelp cmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <div class='setting-title'>Visible</div> </div> <div class='setting-action'> <input type="checkbox" :ref=visible id="visible" name="set-name" class="switch-input"> <label for="visible" class="switch-label"></label> </div> </div> <div class='divide'></div> <div class='setting'> <div class='setting-header'> <svg :ref=effectHelp cmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <div class='setting-title'>Effect</div> </div> <div class='setting-action'> <select :ref=effect class="setting-select effect"> <option value="1">Slide</option> <option value="8">Fade</option> </select> </div> </div> <div class='setting'> <div class='setting-header'> <svg :ref=speedHelp cmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <div class='setting-title'>Speed</div> </div> <div class='setting-action'> <select :ref=speed class="setting-select speed"> <option value="0">Manual</option> <option value="1800">Very Slow</option> <option value="1200">Slow</option> <option value="800">Fast</option> <option value="300">Very Fast</option> </select> </div> </div> <div class='setting'> <div class='setting-header'> <svg :ref=durationHelp cmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <div class='setting-title'>Duration</div> </div> <div class='setting-action'> <select :ref=duration class="setting-select duration"> <option value="3000">Very Short</option> <option value="5000">Short</option> <option value="7000">Long</option> <option value="10000">Very Long</option> </select> </div> </div> <div class='divide'></div> <div class='setting'> <div class='setting-header'> <svg :ref=randomHelp cmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <div class='setting-title'>Random</div> </div> <div class='setting-action'> <input type="checkbox" :ref=random id="random" name="set-name" class="switch-input"> <label for="random" class="switch-label"></label> </div> </div> <div class='setting'> <div class='setting-header'> <svg :ref=hoverPauseHelp cmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <div class='setting-title'>Hover pause</div> </div> <div class='setting-action'> <input type="checkbox" :ref=pause id="pause" name="set-name" class="switch-input"> <label for="pause" class="switch-label"></label> </div> </div> <div class='setting'> <div class='setting-header'> <svg :ref=arrowsHelp cmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#aaa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <div class='setting-title'>Arrows</div> </div> <div class='setting-action'> <input type="checkbox" :ref=arrows id="arrows" name="set-name" class="switch-input"> <label for="arrows" class="switch-label"></label> </div> </div> </div> <div :ref=settingsContainer class="btn-group settings"> <button :ref=settings class="btn"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg> </button> </div> <!-- settings --> <input :ref=replace type="file" class="shell-input"> <!-- PUBLISH/PREVIEW ACTIONS --> <div style="position: absolute; bottom:1em; right:1em; display:flex;"> <!-- PREVIEW --> <div :ref=preview style="color:#717171; background:#dcdcdc;" class="${this._isDefaultImage() ? "disabled" : ""} de-widget-item de-publish"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg> <p :ref=previewText style="padding-left:8px; font-size:14px;" class="de-widget-item-text">Preview</p> </div> <!-- PREVIEW --> <!-- PUBLISH --> <div style="margin-left: 1em;" :ref=publish class="de-widget-item de-publish"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-save"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg> <p :ref=publishText style="padding-left:8px; font-size:14px;" class="de-widget-item-text">Save</p> <!-- SPINER --> <div :ref=publishSpinner style="top:8px; width:100px; position:absolute; transform: scale(0.4, 0.4); width: 100px; display:none" class="loader-inner ball-pulse"> <div></div> <div></div> <div></div> </div> <!-- SPINER --> </div> <!-- PUBLISH --> <div> <!-- PUBLISH/PREVIEW ACTIONS --> </div> `); const { slideEditorShellContainer, addInput, add, edit, replace, rm, settingsContainer, preview, duration, start, random, pause, speed, arrows, effect, visible, URL, caption, left, right, done, cancel, select, skip } = html; boundary.appendChild(slideEditorShellContainer); log.debug(`Built editor html`) // Set slideshowsetiings log.debug(`Setting slideshow setting`) duration.value = slideSettings.pauseTime; // start.value = slideSettings.startSlide; random.checked = slideSettings.randomStart; pause.checked = slideSettings.pauseOnHover; speed.value = slideSettings.animSpeed; arrows.checked = slideSettings.nav; effect.value = slideSettings.effect; visible.checked = slideSettings.visible; log.debug(`Set slideshow setting`) // Save in memory this._root.editorActions = Object.assign(this._root.editorActions, html); this._root.editorActions.select[_slideInitIndex].classList.add("active"); // Tooltips const toolbarTips = [ tippy(URL, { content: "URL", }), tippy(caption, { content: "Caption", }), tippy(rm, { content: "Delete", }), tippy(edit, { content: "Crop", }), tippy(add, { content: "Add", }), tippy(right, { content: "Move Right", }), tippy(left, { content: "Move Left", }), ] const settingsTips = [ tippy(html.durationHelp, { content: "This changes the length of time the slide shows for, before automatically moving to the next slide.", theme: "light-border", }), tippy(html.speedHelp, { content: "This setting changes the length of time the effect takes when moving between slides.", theme: "light-border", }), tippy(html.effectHelp, { content: "This setting changes the effect used when moving between slides.", theme: "light-border", }), tippy(html.visibleHelp, { content: "This controls whether to show the Slideshow on your store.", theme: "light-border", }), tippy(html.arrowsHelp, { content: "Checking this option will allow the customer to manually move back and forth through the slideshow if they wish. Unchecking it will remove that navigation. In either case, the slideshow will still automatically progress according to your other settings.", theme: "light-border", }), tippy(html.hoverPauseHelp, { content: "This will pause the slideshow when the customer hovers over the image, and continue once they move their mouse off. A pointer hovering over a slide usually indicates interest in that slide, and the customer wants or needs more time to look at it.", theme: "light-border", }), tippy(html.randomHelp, { content: "Checking this will always start the slideshow on a random slide when first loaded. The slideshow will continue from that point, and loop around to the beginning once it reaches the end.", theme: "light-border", }), ] tippy(settingsContainer, { content: "Settings", delay: 350, }) const cropActionTips = [ tippy(done, { content: "Crop Image", delay: 350, }), tippy(skip, { content: "Skip Crop", delay: 350, }), tippy(cancel, { content: "Cancel", delay: 350, }) ] createSingleton(cropActionTips, { delay: 350, moveTransition: 'transform 0.2s ease-out', updateDuration: 200, }) createSingleton(toolbarTips, { delay: 350, moveTransition: 'transform 0.2s ease-out', updateDuration: 200, }) createSingleton(settingsTips, { delay: 350, moveTransition: 'transform 0.2s ease-out', updateDuration: 200, }) // Events this._on(html.publish, "click", (e) => this._publish(e)); this._on(html.skip, "click", (e) => this._skip(e)) this._on(cancel, "click", async (e) => this._cancel(e)); this._on(done, "click", async (e) => this._done(e)); this._on(preview, "click", async (e) => this._preview(e)); this._on(replace, "change", async (e) => this._replace(e)); this._on(rm, "click", (e) => this._delete(e)); this._on( [ duration, random, pause, speed, arrows, effect, visible, ], "click", (e) => { if (e.target === visible) { if (visible.checked) { this._slide.elements.img.classList.remove('disabled') } else { this._slide.elements.img.classList.add('disabled') } } this._saveSettings() }) this._on(URL, "click", (e) => { const { captionInput, urlInput } = this._root.editorActions if (URL.classList.contains("disabled")) return if (captionInput) { this._saveCaptionText() _.rm(captionInput) this._root.editorActions.captionInput = null } if (urlInput) { this._saveUrlText() _.rm(urlInput) this._root.editorActions.urlInput = null return } // Get position let { top, left, height, width} = URL.getBoundingClientRect() const urlWidth = 170 top = top + height + 5 + window.scrollY left = left - (urlWidth / 2) + 20 // Add HTML Component const html = _.createFromTemplate(` <div style="width:${urlWidth}px; left:${left}px; top:${top}px" :ref=urlInput class="de-input de-url"> <input :ref=urlInputField style="width:${urlWidth}px;" ${this._activeSlide().url ? `value=${this._activeSlide().url}` : 'placeholder="https://google.com"'}> `) Object.assign(this._root.editorActions, html) _.inAfter(this._root.editorActions.urlInput, document.body) // Focus cursor html.urlInputField.focus() // close URL input if its active if (this._root.editorActions.urlInput.classList.contains("active")) { this._root.editorActions.urlInput.classList.remove("active"); } }); this._on(caption, "click", (e) => { const { urlInput, captionInput } = this._root.editorActions if (caption.classList.contains("disabled")) return if (urlInput) { this._saveUrlText() _.rm(urlInput) this._root.editorActions.urlInput = null } if (captionInput) { this._saveCaptionText() _.rm(captionInput) this._root.editorActions.captionInput = null return } // Get position let { top, left, height, width} = caption.getBoundingClientRect() const captionWidth = 170 top = top + height + 5 + window.scrollY left = left - (captionWidth / 2) + 20 // Add HTML Component const html = _.createFromTemplate(` <div style="width:${captionWidth}px; left:${left}px; top:${top}px" :ref=captionInput class="active de-input de-caption"> <input :ref=captionInputField style="width:${captionWidth}px;" ${this._activeSlide().text ? `value=${this._activeSlide().text}` : 'placeholder="Your Caption Here"'}> `) Object.assign(this._root.editorActions, html) _.inAfter(this._root.editorActions.captionInput, document.body) // Focus cursor html.captionInputField.focus() // close URL input if its active if (this._root.editorActions.captionInput.classList.contains("active")) { this._root.editorActions.captionInput.classList.remove("active"); } }); this._on(left, "click", (e) => { log.debug("MOVE:START", this._slides) const { select } = this._root.editorActions; // Get index of selected image const activeIndex = this.activeSlideIndex(); const newIndex = activeIndex - 1; // Prevent clicking too far left if (newIndex === -1) return; // Update init index this._slideInitIndex = newIndex; // Give active class to selected img select.forEach((s) => s.classList.remove("active")); select[newIndex].classList.add("active"); // Set the order on the image select.forEach((s, i) => this._slides[i].sequence); // Reorder slides _.arrayMove(this._slides, activeIndex, newIndex); // Reindex slide sequence this._slides.forEach((s, i) => { // Index starts @ 1 :( s.sequence = i + 1; }); log.debug("MOVE:END", this._slides) }); this._on(right, "click", (e) => { log.debug("MOVE:START", this._slides) const { select } = this._root.editorActions; // Get index of selected image const activeIndex = this.activeSlideIndex(); const newIndex = activeIndex + 1; // Prevent clicking too far right if (newIndex === select.length) return; // Update init index this._slideInitIndex = newIndex; // Give active class to selected img select.forEach((s) => s.classList.remove("active")); select[newIndex].classList.add("active"); // Set the order on the image select.forEach((s, i) => this._slides[i].sequence); // Reorder slides _.arrayMove(this._slides, activeIndex, newIndex); // Reindex slide sequence this._slides.forEach((s, i) => { // Index starts @ 1 :( s.sequence = i + 1; }); log.debug("MOVE:END", this._slides) }); this._on(settingsContainer, "click", (e) => { const el = document.querySelector(".slideshow-settings-panel"); if (el.style.display === "none" || el.style.display === "") { el.style.display = "block"; } else { el.style.display = "none"; } }); this._on(add, "click", (e) => addInput.click()); // https://stackoverflow.com/a/12102992/2263032 this._on(addInput, "click", function(){ this.value = null; }) this._on(addInput, "change", async (e) => this._addSlide(e)); this._on(edit, "click", async (e) => this._edit(e)); this._on(select, "click", async (e) => this._select(e)); } _saveCaptionText(){ const { captionInput } = this._root.editorActions if (!captionInput) return const activeIndex = this.activeSlideIndex(); const caption = captionInput.querySelector( "input" ).value; this._slides[activeIndex].text = _.slug(caption) } // Save url text _saveUrlText(){ const { urlInput } = this._root.editorActions if (!urlInput) return const activeIndex = this.activeSlideIndex(); const url = urlInput.querySelector( "input" ).value; this._slides[activeIndex].url = _.slug(url) } _delete(e) { log.debug("DELETE:START", this._slides); if (this._root.editorActions.rm.classList.contains("disabled")) return return Swal.fire({ animation: false, title: "Are you sure?", text: "You won't be able to revert this!", icon: "warning", showCancelButton: true, confirmButtonColor: "#20b7e6", cancelButtonColor: "#ea1636", confirmButtonText: "Yes, delete it!", }).then((result) => { if (!result.value) return const { edit, preview, rm, URL, left, right, selectContainer, caption } = this._root.editorActions const oldIndex = this._root.editorActions.select.findIndex((x) => x.classList.contains("active") ); let newIndex = 0; // Use default image if _slides have been deleted if (this._slides.length === 1) { this._slides[oldIndex].imageUrl = "https://d13z1xw8270sfc.cloudfront.net/origin/477866/plain-white-background_1592919904955.jpg"; // Disable editor actions [left, right, selectContainer, edit, preview, rm, URL, caption].forEach(x => x.classList.add("disabled")) } // Remove from // - slides array // - select array // - select html else { _.rm(this._root.editorActions.select[oldIndex]); this._slides.splice(oldIndex, 1); this._root.editorActions.select.splice(oldIndex, 1); // Move SELECT gallery left one place // // FROM // x - x - o - x // ^ // ' // // TO // x - o - x - x // ^ // ' if (oldIndex > 0) { newIndex = oldIndex - 1; } } if (this._slides.length < 2) { [left, right, selectContainer].forEach(x => x.classList.add("disabled")) } this._root.editorActions.select[newIndex].classList.add("active"); const url = this._slides[newIndex].imageUrl; this._slide.bind({ url }); this._slideInitIndex = newIndex; log.debug("DELETE:END", this._slides); }); } async _edit(e) { log.debug("EDIT:START", this._slide) const { preview, edit, selectMoveContainer, captionInput, editorSpinner, urlInput, publish, settingsPanel, cropActions, toolbarContainer, moveContainer, selectContainer, settingsContainer, replace } = this._root.editorActions const { zoomerWrap, overlay, boundary } = this._slide.elements; if (edit.classList.contains("disabled")) return // Loader this._slide.elements.img.classList.add('sleep') // Save points incase the user decides to cancel this._slide.backup = this._slide.get() if (captionInput) { // Save text const activeIndex = this.activeSlideIndex(); this._slides[activeIndex].text = this._root.editorActions.captionInput.querySelector( "input" ).value; _.rm(captionInput) this._root.editorActions.captionInput = null } if (urlInput) { // Save text const activeIndex = this.activeSlideIndex(); this._slides[activeIndex].url = this._root.editorActions.urlInput.querySelector( "input" ).value; _.rm(urlInput) this._root.editorActions.urlInput = null } this._hideEditorActions() this.spinner.start() // Wait between 0.6s-1.0s await new Promise(r => setTimeout(r, Math.floor(Math.random()*1000+600))) this._slide.elements.img.classList.remove('sleep') // Enable interaction zoomerWrap.style.opacity = "1"; zoomerWrap.style.display = "block"; overlay.style.pointerEvents = "auto"; this.spinner.stop() // Show DONE and CANCEL cropActions.style.opacity="1" cropActions.style.zIndex="100" cropActions.style.visibility="visible" log.debug("EDIT:END", this._slide) } async _select(e){ log.debug("SELECT:START", this._slides[newIndex]); const { urlInput, captionInput, } = this._root.editorActions if (captionInput) { // Save text const activeIndex = this.activeSlideIndex(); this._slides[activeIndex].text = this._root.editorActions.captionInput.querySelector( "input" ).value; _.rm(captionInput) this._root.editorActions.captionInput = null } if (urlInput) { // Save text const activeIndex = this.activeSlideIndex(); this._slides[activeIndex].url = this._root.editorActions.urlInput.querySelector( "input" ).value; _.rm(urlInput) this._root.editorActions.urlInput = null } // Get index of selected image var newIndex = Array.from(this._root.editorActions.select).indexOf( e.target ); this._slideInitIndex = newIndex; // Give active class to selected img this._root.editorActions.select.forEach((s) => s.classList.remove("active") ); this._root.editorActions.select[newIndex].classList.add("active"); // Bind image to editor const url = this._slides[newIndex].imageUrl; await this._slide.bind({ url }); log.debug("SELECT:END", this._slides[newIndex]); } _saveSettings() { const { duration, start, random, pause, speed, arrows, effect, visible } = this._root.editorActions log.debug("SETTINGS:START", this.options) Object.assign(this.options, { animSpeed: speed.value, effect: effect.value, nav: arrows.checked, pauseOnHover: pause.checked, pauseTime: duration.value, randomStart: random.checked, // startSlide: start.value ? start.value : 0, visible: visible.checked ? 1 : 0, }); log.debug("SETTINGS:END", this.options) return this.options } _onError(err) { this._emit("error", err) } /** * Add event(s) to element(s). * @param elements DOM-Elements * @param events Event names * @param fn Callback * @param options Optional options * @return Array passed arguments */ _on = this._listen.bind(this, "addEventListener"); _listen(method, elements, events, fn, options = {}) { // Normalize array if (elements instanceof HTMLCollection || elements instanceof NodeList) { elements = Array.from(elements); } else if (!Array.isArray(elements)) { elements = [elements]; } if (!Array.isArray(events)) { events = [events]; } for (const el of elements) { for (const ev of events) { el[method](ev, this._handleError(fn), { capture: false, ...options }); } } return Array.prototype.slice.call(arguments, 1); } _handleError(func) { const that = this return function() { try { Promise.resolve( func.apply(this, arguments) ).catch(err =>{ that._emit("error", err) }) } catch(err) { that._emit("error", err) } } } _isDefaultImage() { return this._slides && this._slides.length && this._slides[0].imageUrl.indexOf('plain-white-background') > -1 } async _addSlide(e) { log.debug("ADD:START", e) if (e.target.files?.length === 0) return; const file = e.target.files[0] e = null const { left, right, urlInput, captionInput, start, select, selectContainer, edit, rm, preview, URL, caption } = this._root.editorActions // Loader this._slide.elements.img.classList.add('sleep') this.spinner.start() // Save other image meta data if (captionInput) { this._saveCaptionText() _.rm(captionInput) this._root.editorActions.captionInput = null } if (urlInput) { // Save text const activeIndex = this.activeSlideIndex(); this._slides[activeIndex].url = this._root.editorActions.urlInput.querySelector( "input" ).value; _.rm(urlInput) this._root.editorActions.urlInput = null } // Slide limit if (this._slides.length >= this._slideLimit) { this._emit("error", `Reached slide limit of ${this._slideLimit}!`); return; } const url = window.URL.createObjectURL(file); // ONLY add slide if its NOT the DEFAULT image if (!this._isDefaultImage()) { // Add gallery item to SELECT const html = _.createFromTemplate(`<div :ref=select class="select-item"></div>`) _.inAfter(html.select, selectContainer) this._on(html.select, "click", async (e) => this._select(e)); let i = select.push(html.select) this._slides.push({ imageUrl: url, }); // Set next active slide on SELECT html const active = this.activeSlideIndex() const next = active + 1 select[active].classList.remove('active') select[next].classList.add('active') // Move slide next to the active one in _slides const last = this._slides.length - 1 const ls = this._slides _.arrayMove(ls, last, next) this._slideInitIndex = next; } else { // Enable editor actions [edit, preview, rm, URL, caption, left, right, selectContainer].forEach(x => x.classList.remove("disabled")) } if (this._slides.length > 1) { [selectContainer, left, right].forEach(x => x.classList.remove("disabled")) } // less than 1mb? wait a bit if (file.size < 1000000) { await new Promise(r => setTimeout(r, Math.floor(Math.random()*500+300))) } // Bind image to editor await this._slide.bind(url); // Save edited photo const blob = await this._slide.result({ type: "blob" }); const img = window.URL.createObjectURL(blob); this._slides[this._slideInitIndex].imageUrl = img; // Hide loader this._slide.elements.img.classList.remove('sleep') this.spinner.stop() log.debug("ADD:END", this._activeSlide()) } _destroyImageEditor() { Object.keys(this._root.editorActions).forEach((key) => { if (Array.isArray(this._root.editorActions[key])) { this._root.editorActions[key].forEach((el, i) => { _.rm(this._root.editorActions[key][i]); delete this._root.editorActions[key][i]; }); // reindex array this._root.editorActions[key] = Array.from( this._root.editorActions[key] ).filter(Boolean); delete this._root.editorActions[key]; } else { _.rm(this._root.editorActions[key]); delete this._root.editorActions[key]; } }); this._root.editorActions = {}; } async _buildImageEditor() { const { namespace, _slideInitIndex, } = this; const { slideshowTarget } = this.options // Destory any previous croppie as we can only init one at a time if (this._slide) { this._slide.destroy(); this._slide = null; } // Build Croppie image dimentions const url = this._slides[_slideInitIndex].imageUrl; var { offsetWidth } = slideshowTarget.parentElement; const { width, height } = await _.dimen(url); const aspect = _.aspect(width, height, offsetWidth, offsetWidth); const opt = { width: aspect.width, height: aspect.height, target: slideshowTarget, url, }; this._slide = await this._buildCroppieInstance(opt); var el = _.createFromTemplate(` <div :obj="editorActions"> <div class="${namespace}-editor-toolbar" :ref=container> <!-- DONE --> <div class="done" :ref=done> <button :ref=done class="${namespace}-toolbar-actions-item"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg> </button> </div> <!-- DONE --> <!-- MODIFY --> <div :ref=modify class="modify"> <!-- REPLACE --> <button :ref=cancel class="${namespace}-toolbar-actions-item"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> </button> <input :ref="input" type="file" style="display:none;"/> <!-- REPLACE --> </div> <!-- MODIFY --> </div> </div> `); // Ref this._root.editorActions = Object.assign( this._root.editorActions, el.editorActions ); const { done, cancel } = this._root.editorActions; // Tooltips tippy(done, { content: "Done", delay: 350, }); tippy(cancel, { content: "Cancel", delay: 350 }); // Events this._on(cancel, "click", async (e) => this._cancel(e)); this._on(done, "click", async (e) => this._done(e)); slideshowTarget.appendChild(el.editorActions.container); } _buildStyles() { var { selectedColours, css, el } = this._style; if (!selectedColours) return; // Compile new style sheet var compiled_style; selectedColours.forEach(function (clr) { var regex = new RegExp("\\[" + clr.name + "\\]", "g"); var hex = clr.hex.trim(); if (hex.startsWith("#")) { hex = hex.slice(1); } if (compiled_style) { compiled_style = compiled_style.replace(regex, hex); } else { compiled_style = css.replace(regex, hex); } }); // Append the compiled style sheet var css = document.querySelector(el); if (!css) { _.addStyle(el, compiled_style); } else { _.rm(css); _.addStyle(el, compiled_style); } } _buildColourPicker(e) { if (this._pickr) { this._pickr.destroyAndRemove(); } // Create the colour pickr var pickr = new Pickr({ el: e.target, theme: "nano", padding: 10, default: this._style.initColour, useAsButton: true, // showAlways: true, autoReposition: false, components: { preview: true, opacity: true, hue: true, }, }); pickr.show(); // pickr._root.app.style.display = 'none' // // Reposition the pickr to where the click occured. // var { top, left } = this._style.rectangle // top = top + 30 // left = left - 93 // setTimeout(function(){ // var arrow = document.createElement('div') // arrow.className = 'pcr-tooltip-arrow' // pickr._root.app.appendChild(arrow) // pickr._root.app.style.top = top.toFixed(3) + 'px' // pickr._root.app.style.left = left.toFixed(3) + 'px' // pickr._root.app.style.display = 'block' // }, 50) this._pickr = pickr; // Append the colour picker in our container // this._root.widget.reset.insertAdjacentElement( 'beforebegin', this._pickr._root.app) // Reposition the pickr // this._pickr._root.app.style.paddingLeft = "20px"; // this._pickr._root.app.style.position = "static"; // Build the custom style sheet when a colour is picked this._pickr.on("changestop", (instance) => { // Get index of active colour var index = this._style.activeColourIndex; if (typeof index !== "undefined") { // Get selected colour hex var hex = instance._color.toHEXA().toString(3); // Update the colour palette on the page this._root.colour[index].style.backgroundColor = hex; // Update the colour swatch this._style.selectedColours[index].hex = hex; // TODO: Push only colours to the publish array that have changed. // Build styles this._buildStyles(); } else { throw "No Active Colour Selected!"; } }); } // Returns // - 0 if has not slides // - the active _slide object if _slides has length _activeSlide(){ return this._slides && this._slides.length && this._slides[this._slideInitIndex] } async _cancel(){ log.debug('START:CANCEL', this._slides) const { editorSpinner } = this._root.editorActions // Hide done, cancel actions this._hideCropActions() // Wait between 0.6s-1.0s await new Promise(r => setTimeout(r, Math.floor(Math.random()*300+200))) // Reset points if (this._slide.backup) { const { url } = this._slide.data const { points, zoom, orientation } = this._slide.backup await this._slide.bind({ url, points, orientation, zoom }) } // Show toolbar, publish settings tools this._showEditorActions() log.debug('END:CANCEL', this._slides) } _showEditorActions(){ const { preview, moveContainer, publish, toolbarContainer, selectMoveContainer, selectContainer, settingsContainer, replace } = this._root.editorActions // Show toolbar elements on page toolbarContainer.style.opacity="1" toolbarContainer.style.zIndex="100" selectContainer.style.opacity="1" settingsContainer.style.opacity="1" settingsContainer.style.zIndex="100" publish.style.opacity="1" replace.style.display="block" selectMoveContainer.style.visibility="visible" selectMoveContainer.style.opacity="1" selectMoveContainer.style.zIndex="100" moveContainer.style.opacity="1" preview.style.opacity="1" } _hideEditorActions(){ const { preview, edit, selectMoveContainer, captionInput, editorSpinner, urlInput, publish, settingsPanel, cropActions, toolbarContainer, moveContainer, selectContainer, settingsContainer, replace } = this._root.editorActions // Hide elements on page toolbarContainer.style.opacity="0" toolbarContainer.style.zIndex="0" selectContainer.style.opacity="0" selectContainer.style.zIndex="0" settingsContainer.style.opacity="0" settingsContainer.style.zIndex="0" replace.style.display="none" settingsPanel.style.display="none" publish.style.opacity="0" selectMoveContainer.style.opacity="0" selectMoveContainer.style.zIndex="0" moveContainer.style.opacity="0" preview.style.opacity="0" } _hideCropActions(){ const { zoomerWrap, overlay } = this._slide.elements; const { cropActions } = this._root.editorActions // Hide DONE and CANCEL cropActions.style.opacity="0" cropActions.style.zIndex="0" // Disable interaction zoomerWrap.style.opacity = "0"; overlay.style.pointerEvents = "none"; } async _buildCroppieInstance({ width, height, target, url, mouseWheelZoom = true, disabled = false }) { const { boundaryHeight, boundaryWidth } = this.options var c = new Croppie(target, { viewport: { width: boundaryWidth, height: boundaryHeight }, boundary: { width: boundaryWidth, height: boundaryHeight }, showZoomer: true, mouseWheelZoom: mouseWheelZoom, enableResize: true, disabled: disabled }); var options = { url: url, zoom: 0, }; await c.bind(options); return c; } _buildCroppieGalleryToolbar() { var namespace = this.namespace; var images = this._slides; var ctnr = this._root.resize.gallery; if (ctnr) { ctnr.container.parentNode.removeChild(ctnr.container); } // Append the toolbox gallery html var html = createFromTemplate(` <div :obj=gallery> <div :ref=container> <div class="${namespace}-gallery-toolbar"> ${images.map((img, i) => `<div :arr=item data-seq=${i}>${i}</div>`).join("")} </div> </div> </div> `); this._root.resize.gallery = html.gallery; document .querySelector(".cr-boundary") .append(this._root.resize.gallery.container); // Rebind selected gallery image to main slide this._on(this._root.resize.gallery.item, "click", (e) => { var i = e.target.dataset.seq; var image = this._slides[i]; this._slide.bind(image); e.target.classList.add("active"); }); } _emit(event, ...args) { this._eventListener[event].forEach((cb) => cb(...args, this)); } on(event, cb) { // Validate if ( typeof cb === "function" && typeof event === "string" && event in this._eventListener ) { this._eventListener[event].push(cb); } return this; } } export default Editor; // async function getThemeColours(storeId) { // return fetch("Editor/svc/Services.svc/GetThemeColours?storeId=" + storeId) // .then((res) => res.json()) // .then((json) => JSON.parse(json).d); // } async function getSlideshowImages(storeId) { if (isDev){ return Promise.resolve() } else { return fetch("Editor/svc/Services.svc/GetSlideShowImages?storeId=" + storeId) .then((res) => res.json()) .then((json) => JSON.parse(json)) .catch(err => log.error("Something went wrong in getSlideshowImages. Reason:", err)) } // return fetch("Editor/svc/Services.svc/GetSlideShowImages?storeId=" + storeId) // .then((res) => res.json()) // .then((json) => JSON.parse(json)); } var s; const storeId = g_fws_sk; var str = JSON.stringify; var prs = JSON.parse; function handleError(err) { log.error("Failed to handleResponse. ", err); } function handleResponse({ request, response }) { return new Promise((resolve, reject) => { const responseHeaders = {}; for (var pair of response.headers.entries()) { responseHeaders[pair[0]] = pair[1]; } if (response.status !== 200) { return Promise.all([ // Try get text // response.text().then((r) => { // return Promise.resolve(r); // }) // .catch(e => { // return Promise.resolve('Failed to parse TEXT response.' + e) // }), // Try get JSON response.json() .then(r => { return Promise.resolve('JSON response: ' + r) }) .catch(e => { return Promise.resolve('Failed to parse JSON response.' + e) }) ]).then((res) => { const msg = "Failed to make request. Request headers: " + str(request) + ". Response headers: " + str(responseHeaders) + ". Response message: " + res; reject(msg); }); } else { log.debug( "Success! Request headers: " + str(request) + ". Response headers: " + str(responseHeaders) ); resolve(responseHeaders) } }); } function init(event) { let mammothBoundaryWidth = 900 if (document.querySelector(".slider-wrapper.theme-default")) { mammothBoundaryWidth = document.querySelector(".slider-wrapper.theme-default").offsetWidth } const configurations = { "storebuilder/89137/electron": { storeId: storeId, slideshowTarget: document.querySelector("#main-content .row"), flex: document.querySelector("#slider"), }, "storebuilder/284203/ritz": { storeId: storeId, slideshowTarget:document.querySelector(".nivo_container"), flex: document.querySelector("#slider"), boundaryHeight: 372, boundaryWidth: 960 }, "storebuilder/309223/mammoth": { storeId: storeId, slideshowTarget: document.querySelector(".slider-wrapper.theme-default"), flex: document.querySelector("#slider"), boundaryHeight: 387, // TODO: use the slider width for mammoth until the slider is responsiv boundaryWidth: mammothBoundaryWidth }, }; var allowed = [ "storebuilder/309223/mammoth", "storebuilder/284203/ritz", "storebuilder/89137/electron", ]; let template; if (localStorage && localStorage.getItem("template")) { template = localStorage.getItem("template"); } // Only run fow allowed templates if (allowed.indexOf(template) === -1) { log.debug("Template [%s] is not in allowed list", template); return; } // Index of designs dont need logging, so they are set as isDev if (localStorage && localStorage.getItem("isDev")) { isDev = localStorage.getItem("isDev") === "true" } window.log = log = new Logger({ debugEnabled: true, storeId: g_fws_sk, isDev: isDev, uuid: _.uuid(), }); log.debug(`isDev: [${isDev}], Template: [${template}]`); if (navigator && navigator.userAgent) { log.debug("User agent:", navigator.userAgent); } Promise.all([getSlideshowImages(storeId)]).then( function ([images]) { log.debug("Loaded slides:", images && images.slides && images.slides.length); const conf = Object.assign(configurations[template], images) log.debug("Initializing Editor with options:", conf) window.editor = new Editor(conf); editor .on("init", (e) => log.debug("INIT", e)) .on("warn", (e) => log.warn("WARN", e)) .on("error", function (e) { log.error("EDITOR_ERROR", e); Swal.fire({ icon: "error", title: "Oops...", html: e + "<br><br>" + "[DEBUG_ID] " + log._uuid, animation: false, confirmButtonColor: "#f054a7", }); }) } ).catch(err => { Swal.fire({ icon: "error", title: "Oops...", html: err, // footer: "<a href>Why do I have this issue?</a>", animation: false, confirmButtonColor: "#f054a7", }); log.error("Failed to run getSlideshowImages. Reason:", err); }); } // Initial Editor, attach advanced logging document.addEventListener("DOMContentLoaded", function () { init(); }); window.addEventListener('unhandledrejection', function(event) { const err = "[GLOBAL_ERROR]: " + event.reason log.error(err); Swal.fire({ icon: "error", title: "DEBUG_ID: ["+log._uuid+"]", html: err, animation: false, confirmButtonColor: "#f054a7", }); event.preventDefault(); });