@etsoo/editor
Version:
ETSOO Free WYSIWYG HTML Editor
1,185 lines (1,183 loc) • 68 kB
JavaScript
'use strict';
var shared = require('@etsoo/shared');
var EOEditorHistory = require('../classes/EOEditorHistory.js');
var ImageUtils = require('../ImageUtils.js');
var EOImageEditorLabels = require('./EOImageEditorLabels.js');
var EOPalette = require('./EOPalette.js');
var EOPopup = require('./EOPopup.js');
let fabric;
/**
* EOEditor Image Editor separator
*/
const EOImageEditorSeparator = {
name: "s",
icon: "",
label: ""
};
const embossMatrix = [1, 1, 1, 1, 0.7, -1, -1, -1, -1];
const sharpenMatrix = [0, -1, 0, -1, 5, -1, 0, -1, 0];
const cropPath = '<path d="M7,17V1H5V5H1V7H5V17A2,2 0 0,0 7,19H17V23H19V19H23V17M17,15H19V7C19,5.89 18.1,5 17,5H9V7H17V15Z" />';
/**
* EOEditor Image Editor
* http://fabricjs.com/docs/fabric.Canvas.html
*/
class EOImageEditor extends HTMLElement {
/**
* Canvas
*/
canvas;
/**
* Fabric canvas
*/
fc;
/**
* Main image
*/
image;
/**
* Current active object
*/
activeObject;
/**
* Complete callback
*/
callback;
/**
* Popup
*/
popup;
/**
* Fonts
*/
fonts = ["Arial", "Helvetica", "Simsun"];
/**
* Modal div
*/
modalDiv;
// Is small screen
xs;
// Color palette
palette;
// Container
container;
// Toolbar
toolbar;
// Icons
icons;
// PNG format
pngFormat;
// Mover div
mover;
// Settings div
settings;
// History
history;
// Redo/undo button
redo;
undo;
fcSize;
containerRect;
rect;
originalWidth;
originalHeight;
_labels;
/**
* Labels
*/
get labels() {
return this._labels;
}
/**
* Panel color
*/
get panelColor() {
return this.getAttribute("panelColor");
}
set panelColor(value) {
if (value)
this.setAttribute("panelColor", value);
else
this.removeAttribute("panelColor");
}
/**
* Language
*/
get language() {
return this.getAttribute("language");
}
set language(value) {
if (value)
this.setAttribute("language", value);
else
this.removeAttribute("language");
}
constructor() {
super();
this.hidden = true;
const xs = window.innerWidth < 480;
this.xs = xs;
const template = document.createElement("template");
template.innerHTML = `
<style id="style"></style>
<div class="modal">
<div class="close-button">${this.createSVG('<path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22C17.53,22 22,17.53 22,12C22,6.47 17.53,2 12,2M14.59,8L12,10.59L9.41,8L8,9.41L10.59,12L8,14.59L9.41,16L12,13.41L14.59,16L16,14.59L13.41,12L16,9.41L14.59,8Z" />')}</div>
<div class="container"><canvas></canvas></div>
<div class="settings"></div>
<div class="toolbar">
<div class="icons"></div>
<div class="panels">
<div class="size-indicator">0 x 0</div>
<div class="move-panel"><div class="mover"></div></div>
</div>
</div>
<eo-popup></eo-popup>
<eo-palette></eo-palette>
</div>
<div class="wrapper"></div>
`;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.appendChild(template.content);
this.loadStyles(shadowRoot.getElementById("style"));
this.popup = shadowRoot.querySelector("eo-popup");
this.palette = shadowRoot.querySelector("eo-palette");
this.modalDiv = shadowRoot.querySelector("div.modal");
const clickHandler = (target) => {
if (this.activeObject == null)
this.showSettings("");
else if (target instanceof Node && target.nodeName === "DIV") {
this.fc?.discardActiveObject();
this.fc?.renderAll();
}
};
this.container = this.modalDiv.querySelector(".container");
this.container.addEventListener("click", (e) => {
clickHandler(e.target);
});
this.container.addEventListener("touchstart", (e) => {
clickHandler(e.target);
}, { passive: true });
this.canvas = shadowRoot.querySelector("canvas");
this.toolbar = this.modalDiv.querySelector("div.toolbar");
this.icons = this.toolbar.querySelector(".icons");
this.mover = this.modalDiv.querySelector("div.mover");
this.settings =
this.modalDiv.querySelector("div.settings");
this.container.addEventListener("scroll", () => {
if (this.fcSize == null ||
this.containerRect == null ||
this.rect == null)
return;
const [w, h] = this.fcSize;
const top = this.container.scrollTop;
if (top === 0) {
this.mover.style.top = "0px";
}
else {
const t = (this.rect.height * top) / h;
this.mover.style.top = `${t}px`;
}
const left = this.container.scrollLeft;
if (left === 0) {
this.mover.style.left = "0px";
}
else {
const l = (this.rect.width * left) / w;
this.mover.style.left = `${l}px`;
}
});
const adjustMover = (clientX, clientY) => {
if (this.rect == null || this.fcSize == null)
return;
// Event.offsetX will be the mover, not the mover container
const offsetX = clientX - this.rect.left;
const offsetY = clientY - this.rect.top;
const w = parseFloat(this.mover.style.width);
const h = parseFloat(this.mover.style.height);
const [fw, fh] = this.fcSize;
let nl;
if (offsetX + w / 2 >= this.rect.width) {
nl = this.rect.width - 2 - w;
}
else {
nl = offsetX - w / 2;
if (nl < 0)
nl = 0;
}
this.mover.style.left = `${nl}px`;
this.container.scrollLeft = (fw * nl) / this.rect.width;
let nt;
if (offsetY + h / 2 >= this.rect.height) {
nt = this.rect.height - 2 - h;
}
else {
nt = offsetY - h / 2;
if (nt < 0)
nt = 0;
}
this.mover.style.top = `${nt}px`;
this.container.scrollTop = (fh * nt) / this.rect.height;
};
const p = this.mover.parentElement;
p.addEventListener("mousedown", (event) => {
this.preventEvent(event);
adjustMover(event.clientX, event.clientY);
});
p.addEventListener("mousemove", (event) => {
if (event.buttons !== 1)
return;
this.preventEvent(event);
adjustMover(event.clientX, event.clientY);
});
const touchHandler = (event) => {
this.preventEvent(event);
const x = event.touches.item(0)?.clientX;
const y = event.touches.item(0)?.clientY;
if (x == null || y == null)
return;
adjustMover(x, y);
};
p.addEventListener("touchstart", touchHandler, { passive: true });
p.addEventListener("touchmove", touchHandler, { passive: true });
// document.fonts maybe not available in some browsers, like jsdom
document.fonts?.ready.then((value) => {
value.forEach((v) => {
if (!this.fonts.includes(v.family))
this.fonts.push(v.family);
});
});
}
async loadStyles(style) {
const styles = await Promise.resolve().then(function () { return require('./EOImageEditor.css.js'); });
style.innerHTML = styles.default;
this.style.setProperty("--height", `${this.xs ? 160 : 120}px`);
}
preventEvent(event) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
}
async connectedCallback() {
// Load the library dynamically
fabric = await import('fabric');
// https://github.com/fabricjs/fabric.js/issues/3319
// Change the padding logic to include background-color
fabric.Textbox.prototype.set({
_getNonTransformedDimensions() {
return new fabric.Point(this.width, this.height).scalarAdd(this.padding);
},
_calculateCurrentDimensions() {
// Controls dimensions
return this._getNonTransformedDimensions().transform(this.getViewportTransform(), true);
}
});
this.hidden = true;
this.createCommands();
if (this.panelColor) {
this.style.setProperty("--color-panel", this.panelColor);
}
window.addEventListener("resize", this.onResize.bind(this));
window.addEventListener("keydown", this.onKeypress.bind(this));
}
disconnectedCallback() {
this.hidden = true;
window.removeEventListener("resize", this.onResize.bind(this));
window.removeEventListener("keydown", this.onKeypress.bind(this));
}
onKeypress(event) {
if (this.activeObject) {
const keys = shared.Keyboard.Keys;
if (event.key === keys.Delete) {
this.preventEvent(event);
this.doAction("delete");
return;
}
else {
const change = [0, 0];
if (event.key === keys.ArrowLeft) {
change[0] = -1;
}
else if (event.key === keys.ArrowRight) {
change[0] = 1;
}
else if (event.key === keys.ArrowUp) {
change[1] = -1;
}
else if (event.key === keys.ArrowDown) {
change[1] = 1;
}
else {
return;
}
this.preventEvent(event);
const left = this.activeObject.left ?? 0;
const top = this.activeObject.top ?? 0;
this.activeObject.left = left + change[0];
this.activeObject.top = top + change[1];
this.fc?.renderAll();
}
}
}
onResize() {
this.updateSize();
}
createCommands() {
const language = this.language ?? window.navigator.language;
EOImageEditorLabels.EOImageEditorGetLabels(language).then((l) => {
this._labels = l;
this.palette.applyLabel = l.ok;
const commands = [
{
name: "undo",
icon: '<path d="M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z" />',
label: l.undo
},
{
name: "redo",
icon: '<path d="M18.4,10.6C16.55,9 14.15,8 11.5,8C6.85,8 2.92,11.03 1.54,15.22L3.9,16C4.95,12.81 7.95,10.5 11.5,10.5C13.45,10.5 15.23,11.22 16.62,12.38L13,16H22V7L18.4,10.6Z" />',
label: l.redo
},
EOImageEditorSeparator,
{
name: "zoomIn",
icon: '<path d="M15.5,14L20.5,19L19,20.5L14,15.5V14.71L13.73,14.43C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.43,13.73L14.71,14H15.5M9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14M12,10H10V12H9V10H7V9H9V7H10V9H12V10Z" />',
label: l.zoomIn
},
{
name: "zoomOut",
icon: '<path d="M15.5,14H14.71L14.43,13.73C15.41,12.59 16,11.11 16,9.5A6.5,6.5 0 0,0 9.5,3A6.5,6.5 0 0,0 3,9.5A6.5,6.5 0 0,0 9.5,16C11.11,16 12.59,15.41 13.73,14.43L14,14.71V15.5L19,20.5L20.5,19L15.5,14M9.5,14C7,14 5,12 5,9.5C5,7 7,5 9.5,5C12,5 14,7 14,9.5C14,12 12,14 9.5,14M7,9H12V10H7V9Z" />',
label: l.zoomOut
},
EOImageEditorSeparator,
{
name: "rotateLeft",
icon: '<path d="M13,4.07V1L8.45,5.55L13,10V6.09C15.84,6.57 18,9.03 18,12C18,14.97 15.84,17.43 13,17.91V19.93C16.95,19.44 20,16.08 20,12C20,7.92 16.95,4.56 13,4.07M7.1,18.32C8.26,19.22 9.61,19.76 11,19.93V17.9C10.13,17.75 9.29,17.41 8.54,16.87L7.1,18.32M6.09,13H4.07C4.24,14.39 4.79,15.73 5.69,16.89L7.1,15.47C6.58,14.72 6.23,13.88 6.09,13M7.11,8.53L5.7,7.11C4.8,8.27 4.24,9.61 4.07,11H6.09C6.23,10.13 6.58,9.28 7.11,8.53Z" />',
label: l.rotateLeft
},
{
name: "rotateRight",
icon: '<path d="M16.89,15.5L18.31,16.89C19.21,15.73 19.76,14.39 19.93,13H17.91C17.77,13.87 17.43,14.72 16.89,15.5M13,17.9V19.92C14.39,19.75 15.74,19.21 16.9,18.31L15.46,16.87C14.71,17.41 13.87,17.76 13,17.9M19.93,11C19.76,9.61 19.21,8.27 18.31,7.11L16.89,8.53C17.43,9.28 17.77,10.13 17.91,11M15.55,5.55L11,1V4.07C7.06,4.56 4,7.92 4,12C4,16.08 7.05,19.44 11,19.93V17.91C8.16,17.43 6,14.97 6,12C6,9.03 8.16,6.57 11,6.09V10L15.55,5.55Z" />',
label: l.rotateRight
},
EOImageEditorSeparator,
{
name: "text",
icon: '<path d="M18.5,4L19.66,8.35L18.7,8.61C18.25,7.74 17.79,6.87 17.26,6.43C16.73,6 16.11,6 15.5,6H13V16.5C13,17 13,17.5 13.33,17.75C13.67,18 14.33,18 15,18V19H9V18C9.67,18 10.33,18 10.67,17.75C11,17.5 11,17 11,16.5V6H8.5C7.89,6 7.27,6 6.74,6.43C6.21,6.87 5.75,7.74 5.3,8.61L4.34,8.35L5.5,4H18.5Z" />',
label: l.text
},
{
name: "image",
icon: '<path d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13.96,12.29L11.21,15.83L9.25,13.47L6.5,17H17.5L13.96,12.29Z" />',
label: l.image
},
{
name: "crop",
icon: cropPath,
label: l.crop
},
{
name: "filter",
icon: '<path d="M12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16M18.7,12.4C18.42,12.24 18.13,12.11 17.84,12C18.13,11.89 18.42,11.76 18.7,11.6C20.62,10.5 21.69,8.5 21.7,6.41C19.91,5.38 17.63,5.3 15.7,6.41C15.42,6.57 15.16,6.76 14.92,6.95C14.97,6.64 15,6.32 15,6C15,3.78 13.79,1.85 12,0.81C10.21,1.85 9,3.78 9,6C9,6.32 9.03,6.64 9.08,6.95C8.84,6.75 8.58,6.56 8.3,6.4C6.38,5.29 4.1,5.37 2.3,6.4C2.3,8.47 3.37,10.5 5.3,11.59C5.58,11.75 5.87,11.88 6.16,12C5.87,12.1 5.58,12.23 5.3,12.39C3.38,13.5 2.31,15.5 2.3,17.58C4.09,18.61 6.37,18.69 8.3,17.58C8.58,17.42 8.84,17.23 9.08,17.04C9.03,17.36 9,17.68 9,18C9,20.22 10.21,22.15 12,23.19C13.79,22.15 15,20.22 15,18C15,17.68 14.97,17.36 14.92,17.05C15.16,17.25 15.42,17.43 15.7,17.59C17.62,18.7 19.9,18.62 21.7,17.59C21.69,15.5 20.62,13.5 18.7,12.4Z" />',
label: l.filter
},
EOImageEditorSeparator,
{
name: "hcenter",
icon: '<path d="M19,16V13H23V11H19V8L15,12L19,16M5,8V11H1V13H5V16L9,12L5,8M11,20H13V4H11V20Z" />',
label: l.hcenter
},
{
name: "vcenter",
icon: '<path d="M8,19H11V23H13V19H16L12,15L8,19M16,5H13V1H11V5H8L12,9L16,5M4,11V13H20V11H4Z" />',
label: l.vcenter
},
{
name: "bringToFront",
icon: '<path d="M2,2H16V16H2V2M22,8V22H8V18H10V20H20V10H18V8H22Z" />',
label: l.bringToFront
},
{
name: "bringToBack",
icon: '<path d="M2,2H16V16H2V2M22,8V22H8V18H18V8H22M4,4V14H14V4H4Z" />',
label: l.bringToBack
},
{
name: "delete",
icon: '<path d="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z" />',
label: l.delete
},
EOImageEditorSeparator,
{
name: "preview",
icon: '<path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" />',
label: l.preview
},
{
name: "complete",
icon: '<path d="M0.41,13.41L6,19L7.41,17.58L1.83,12M22.24,5.58L11.66,16.17L7.5,12L6.07,13.41L11.66,19L23.66,7M18,7L16.59,5.58L10.24,11.93L11.66,13.34L18,7Z" />',
label: l.complete
}
];
// For small screens
if (this.xs) {
for (let i = commands.length - 1; i >= 0; i--) {
if (commands[i].name === "s") {
commands.splice(i, 1);
}
}
}
const html = commands
.map((c) => c.name === "s"
? '<div class="separator"></div>'
: `<button name="${c.name}" title="${c.label}">${c.name === "image"
? '<input id="imageFile" type="file" multiple accept="image/*" style="position: absolute; left: -2px; top: -2px; width: 40px; height: 32px; opacity: 0">'
: ""}${this.createSVG(c.icon)}</button>`)
.join("") +
`<label><input type="checkbox" name="pngFormat">PNG</label>`;
this.icons.innerHTML = html;
this.pngFormat = this.toolbar.querySelector('input[name="pngFormat"]');
this.icons.querySelectorAll("button").forEach((b) => {
b.addEventListener("click", () => {
this.doAction(b.name, b);
});
if (b.name === "redo")
this.redo = b;
else if (b.name === "undo")
this.undo = b;
});
const loadFile = (file) => {
if (!file.type.startsWith("image/"))
return;
shared.DomUtils.fileToDataURL(file).then((data) => {
fabric.FabricImage.fromURL(data).then((image) => {
const imageState = {
title: l.image,
action: () => {
//image.lockUniScaling = true;
image.setControlsVisibility({
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false // middle right
});
this.fc?.add(image);
},
undo: () => this.fc?.remove(image)
};
imageState.action();
this.fc?.setActiveObject(image);
this.history?.pushState(imageState);
});
});
};
const fileInput = this.icons.querySelector('input[type="file"]');
if (fileInput) {
fileInput.addEventListener("click", () => {
fileInput.value = "";
});
fileInput.addEventListener("change", () => {
const files = fileInput.files;
if (files == null || files.length === 0)
return;
for (let file of files) {
loadFile(file);
}
});
}
const closeDiv = this.modalDiv.querySelector(".close-button");
if (closeDiv) {
closeDiv.title = l.close;
closeDiv.addEventListener("click", () => this.reset());
}
});
}
createSVG(path, size = 24) {
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24">${path}</svg>`;
}
clear() {
if (this.fc == null)
return;
this.fc.remove(...this.fc
.getObjects("rect")
.filter((r) => Reflect.get(r, "name") === "crop"));
this.fc.remove(...this.fc
.getObjects("i-text")
.filter((t) => t instanceof fabric.IText && t.text?.trim() === ""));
this.fc.discardActiveObject();
}
setCursor(cursor = "default") {
const f = this.fc;
if (f == null)
return;
if (f.defaultCursor === cursor)
return;
f.defaultCursor = cursor;
if (this.image)
this.image.hoverCursor = cursor;
}
_textInput = false;
get textInput() {
return this._textInput;
}
set textInput(value) {
this._textInput = value;
if (value)
this.setCursor("text");
else
this.setCursor();
}
doAction(name, b) {
const fc = this.fc;
if (fc == null)
return;
const o = this.activeObject ?? this.image;
const l = this.labels;
if (["image", "crop", "filter"].includes(name)) {
this.setCursor();
this.textInput = false;
}
switch (name) {
case "bringToBack":
if (o == null)
return;
const bringToBackState = {
title: l.bringToBack,
action: () => fc.sendObjectBackwards(o, true),
undo: () => fc.bringObjectForward(o, true)
};
bringToBackState.action();
this.history?.pushState(bringToBackState);
break;
case "bringToFront":
if (o == null)
return;
const bringToFrontState = {
title: l.bringToBack,
action: () => fc.bringObjectForward(o, true),
undo: () => fc.sendObjectBackwards(o, true)
};
bringToFrontState.action();
this.history?.pushState(bringToFrontState);
break;
case "complete":
this.clear();
const data = fc.toDataURL({
format: this.pngFormat?.checked ? "png" : "jpeg",
quality: 1,
multiplier: 1
});
if (data) {
if (this.callback)
this.callback(data);
}
this.reset();
break;
case "crop":
if (o?.type === "rect" && Reflect.get(o, "name") === "crop") {
// Size
const { width, height, left = 0, top = 0 } = o;
if (width == null || height == null)
return;
// Cache sizes
const sizes = [
fc.getWidth(),
fc.getHeight(),
this.originalWidth,
this.originalHeight
];
const cropState = {
title: l.crop,
action: () => {
const zoom = fc.getZoom();
const scaleX = o.scaleX ?? 1;
const scaleY = o.scaleY ?? 1;
// Take the rect border into account
// Otherwise the saved image will have the mask borders
const nw = Math.floor(width * zoom * scaleX) - 1;
const nh = Math.floor(height * zoom * scaleY) - 1;
const nl = Math.ceil(left * zoom) + 1;
const nt = Math.ceil(top * zoom) + 1;
// Apply
// https://stackoverflow.com/questions/44437734/image-clipping-with-visible-overflow-in-fabricjs/44454016#44454016
fc.clipPath = o;
// Size
fc.setWidth(nw);
fc.setHeight(nh);
fc.absolutePan(new fabric.Point(nl, nt));
this.originalWidth = width * scaleX;
this.originalHeight = height * scaleY;
this.updateSize();
fc.remove(o);
},
undo: () => {
fc.clipPath = undefined;
// Size
fc.width = sizes[0];
fc.height = sizes[1];
fc.absolutePan(new fabric.Point(0, 0));
this.originalWidth = sizes[2];
this.originalHeight = sizes[3];
this.updateSize();
}
};
cropState.action();
this.history?.pushState(cropState);
}
else {
this.cropSettings();
}
break;
case "delete":
const objs = fc.getActiveObjects();
if (objs) {
const deleteState = {
title: `${l.delete}`,
action: () => fc.remove(...objs),
undo: () => fc.add(...objs)
};
deleteState.action();
this.history?.pushState(deleteState);
}
break;
case "filter":
if (o instanceof fabric.FabricImage)
this.filterSettings(o);
break;
case "hcenter":
if (o) {
const hZoom = fc.getZoom() ?? 1;
if (hZoom === 1) {
fc.centerObjectH(o);
}
else {
const hCenter = (fc.width / hZoom - (o.width ?? 0) * (o.scaleX ?? 1)) / 2;
o.left = hCenter;
}
}
break;
case "preview":
this.clear();
const pData = fc.toDataURL({
format: this.pngFormat?.checked ? "png" : "jpeg",
quality: 1,
multiplier: 1
});
if (pData) {
const win = window.open();
if (win) {
const img = win.document.createElement("img");
img.src = pData;
win.document.body.appendChild(img);
win.document.title = this.labels.preview;
}
}
break;
case "redo":
this.history?.forward();
break;
case "rotateLeft":
if (o) {
const rotateLeftState = {
title: l.rotateLeft,
action: () => this.doRotate(o, -90),
undo: () => this.doRotate(o, 90)
};
rotateLeftState.action();
this.history?.pushState(rotateLeftState);
}
break;
case "rotateRight":
if (o) {
const rotateRightState = {
title: l.rotateRight,
action: () => this.doRotate(o, 90),
undo: () => this.doRotate(o, -90)
};
rotateRightState.action();
this.history?.pushState(rotateRightState);
}
break;
case "text":
this.textInput = true;
this.textSettings();
break;
case "undo":
this.history?.back();
break;
case "vcenter":
if (o) {
const vZoom = fc.getZoom();
if (vZoom === 1) {
fc.centerObjectV(o);
}
else {
const vCenter = (fc.height / vZoom - (o.height ?? 0) * (o.scaleY ?? 1)) / 2;
o.top = vCenter;
}
}
break;
case "zoomIn":
const zi = (fc.getZoom() + 0.1).toExact();
if (zi > 10)
return;
const zoomInState = {
title: `${l.zoomIn}: ${zi}`,
action: () => {
fc.setZoom(zi);
this.updateZoomSize();
},
undo: () => {
fc.setZoom(zi - 0.1);
this.updateZoomSize();
}
};
zoomInState.action();
this.history?.pushState(zoomInState);
break;
case "zoomOut":
const zo = (fc.getZoom() - 0.1).toExact();
if (zo <= 0.1)
return;
const zoomOutState = {
title: `${l.zoomOut}: ${zo}`,
action: () => {
fc.setZoom(zo);
this.updateZoomSize();
},
undo: () => {
fc.setZoom(zo + 0.1);
this.updateZoomSize();
}
};
zoomOutState.action();
this.history?.pushState(zoomOutState);
break;
}
fc.renderAll();
}
updateZoomSize() {
const fc = this.fc;
if (fc == null || this.originalWidth == null || this.originalHeight == null)
return;
const zoom = fc.getZoom();
fc.setWidth(this.originalWidth * zoom);
fc.setHeight(this.originalHeight * zoom);
this.updateSize();
}
findFilter(item, name) {
const type = item.type;
if (type === "Convolute") {
const matrix = item.matrix;
if (name === "Emboss" && matrix === embossMatrix)
return true;
if (name === "Sharpen" && matrix === sharpenMatrix)
return true;
return false;
}
return type === name;
}
cropSettings() {
const fname = "crop";
if (this.isSettingShowing(fname))
return;
const layout = ["*", "1:1", "2:1", "3:2", "4:3", "5:4", "7:5", "16:9"]
.map((r) => `<button class="vflex">${this.createSVG(cropPath, 20)}<span>${r}</span></button>`)
.join("");
this.showSettings(layout, fname, "flex");
this.settings
.querySelectorAll("button")
.forEach((button) => {
button.addEventListener("click", () => {
if (this.fc == null)
return;
// Size
const zoom = this.fc.getZoom();
if (this.fc.width == null || this.fc.height == null)
return;
const width = (this.fc.width / zoom).toExact(0);
const height = (this.fc.height / zoom).toExact(0);
// Ratio
let rText = button.querySelector("span")?.innerText;
if (rText == null)
return;
let rw, rh, rl, rt;
const custom = rText === "*";
const rItems = custom
? [1, 1]
: rText.split(":").map((i) => parseFloat(i));
const w = rItems[0];
const h = rItems[1];
if (w / h > width / height) {
// More height
rw = width;
rl = 0;
rh = ((rw * h) / w).toExact(0);
rt = (height - rh) / 2;
}
else {
// More width
rh = height;
rt = 0;
rw = ((rh * w) / h).toExact(0);
rl = (width - rw) / 2;
}
if (this.fc.clipPath) {
rl += this.fc.clipPath.left ?? 0;
rt += this.fc.clipPath.top ?? 0;
}
// http://jsfiddle.net/a7mad24/aPLq5/
const rect = new fabric.Rect({
width: rw,
height: rh,
left: rl,
top: rt,
fill: "#fff",
opacity: 0.2,
//fill: 'transparent',
//stroke: '#ff0000',
//strokeDashArray: [5, 5],
name: "crop"
});
if (custom) ;
else {
//rect.lockUniScaling = true;
rect.setControlsVisibility({
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false // middle right
});
}
this.fc.add(rect);
this.fc.bringObjectToFront(rect);
this.fc.setActiveObject(rect);
// Scroll to here
this.container.scrollTop = rt;
this.container.scrollLeft = rl;
this.showSettings("");
});
});
}
filterSettings(o) {
const fname = "filter";
if (this.isSettingShowing(fname))
return;
const filters = o.filters ?? [];
const fd = [
{ name: "Grayscale" },
{ name: "Invert" },
{ name: "Brownie" },
{ name: "Vintage" },
{ name: "Kodachrome" },
{ name: "Technicolor" },
{ name: "Polaroid" },
{ name: "Sharpen" },
{ name: "Emboss" },
{
name: "Brightness",
value: [-1, 1, 0.2]
},
{
name: "Saturation",
value: [0, 1, 0.1]
},
{
name: "Contrast",
value: [-1, 1, 0.2]
},
{
name: "Vibrance",
value: [-1, 1, 0.2]
},
{
name: "HueRotation",
value: [-1, 1, 0.2],
property: "rotation"
},
{
name: "Blur",
value: [0, 1, 0.1]
},
{
name: "Noise",
value: [0, 400, 20]
},
{
name: "Pixelate",
value: [1, 20, 1],
property: "blocksize"
}
];
const l = this.labels;
const layout = fd
.map((f) => {
const filter = filters.find((item) => this.findFilter(item, f.name));
const v = f.value;
const n = shared.Utils.formatInitial(f.name, false);
return `<label${v == null ? ' class="span2"' : ""}><input type="checkbox"${filter == null ? "" : " checked"} name="${f.name}"/>${Reflect.get(l, n)}</label>${v == null
? ""
: ` <input type="range" data-property="${f.property ?? ""}" name="${f.name}-value" min="${v[0]}" max="${v[1]}" step="${v[2]}"${filter == null
? " disabled"
: ` value="${Reflect.get(filter, f.property ?? n) ?? ""}"`}/>`}`;
})
.join("");
this.showSettings(layout, fname, "form");
const f = fabric.filters;
this.settings
.querySelectorAll("input")
.forEach((input) => {
input.addEventListener("input", () => {
let name = input.name;
let property;
let value = null;
let checked;
if (name.endsWith("-value")) {
name = name.substring(0, name.length - 6);
value = input.valueAsNumber;
property = input.dataset["property"];
checked = true;
}
else {
checked = input.checked;
const valueInput = this.settings.querySelector(`input[name="${name}-value"]`);
if (valueInput) {
valueInput.disabled = !checked;
property = valueInput.dataset["property"];
value = valueInput.valueAsNumber;
}
}
const fi = filters.findIndex((item) => this.findFilter(item, name));
let filter = fi === -1 ? undefined : filters[fi];
if (checked) {
if (filter == null) {
if (name === "Emboss") {
filter = new f.Convolute({
matrix: embossMatrix
});
}
else if (name === "Sharpen") {
filter = new f.Convolute({
matrix: sharpenMatrix
});
}
else {
const fc = Reflect.get(f, name);
filter = new fc();
}
if (o.filters == null)
o.filters = [filter];
else
o.filters.push(filter);
}
if (value != null) {
const p = property ? property : shared.Utils.formatInitial(name, false);
Reflect.set(filter, p, value);
}
}
else {
filters.splice(fi, 1);
}
o.applyFilters();
this.fc?.renderAll();
});
});
}
imageSettings(o) {
const layout = `<label>${this.labels?.opacity} <input type="range" name="opacity" value="${o.opacity}" min="0.1" max="1" step="0.1"/></label>`;
this.showSettings(layout, "image", "flex");
const opacityInput = this.settings.querySelector('input[name="opacity"]');
opacityInput?.addEventListener("input", () => {
o.opacity = opacityInput.valueAsNumber;
this.fc?.renderAll();
});
}
getTextSettings() {
if (this.isSettingShowing("text", false)) {
const shadowColorInput = this.settings.querySelector('input[name="shadowColor"]');
let shadow;
const color = shadowColorInput?.value;
if (color) {
const offsetX = this.settings.querySelector('input[name="shadowOffsetX"]')?.valueAsNumber ?? 1;
const offsetY = this.settings.querySelector('input[name="shadowOffsetY"]')?.valueAsNumber ?? 1;
const blur = this.settings.querySelector('input[name="shadowOffsetBlur"]')?.valueAsNumber ?? 0;
shadow = new fabric.Shadow({
color,
offsetX,
offsetY,
blur
});
}
return {
fontFamily: this.settings.querySelector('select[name="fontFamily"]')?.value,
fontWeight: this.settings.querySelector('input[name="fontWeight"]')?.valueAsNumber,
opacity: this.settings.querySelector('input[name="opacity"]')?.valueAsNumber,
padding: this.settings.querySelector('input[name="padding"]')?.valueAsNumber,
fill: this.settings
.querySelector('input[name="fill"]')
?.value.trim(),
backgroundColor: this.settings
.querySelector('input[name="bgColor"]')
?.value.trim(),
fontStyle: (this.settings.querySelector('input[name="italic"]')?.checked
? "italic"
: "normal"),
underline: this.settings.querySelector('input[name="underline"]')?.checked,
linethrough: this.settings.querySelector('input[name="linethrough"]')?.checked,
shadow
};
}
return undefined;
}
textSettings(o) {
const l = this.labels;
let shadow = o?.shadow
? typeof o.shadow === "string"
? new fabric.Shadow(o.shadow)
: o.shadow
: undefined;
const shadowLayout = o
? `<label><span>${l.shadow}:</span><input type="text" name="shadowColor" title="${l.color}" value="${shadow?.color ?? ""}"/>
</label><input type="range" name="shadowOffsetX" title="${l.offsetX}" value="${shadow?.offsetX ?? 1}" min="-10" max="10" step="1"/>
<input type="range" name="shadowOffsetY" title="${l.offsetY}" value="${shadow?.offsetY ?? 1}" min="-10" max="10" step="1"/>
<input type="range" name="shadowBlur" title="${l.blur}" value="${shadow?.blur ?? 0}" min="0" max="15" step="1"/>`
: "";
const layout = `<label><span>${l.fontFamily}:</span><select name="fontFamily">${this.fonts
.sort()
.map((f) => `<option>${f}</option>`)
.join("")}</select>
</label><label><span>${l.fontWeight}:</span><input type="range" name="fontWeight" value="${o?.fontWeight ?? 100}" min="100" max="1000" step="100"/></label>
<label><span>${l.opacity}:</span><input type="range" name="opacity" value="${o?.opacity ?? 1}" min="0.1" max="1" step="0.1"/></label>
<label><span>${l.padding}:</span><input type="number" name="padding" value="${o?.padding ?? 0}" min="0" max="100" step="1"/></label>
<label><span>${l.color}:</span><input type="text" name="fill" value="${o?.fill ?? "#000"}"/></label>
<label><span>${l.bgColor}:</span><input type="text" name="bgColor" value="${o?.backgroundColor ?? ""}"/></label>
<label><input type="checkbox" name="italic"${o?.fontStyle === "italic" ? " checked" : ""}/>${l.italic}</label>
<label><input type="checkbox" name="underline"${o?.underline ? " checked" : ""}/>${l.underline}</label>
<label><input type="checkbox" name="linethrough"${o?.linethrough ? " checked" : ""}/>${l.strikethrough}</label>${shadowLayout}`;
this.showSettings(layout, "text", "flex");
const fontFamilySelect = this.settings.querySelector('select[name="fontFamily"]');
const fontWeightInput = this.settings.querySelector('input[name="fontWeight"]');
const opacityInput = this.settings.querySelector('input[name="opacity"]');
const paddingInput = this.settings.querySelector('input[name="padding"]');
const italicInput = this.settings.querySelector('input[name="italic"]');
const underlineInput = this.settings.querySelector('input[name="underline"]');
const linethroughInput = this.settings.querySelector('input[name="linethrough"]');
const shadowColorInput = this.settings.querySelector('input[name="shadowColor"]');
if (o) {
if (fontFamilySelect) {
if (o.fontFamily)
fontFamilySelect.value = o.fontFamily;
fontFamilySelect.addEventListener("change", () => {
o.fontFamily = fontFamilySelect.value;
this.fc?.renderAll();
});
}
fontWeightInput?.addEventListener("input", () => {
o.fontWeight = fontWeightInput.valueAsNumber;
this.fc?.renderAll();
});
opacityInput?.addEventListener("input", () => {
o.opacity = opacityInput.valueAsNumber;
this.fc?.renderAll();
});
paddingInput?.addEventListener("input", () => {
o.padding = paddingInput.valueAsNumber;
this.fc?.renderAll();
});
italicInput?.addEventListener("change", () => {
o.fontStyle = italicInput.checked ? "italic" : "normal";
this.fc?.renderAll();
});
underlineInput?.addEventListener("change", () => {
//o.underline = underlineInput.checked;
o.set("underline", underlineInput.checked);
this.fc?.renderAll();
});
linethroughInput?.addEventListener("change", () => {
// o.linethrough = linethroughInput.checked;
o.set("linethrough", linethroughInput.checked);
this.fc?.renderAll();
});
if (shadowColorInput) {
this.palette.setupInput(shadowColorInput);
shadowColorInput.addEventListener("change", () => {
shadowColorInput.dispatchEvent(new Event("input"));
});
const shadowOffsetXInput = this.settings.querySelector('input[name="shadowOffsetX"]');
const shadowOffsetYInput = this.settings.querySelector('input[name="shadowOffsetY"]');
const shadowBlurInput = this.settings.querySelector('input[name="shadowBlur"]');
[
shadowColorInput,
shadowOffsetXInput,
shadowOffsetYInput,
shadowBlurInput
].forEach((input) => {
input?.addEventListener("input", () => {
const color = shadowColorInput.value.trim();
if (!color)
return;
if (shadow == null) {
shadow = new fabric.Shadow({
color,
offsetX: shadowOffsetXInput?.valueAsNumber,
offsetY: shadowOffsetYInput?.valueAsNumber,
blur: shadowBlurInput?.valueAsNumber
});
o.shadow = shadow;
}
else {
const name = input.name;
switch (name) {
case "shadowOffsetX":
shadow.offsetX = shadowOffsetXInput?.valueAsNumber ?? 0;
break;
case "shadowOffsetY":
shadow.offsetY = shadowOffsetYInput?.valueAsNumber ?? 0;
break;
case "shadowBlur":
shadow.blur = shadowBlurInput?.valueAsNumber ?? 0;
break;
default:
shadow.color = color;
break;
}
}
this.fc?.renderAll();
});
});
}
}
const fillInput = this.settings.querySelector('input[name="fill"]');
if (fillInput) {
this.palette.setupInput(fillInput);
if (o) {
fillInput.addEventListener("change", () => {
o.set("fill", fillInput.value);
this.fc?.renderAll();
});
}
}
const bgColorInput = this.settings.querySelector('input[name="bgColor"]');
if (bgColorInput) {
this.palette.setupInput(bgColorInput);
if (o) {
bgColorInput.addEventListener("change", () => {
o.set("backgroundColor", bgColorInput.value);
this.fc?.renderAll();
});
}
}
}
showSettings(html, name, className) {
this.settings.innerHTML = html;
this.settings.style.visibility = html === "" ? "hidden" : "visible";
this.settings.dataset["name"] = name;
this.settings.classList.forEach((c, _k, p) => {
if (c === "settings")
return;
p.remove(c);
});
if (className) {
this.settings.classList.add(className);
}
}
isSettingShowing(name, clear = true) {
if (this.settings.dataset["name"] === name)