Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save sambarrowclough/27f9321ae4fb6b521268d00cf53db29e to your computer and use it in GitHub Desktop.
Save sambarrowclough/27f9321ae4fb6b521268d00cf53db29e to your computer and use it in GitHub Desktop.
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();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment