This commit is contained in:
Agriya Dev5
2021-05-22 13:32:01 +05:30
1048 changed files with 54098 additions and 53780 deletions

View File

@@ -6,12 +6,9 @@
* @copyright 2010 Jeff Schiller, 2010 Alexis Deveria
*/
// Dependencies:
// 1) jQuery (for $.alert())
import 'pathseg';
import {NS} from './namespaces.js';
import { NS } from './namespaces.js';
const supportsSVG_ = (function () {
return Boolean(document.createElementNS && document.createElementNS(NS.SVG, 'svg').createSVGRect);
@@ -23,14 +20,13 @@ return Boolean(document.createElementNS && document.createElementNS(NS.SVG, 'svg
*/
export const supportsSvg = () => supportsSVG_;
const {userAgent} = navigator;
const { userAgent } = navigator;
const svg = document.createElementNS(NS.SVG, 'svg');
// Note: Browser sniffing should only be used if no other detection method is possible
const isOpera_ = Boolean(window.opera);
const isWebkit_ = userAgent.includes('AppleWebKit');
const isGecko_ = userAgent.includes('Gecko/');
const isIE_ = userAgent.includes('MSIE');
const isChrome_ = userAgent.includes('Chrome/');
const isWindows_ = userAgent.includes('Windows');
const isMac_ = userAgent.includes('Macintosh');
@@ -53,7 +49,7 @@ const seg = path.createSVGPathSegLinetoAbs(5, 5);
try {
seglist.replaceItem(seg, 1);
return true;
} catch (err) {}
}catch (err) {/* empty */}
return false;
}());
@@ -65,7 +61,7 @@ const seg = path.createSVGPathSegLinetoAbs(5, 5);
try {
seglist.insertItemBefore(seg, 1);
return true;
} catch (err) {}
}catch (err) {/* empty */}
return false;
}());
@@ -165,11 +161,6 @@ export const isWebkit = () => isWebkit_;
* @returns {boolean}
*/
export const isGecko = () => isGecko_;
/**
* @function module:browser.isIE
* @returns {boolean}
*/
export const isIE = () => isIE_;
/**
* @function module:browser.isChrome
* @returns {boolean}

View File

@@ -16,6 +16,7 @@ export const NS = {
SE: 'http://svg-edit.googlecode.com',
SVG: 'http://www.w3.org/2000/svg',
XLINK: 'http://www.w3.org/1999/xlink',
OI: 'http://www.optimistik.fr/namespace/svg/OIdata',
XML: 'http://www.w3.org/XML/1998/namespace',
XMLNS: 'http://www.w3.org/2000/xmlns/' // see http://www.w3.org/TR/REC-xml-names/#xmlReserved
};
@@ -26,7 +27,7 @@ export const NS = {
*/
export const getReverseNS = function () {
const reverseNS = {};
Object.entries(NS).forEach(([name, URI]) => {
Object.entries(NS).forEach(([ name, URI ]) => {
reverseNS[URI] = name.toLowerCase();
});
return reverseNS;

View File

@@ -6,11 +6,11 @@
* @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
*/
import {NS} from './namespaces.js';
import { NS } from './namespaces.js';
const wAttrs = ['x', 'x1', 'cx', 'rx', 'width'];
const hAttrs = ['y', 'y1', 'cy', 'ry', 'height'];
const unitAttrs = ['r', 'radius', ...wAttrs, ...hAttrs];
const wAttrs = [ 'x', 'x1', 'cx', 'rx', 'width' ];
const hAttrs = [ 'y', 'y1', 'cy', 'ry', 'height' ];
const unitAttrs = [ 'r', 'radius', ...wAttrs, ...hAttrs ];
// unused
/*
const unitNumMap = {
@@ -55,7 +55,6 @@ let typeMap_ = {};
* @returns {Integer} The number of digits number should be rounded to
*/
/* eslint-disable jsdoc/valid-types */
/**
* @typedef {PlainObject} module:units.TypeMap
* @property {Float} em
@@ -68,7 +67,6 @@ let typeMap_ = {};
* @property {Integer} px
* @property {0} %
*/
/* eslint-enable jsdoc/valid-types */
/**
* Initializes this module.
@@ -204,14 +202,14 @@ export const setUnitAttr = function (elem, attr, val) {
};
const attrsToConvert = {
line: ['x1', 'x2', 'y1', 'y2'],
circle: ['cx', 'cy', 'r'],
ellipse: ['cx', 'cy', 'rx', 'ry'],
foreignObject: ['x', 'y', 'width', 'height'],
rect: ['x', 'y', 'width', 'height'],
image: ['x', 'y', 'width', 'height'],
use: ['x', 'y', 'width', 'height'],
text: ['x', 'y']
line: [ 'x1', 'x2', 'y1', 'y2' ],
circle: [ 'cx', 'cy', 'r' ],
ellipse: [ 'cx', 'cy', 'rx', 'ry' ],
foreignObject: [ 'x', 'y', 'width', 'height' ],
rect: [ 'x', 'y', 'width', 'height' ],
image: [ 'x', 'y', 'width', 'height' ],
use: [ 'x', 'y', 'width', 'height' ],
text: [ 'x', 'y' ]
};
/**
@@ -230,13 +228,8 @@ export const convertAttrs = function (element) {
for (let i = 0; i < len; i++) {
const attr = attrs[i];
const cur = element.getAttribute(attr);
if (cur) {
if (!isNaN(cur)) {
element.setAttribute(attr, (cur / typeMap_[unit]) + unit);
}
// else {
// Convert existing?
// }
if (cur && !isNaN(cur)) {
element.setAttribute(attr, (cur / typeMap_[unit]) + unit);
}
}
};
@@ -306,7 +299,7 @@ export const isValidUnit = function (attr, val, selectedElement) {
try {
const elem = elementContainer_.getElement(val);
result = (!elem || elem === selectedElement);
} catch (e) {}
} catch (e) {/* empty fn */}
return result;
}
return true;

493
src/editor/ConfigObj.js Normal file
View File

@@ -0,0 +1,493 @@
// eslint-disable-next-line node/no-unpublished-import
import deparam from 'deparam';
import { mergeDeep } from './components/jgraduate/Util.js';
/**
* Escapes special characters in a regular expression.
* @function regexEscape
* @param {string} str
* @returns {string}
*/
export const regexEscape = function (str) {
// Originally from: http://phpjs.org/functions
return String(str).replace(/[.\\+*?[^\]$(){}=!<>|:-]/g, '\\$&');
};
/**
* @class configObj
*/
export default class ConfigObj {
/**
* @param {PlainObject} editor
*/
constructor (editor) {
/**
* Preferences.
* @interface module:SVGEditor.Prefs
* @property {string} [lang="en"] Two-letter language code. The language must exist in the Editor Preferences language list. Defaults to "en" if `locale.js` detection does not detect another language.
* @property {module:SVGEditor.IconSize} [iconsize="s" || "m"] Size of the toolbar icons. Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise.
* @property {string} [bkgd_color="#FFF"] Color hex for canvas background color. Defaults to white.
* @property {string} [bkgd_url=""] Background raster image URL. This image will fill the background of the document; useful for tracing purposes.
* @property {"embed"|"ref"} [img_save="embed"] Defines whether included raster images should be saved as Data URIs when possible, or as URL references. Settable in the Document Properties dialog.
* @property {boolean} [save_notice_done=false] Used to track alert status
* @property {boolean} [export_notice_done=false] Used to track alert status
* @todo `save_notice_done` and `export_notice_done` should be changed to flags rather than preferences
*/
this.defaultPrefs = {
// EDITOR OPTIONS (DIALOG)
/**
* Default to "en" if locale.js detection does not detect another language.
*/
lang: 'en',
/**
* Will default to 's' if the window height is smaller than the minimum
* height and 'm' otherwise.
*/
iconsize: '',
bkgd_color: '#FFF',
bkgd_url: '',
// DOCUMENT PROPERTIES (DIALOG)
img_save: 'embed',
// ALERT NOTICES
// Only shows in UI as far as alert notices, but useful to remember, so keeping as pref
save_notice_done: false,
export_notice_done: false
};
/**
* @tutorial ConfigOptions
* @interface module:SVGEditor.Config
* @property {string} [canvasName="default"] Used to namespace storage provided via `ext-storage.js`; you can use this if you wish to have multiple independent instances of SVG Edit on the same domain
* @property {boolean} [no_save_warning=false] If `true`, prevents the warning dialog box from appearing when closing/reloading the page. Mostly useful for testing.
* @property {string} [imgPath="images/"] The path where the SVG icons are located, with trailing slash. Note that as of version 2.7, this is not configurable by URL for security reasons.
* @property {boolean} [preventAllURLConfig=false] Set to `true` to override the ability for URLs to set non-content configuration (including extension config).
* Must be set early, i.e., in `svgedit-config-iife.js`; extension loading is too late!
* @property {boolean} [preventURLContentLoading=false] Set to `true` to override the ability for URLs to set URL-based SVG content.
* Must be set early, i.e., in `svgedit-config-iife.js`; extension loading is too late!
* @property {boolean} [lockExtensions=false] Set to `true` to override the ability for URLs to set their own extensions; disallowed in URL setting. There is no need for this when `preventAllURLConfig` is used.
* Must be set early, i.e., in `svgedit-config-iife.js`; extension loading is too late!
* @property {boolean} [noDefaultExtensions=false] If set to `true`, prohibits automatic inclusion of default extensions (though "extensions" can still be used to add back any desired default extensions along with any other extensions).
* This can only be meaningfully used in `svgedit-config-iife.js` or in the URL
* @property {boolean} [noStorageOnLoad=false] Some interaction with `ext-storage.js`; prevent even the loading of previously saved local storage.
* @property {boolean} [forceStorage=false] Some interaction with `ext-storage.js`; strongly discouraged from modification as it bypasses user privacy by preventing them
* from choosing whether to keep local storage or not (and may be required by law in some regions)
* @property {boolean} [emptyStorageOnDecline=false] Used by `ext-storage.js`; empty any prior storage if the user declines to store
* @property {boolean} [avoidClientSide=false] DEPRECATED (use `avoidClientSideDownload` instead); Used by `ext-server_opensave.js`; set to `true` if you wish to always save to server and not only as fallback when client support is lacking
* @property {boolean} [avoidClientSideDownload=false] Used by `ext-server_opensave.js`; set to `true` if you wish to always save to server and not only as fallback when client support is lacking
* @property {boolean} [avoidClientSideOpen=false] Used by `ext-server_opensave.js`; set to `true` if you wish to always open from the server and not only as fallback when FileReader client support is lacking
* @property {string[]} [extensions=[]] Extensions to load on startup. Use an array in `setConfig` and comma separated file names in the URL.Extension names must begin with "ext-".
* Note that as of version 2.7, paths containing "/", "\", or ":", are disallowed for security reasons.
* Although previous versions of this list would entirely override the default list, as of version 2.7, the defaults will always be added to this explicit list unless the configuration `noDefaultExtensions` is included.
* See {@link module:SVGEditor~defaultExtensions}.
* @property {string[]} [allowedOrigins=[]] Used by `ext-xdomain-messaging.js` to indicate which origins are permitted for cross-domain messaging (e.g., between the embedded editor and main editor code).
* Besides explicit domains, one might add '*' to allow all domains (not recommended for privacy/data integrity of your user's content!),
* `window.location.origin` for allowing the same origin (should be safe if you trust all apps on your domain), 'null' to allow `file:///` URL usage
* @property {string} [paramurl] This was available via URL only. Allowed an un-encoded URL within the query string (use "url" or "source" with a data: URI instead)
* @property {Float} [canvas_expansion=3] The minimum area visible outside the canvas, as a multiple of the image dimensions. The larger the number, the more one can scroll outside the canvas.
* @property {PlainObject} [initFill] Init fill properties
* @property {string} [initFill.color="FF0000"] The initial fill color. Must be a hex code string. Defaults to solid red.
* @property {Float} [initFill.opacity=1] The initial fill opacity. Must be a number between 0 and 1
* @property {PlainObject} [initStroke] Init stroke properties
* @property {Float} [initStroke.width=5] The initial stroke width. Must be a positive number.
* @property {string} [initStroke.color="000000"] The initial stroke color. Must be a hex code. Defaults to solid black.
* @property {Float} [initStroke.opacity=1] The initial stroke opacity. Must be a number between 0 and 1.
* @property {PlainObject} text Text style properties
* @property {Float} [text.stroke_width=0] Text stroke width
* @property {Float} [text.font_size=24] Text font size
* @property {string} [text.font_family="serif"] Text font family
* @property {Float} [initOpacity=1] Initial opacity (multiplied by 100)
* @property {module:SVGEditor.XYDimensions} [dimensions=[640, 480]] The default width/height of a new document. Use an array in `setConfig` (e.g., `[800, 600]`) and comma separated numbers in the URL.
* @property {boolean} [gridSnapping=false] Enable snap to grid by default. Set in Editor Options.
* @property {string} [gridColor="#000"] Accepts hex, e.g., '#000'. Set in Editor Options. Defaults to black.
* @property {string} [baseUnit="px"] Set in Editor Options.
* @property {Float} [snappingStep=10] Set the default grid snapping value. Set in Editor Options.
* @property {boolean} [showRulers=true] Initial state of ruler display (v2.6). Set in Editor Options.
* @property {string} [initTool="select"] The initially selected tool. Must be either the ID of the button for the tool, or the ID without `tool_` prefix (e.g., "select").
* @property {boolean} [wireframe=false] Start in wireframe mode
* @property {boolean} [showlayers=false] Open the layers side-panel by default.
* @property {"new"|"same"} [exportWindowType="new"] Can be "new" or "same" to indicate whether new windows will be generated for each export;
* the `window.name` of the export window is namespaced based on the `canvasName` (and incremented if "new" is selected as the type). Introduced 2.8.
* @property {boolean} [showGrid=false] Set by `ext-grid.js`; determines whether or not to show the grid by default
* @property {boolean} [show_outside_canvas=true] Defines whether or not elements outside the canvas should be visible. Set and used in `svgcanvas.js`.
* @property {boolean} [selectNew=true] If true, will replace the selection with the current element and automatically select element objects (when not in "path" mode) after they are created, showing their grips (v2.6).
* Set and used in `svgcanvas.js` (`mouseUp`).
*/
this.defaultConfig = {
canvasName: 'default',
canvas_expansion: 3,
initFill: {
color: 'FF0000', // solid red
opacity: 1
},
initStroke: {
width: 5,
color: '000000', // solid black
opacity: 1
},
text: {
stroke_width: 0,
font_size: 24,
font_family: 'serif'
},
initOpacity: 1,
initTool: 'select',
exportWindowType: 'new', // 'same' (todo: also support 'download')
wireframe: false,
showlayers: false,
no_save_warning: false,
// PATH CONFIGURATION
// The following path configuration items are disallowed in the URL (as should any future path configurations)
imgPath: './images/',
// DOCUMENT PROPERTIES
// Change the following to a preference (already in the Document Properties dialog)?
dimensions: [ 640, 480 ],
// EDITOR OPTIONS
// Change the following to preferences (already in the Editor Options dialog)?
gridSnapping: false,
gridColor: '#000',
baseUnit: 'px',
snappingStep: 10,
showRulers: true,
// URL BEHAVIOR CONFIGURATION
preventAllURLConfig: false,
preventURLContentLoading: false,
// EXTENSION CONFIGURATION (see also preventAllURLConfig)
lockExtensions: false, // Disallowed in URL setting
noDefaultExtensions: false, // noDefaultExtensions can only be meaningfully used in `svgedit-config-iife.js` or in the URL
// EXTENSION-RELATED (GRID)
showGrid: false, // Set by ext-grid.js
// EXTENSION-RELATED (STORAGE)
noStorageOnLoad: false, // Some interaction with ext-storage.js; prevent even the loading of previously saved local storage
forceStorage: false, // Some interaction with ext-storage.js; strongly discouraged from modification as it bypasses user privacy by preventing them from choosing whether to keep local storage or not
emptyStorageOnDecline: false, // Used by ext-storage.js; empty any prior storage if the user declines to store
// EXTENSION (CLIENT VS. SERVER SAVING/OPENING)
avoidClientSide: false, // Deprecated in favor of `avoidClientSideDownload`
avoidClientSideDownload: false,
avoidClientSideOpen: false
};
this.curPrefs = {};
// Note: The difference between Prefs and Config is that Prefs
// can be changed in the UI and are stored in the browser,
// while config cannot
this.urldata = {};
/**
* @name module:SVGEditor~defaultExtensions
* @type {string[]}
*/
this.defaultExtensions = [
'ext-connector',
'ext-eyedropper',
'ext-grid',
'ext-imagelib',
// 'ext-arrows',
// 'ext-markers',
'ext-overview_window',
'ext-panning',
'ext-shapes',
'ext-polystar',
'ext-storage',
'ext-opensave',
// 'ext-helloworld',
];
this.curConfig = {
// We do not put on defaultConfig to simplify object copying
// procedures (we obtain instead from defaultExtensions)
extensions: [],
userExtensions: [],
/**
* Can use `location.origin` to indicate the current
* origin. Can contain a '*' to allow all domains or 'null' (as
* a string) to support all `file:///` URLs. Cannot be set by
* URL for security reasons (not safe, at least for
* privacy or data integrity of SVG content).
* Might have been fairly safe to allow
* `new URL(location.href).origin` by default but
* avoiding it ensures some more security that even third
* party apps on the same domain also cannot communicate
* with this app by default.
* For use with `ext-xdomain-messaging.js`
* @todo We might instead make as a user-facing preference.
*/
allowedOrigins: []
};
this.editor = editor;
}
/**
* @function setupCurPrefs
* @returns {void}
*/
setupCurPrefs () {
const curPrefs = { ...this.defaultPrefs, ...this.curPrefs }; // Now safe to merge with priority for curPrefs in the event any are already set
// Export updated prefs
this.curPrefs = curPrefs;
}
/**
* Sets up current config based on defaults.
* @returns {void}
*/
setupCurConfig () {
const curConfig = { ...this.defaultConfig, ...this.curConfig }; // Now safe to merge with priority for curConfig in the event any are already set
// Now deal with extensions and other array config
if (!curConfig.noDefaultExtensions) {
curConfig.extensions = [ ...this.defaultExtensions ];
}
// Export updated config
this.curConfig = curConfig;
}
/**
* @function loadFromURL Load config/data from URL if given
* @returns {void}
*/
loadFromURL () {
const { search, searchParams } = new URL(location);
if (search) {
this.urldata = deparam(searchParams.toString());
[ 'initStroke', 'initFill' ].forEach((prop) => {
if (searchParams.has(`${prop}[color]`)) {
// Restore back to original non-deparamed value to avoid color
// strings being converted to numbers
this.urldata[prop].color = searchParams.get(`${prop}[color]`);
}
});
if (searchParams.has('bkgd_color')) {
this.urldata.bkgd_color = '#' + searchParams.get('bkgd_color');
}
if (this.urldata.dimensions) {
this.urldata.dimensions = this.urldata.dimensions.split(',');
}
if (this.urldata.extensions) {
// For security reasons, disallow cross-domain or cross-folder
// extensions via URL
this.urldata.extensions = (/[:/\\]/).test(this.urldata.extensions)
? ''
: this.urldata.extensions.split(',');
}
// Disallowing extension paths via URL for
// security reasons, even for same-domain
// ones given potential to interact in undesirable
// ways with other script resources
[ 'userExtensions', 'imgPath' ]
.forEach(function (pathConfig) {
if (this.urldata[pathConfig]) {
delete this.urldata[pathConfig];
}
});
// Note: `source` and `url` (as with `storagePrompt` later) are not
// set on config but are used below
this.setConfig(this.urldata, { overwrite: false });
this.setupCurConfig();
if (!this.curConfig.preventURLContentLoading) {
let { source } = this.urldata;
if (!source) { // urldata.source may have been null if it ended with '='
const src = searchParams.get('source');
if (src && src.startsWith('data:')) {
source = src;
}
}
if (source) {
if (source.startsWith('data:')) {
this.editor.loadFromDataURI(source);
} else {
this.editor.loadFromString(source);
}
return;
}
if (this.urldata.url) {
this.editor.loadFromURL(this.urldata.url);
return;
}
}
if (!this.urldata.noStorageOnLoad || this.curConfig.forceStorage) {
this.loadContentAndPrefs();
}
} else {
this.setupCurConfig();
this.loadContentAndPrefs();
}
}
/**
* Where permitted, sets canvas and/or `configObj.defaultPrefs` based on previous
* storage. This will override URL settings (for security reasons) but
* not `svgedit-config-iife.js` configuration (unless initial user
* overriding is explicitly permitted there via `allowInitialUserOverride`).
* @function module:SVGEditor.loadContentAndPrefs
* @todo Split `allowInitialUserOverride` into `allowOverrideByURL` and
* `allowOverrideByUserStorage` so `svgedit-config-iife.js` can disallow some
* individual items for URL setting but allow for user storage AND/OR
* change URL setting so that it always uses a different namespace,
* so it won't affect pre-existing user storage (but then if users saves
* that, it will then be subject to tampering
* @returns {void}
*/
loadContentAndPrefs () {
if (!this.curConfig.forceStorage &&
(this.curConfig.noStorageOnLoad ||
!(/(?:^|;\s*)svgeditstore=(?:prefsAndContent|prefsOnly)/).test(document.cookie)
)
) {
return;
}
// LOAD CONTENT
if (this.editor.storage && // Cookies do not have enough available memory to hold large documents
(this.curConfig.forceStorage ||
(!this.curConfig.noStorageOnLoad &&
(/(?:^|;\s*)svgeditstore=prefsAndContent/).test(document.cookie))
)
) {
const name = 'svgedit-' + this.curConfig.canvasName;
const cached = this.editor.storage.getItem(name);
if (cached) {
this.editor.loadFromString(cached);
}
}
// LOAD PREFS
Object.keys(this.defaultPrefs).forEach((key) => {
const storeKey = 'svg-edit-' + key;
if (this.editor.storage) {
const val = this.editor.storage.getItem(storeKey);
if (val) {
this.defaultPrefs[key] = String(val); // Convert to string for FF (.value fails in Webkit)
}
} else if (window.widget) {
this.defaultPrefs[key] = window.widget.preferenceForKey(storeKey);
} else {
const result = document.cookie.match(
new RegExp('(?:^|;\\s*)' + regexEscape(
encodeURIComponent(storeKey)
) + '=([^;]+)')
);
this.defaultPrefs[key] = result ? decodeURIComponent(result[1]) : '';
}
});
}
/**
* Allows setting of preferences or configuration (including extensions).
* @function module:SVGEditor.setConfig
* @param {module:SVGEditor.Config|module:SVGEditor.Prefs} opts The preferences or configuration (including extensions). See the tutorial on {@tutorial ConfigOptions} for info on config and preferences.
* @param {PlainObject} [cfgCfg] Describes configuration which applies to the
* particular batch of supplied options
* @param {boolean} [cfgCfg.allowInitialUserOverride=false] Set to true if you wish
* to allow initial overriding of settings by the user via the URL
* (if permitted) or previously stored preferences (if permitted);
* note that it will be too late if you make such calls in extension
* code because the URL or preference storage settings will
* have already taken place.
* @param {boolean} [cfgCfg.overwrite=true] Set to false if you wish to
* prevent the overwriting of prior-set preferences or configuration
* (URL settings will always follow this requirement for security
* reasons, so `svgedit-config-iife.js` settings cannot be overridden unless it
* explicitly permits via `allowInitialUserOverride` but extension config
* can be overridden as they will run after URL settings). Should
* not be needed in `svgedit-config-iife.js`.
* @returns {void}
*/
setConfig (opts, cfgCfg = {}) {
/**
*
* @param {module:SVGEditor.Config|module:SVGEditor.Prefs} cfgObj
* @param {string} key
* @param {any} val See {@link module:SVGEditor.Config} or {@link module:SVGEditor.Prefs}
* @returns {void}
*/
const extendOrAdd = (cfgObj, key, val) => {
if (cfgObj[key] && typeof cfgObj[key] === 'object') {
// $.extend(true, cfgObj[key], val);
cfgObj[key] = mergeDeep(cfgObj[key], val);
} else {
cfgObj[key] = val;
}
};
Object.entries(opts).forEach(([ key, val ]) => {
// Only allow prefs defined in configObj.defaultPrefs or...
if (this.defaultPrefs[key]) {
if (cfgCfg.overwrite === false && (
this.curConfig.preventAllURLConfig ||
this.curPrefs[key])
) {
return;
}
if (cfgCfg.allowInitialUserOverride === true) {
this.defaultPrefs[key] = val;
} else {
this.pref(key, val);
}
} else if ([ 'extensions', 'userExtensions', 'allowedOrigins' ].includes(key)) {
if (cfgCfg.overwrite === false &&
(
this.curConfig.preventAllURLConfig ||
[ 'allowedOrigins' ].includes(key) ||
(key === 'extensions' && this.curConfig.lockExtensions)
)
) {
return;
}
this.curConfig[key] = this.curConfig[key].concat(val); // We will handle any dupes later
// Only allow other configObj.curConfig if defined in configObj.defaultConfig
} else if ({}.hasOwnProperty.call(this.defaultConfig, key)) {
if (cfgCfg.overwrite === false && (
this.curConfig.preventAllURLConfig ||
{}.hasOwnProperty.call(this.curConfig, key)
)) {
return;
}
// Potentially overwriting of previously set config
if ({}.hasOwnProperty.call(this.curConfig, key)) {
if (cfgCfg.overwrite === false) {
return;
}
extendOrAdd(this.curConfig, key, val);
} else if (cfgCfg.allowInitialUserOverride === true) {
extendOrAdd(this.defaultConfig, key, val);
} else if (this.defaultConfig[key] && typeof this.defaultConfig[key] === 'object') {
this.curConfig[key] = Array.isArray(this.defaultConfig[key]) ? [] : {};
this.curConfig[key] = mergeDeep(this.curConfig[key], val);
// $.extend(true, this.curConfig[key], val); // Merge properties recursively, e.g., on initFill, initStroke objects
} else {
this.curConfig[key] = val;
}
}
});
}
/**
* Store and retrieve preferences.
* @function pref
* @param {string} key The preference name to be retrieved or set
* @param {string} [val] The value. If the value supplied is missing or falsey, no change to the preference will
* be made unless `mayBeEmpty` is set.
* @param {boolean} [mayBeEmpty] If value may be falsey.
* @returns {string|void} If val is missing or falsey and `mayBeEmpty` is not set, the
* value of the previously stored preference will be returned.
* @todo Review whether any remaining existing direct references to
* getting `curPrefs` can be changed to use `svgEditor.configObj.pref()` getting to ensure
* `defaultPrefs` fallback (also for sake of `allowInitialUserOverride`);
* specifically, `bkgd_color` could be changed so that the pref dialog has a
* button to auto-calculate background, but otherwise uses `svgEditor.configObj.pref()` to
* be able to get default prefs or overridable settings
*/
pref (key, val, mayBeEmpty) {
if (mayBeEmpty || val) {
this.curPrefs[key] = val;
return undefined;
}
return (key in this.curPrefs) ? this.curPrefs[key] : this.defaultPrefs[key];
}
/**
* @function load load Config
* @returns {void}
*/
load () {
this.loadFromURL(this.editor);
this.setupCurPrefs(this.editor);
}
}

1212
src/editor/Editor.js Normal file

File diff suppressed because it is too large Load Diff

811
src/editor/EditorStartup.js Normal file
View File

@@ -0,0 +1,811 @@
/* globals $ seConfirm seAlert */
import './touch.js';
import { convertUnit } from '../common/units.js';
import {
putLocale
} from './locale.js';
import {
hasCustomHandler, getCustomHandler, injectExtendedContextMenuItemsIntoDom
} from './contextmenu.js';
import editorTemplate from './templates/editorTemplate.js';
import SvgCanvas from '../svgcanvas/svgcanvas.js';
import Rulers from './Rulers.js';
/**
* @fires module:svgcanvas.SvgCanvas#event:svgEditorReady
* @returns {void}
*/
const readySignal = () => {
// let the opener know SVG Edit is ready (now that config is set up)
const w = window.opener || window.parent;
if (w) {
try {
/**
* Triggered on a containing `document` (of `window.opener`
* or `window.parent`) when the editor is loaded.
* @event module:SVGEditor#event:svgEditorReadyEvent
* @type {Event}
* @property {true} bubbles
* @property {true} cancelable
*/
/**
* @name module:SVGthis.svgEditorReadyEvent
* @type {module:SVGEditor#event:svgEditorReadyEvent}
*/
const svgEditorReadyEvent = new w.CustomEvent('svgEditorReady', {
bubbles: true,
cancelable: true
});
w.document.documentElement.dispatchEvent(svgEditorReadyEvent);
} catch (e) {/* empty fn */}
}
};
const { $id } = SvgCanvas;
/**
*
*/
class EditorStartup {
/**
*
*/
constructor () {
this.extensionsAdded = false;
this.messageQueue = [];
this.$svgEditor = $id('svg_editor');
}
/**
* Auto-run after a Promise microtask.
* @function module:SVGthis.init
* @returns {void}
*/
async init () {
if ('localStorage' in window) { // && onWeb removed so Webkit works locally
this.storage = window.localStorage;
}
this.configObj.load();
const self = this;
const { i18next } = await putLocale(this.configObj.pref('lang'), this.goodLangs);
this.i18next = i18next;
// eslint-disable-next-line no-unsanitized/method
await import(`./components/index.js`);
// eslint-disable-next-line no-unsanitized/method
await import(`./dialogs/index.js`);
// allow to prepare the dom without display
this.$svgEditor.style.visibility = 'hidden';
try {
// add editor components to the DOM
document.body.append(editorTemplate.content.cloneNode(true));
// Image props dialog added to DOM
const newSeImgPropDialog = document.createElement('se-img-prop-dialog');
newSeImgPropDialog.setAttribute('id', 'se-img-prop');
document.body.append(newSeImgPropDialog);
newSeImgPropDialog.init(this.i18next);
// editor prefences dialoag added to DOM
const newSeEditPrefsDialog = document.createElement('se-edit-prefs-dialog');
newSeEditPrefsDialog.setAttribute('id', 'se-edit-prefs');
document.body.append(newSeEditPrefsDialog);
newSeEditPrefsDialog.init(this.i18next);
// canvas menu added to DOM
const dialogBox = document.createElement('se-cmenu_canvas-dialog');
dialogBox.setAttribute('id', 'se-cmenu_canvas');
document.body.append(dialogBox);
dialogBox.init(this.i18next);
// alertDialog added to DOM
const alertBox = document.createElement('se-alert-dialog');
alertBox.setAttribute('id', 'se-alert-dialog');
document.body.append(alertBox);
// promptDialog added to DOM
const promptBox = document.createElement('se-prompt-dialog');
promptBox.setAttribute('id', 'se-prompt-dialog');
document.body.append(promptBox);
// Export dialog added to DOM
const exportDialog = document.createElement('se-export-dialog');
exportDialog.setAttribute('id', 'se-export-dialog');
document.body.append(exportDialog);
exportDialog.init(this.i18next);
} catch (err) {
console.error(err);
}
/**
* @name module:SVGthis.canvas
* @type {module:svgcanvas.SvgCanvas}
*/
this.svgCanvas = new SvgCanvas(
$id('svgcanvas'),
this.configObj.curConfig
);
this.leftPanel.init();
this.bottomPanel.init();
this.topPanel.init();
this.layersPanel.init();
this.mainMenu.init();
const { undoMgr } = this.svgCanvas;
this.workarea = document.getElementById('workarea');
this.canvMenu = document.getElementById('se-cmenu_canvas');
this.exportWindow = null;
this.defaultImageURL = this.configObj.curConfig.imgPath + 'logo.svg';
const zoomInIcon = 'crosshair';
const zoomOutIcon = 'crosshair';
this.uiContext = 'toolbars';
// For external openers
readySignal();
this.rulers = new Rulers(this);
this.layersPanel.populateLayers();
this.selectedElement = null;
this.multiselected = false;
const aLinks = $id('cur_context_panel').querySelectorAll('a');
for (const aLink of aLinks) {
aLink.addEventListener('click', (evt) => {
const link = evt.currentTarget;
if (link.hasAttribute('data-root')) {
this.svgCanvas.leaveContext();
} else {
this.svgCanvas.setContext(link.textContent);
}
this.svgCanvas.clearSelection();
return false;
});
}
// bind the selected event to our function that handles updates to the UI
this.svgCanvas.bind('selected', this.selectedChanged.bind(this));
this.svgCanvas.bind('transition', this.elementTransition.bind(this));
this.svgCanvas.bind('changed', this.elementChanged.bind(this));
this.svgCanvas.bind('exported', this.exportHandler.bind(this));
this.svgCanvas.bind('exportedPDF', function (win, data) {
if (!data.output) { // Ignore Chrome
return;
}
const { exportWindowName } = data;
if (exportWindowName) {
this.exportWindow = window.open('', this.exportWindowName); // A hack to get the window via JSON-able name without opening a new one
}
if (!this.exportWindow || this.exportWindow.closed) {
seAlert(this.i18next.t('notification.popupWindowBlocked'));
return;
}
this.exportWindow.location.href = data.output;
}.bind(this));
this.svgCanvas.bind('zoomed', this.zoomChanged.bind(this));
this.svgCanvas.bind('zoomDone', this.zoomDone.bind(this));
this.svgCanvas.bind(
'updateCanvas',
/**
* @param {external:Window} win
* @param {PlainObject} centerInfo
* @param {false} centerInfo.center
* @param {module:math.XYObject} centerInfo.newCtr
* @listens module:svgcanvas.SvgCanvas#event:updateCanvas
* @returns {void}
*/
function (win, { center, newCtr }) {
this.updateCanvas(center, newCtr);
}.bind(this)
);
this.svgCanvas.bind('contextset', this.contextChanged.bind(this));
this.svgCanvas.bind('extension_added', this.extAdded.bind(this));
this.svgCanvas.textActions.setInputElem($id('text'));
this.setBackground(this.configObj.pref('bkgd_color'), this.configObj.pref('bkgd_url'));
// update resolution option with actual resolution
const res = this.svgCanvas.getResolution();
if (this.configObj.curConfig.baseUnit !== 'px') {
res.w = convertUnit(res.w) + this.configObj.curConfig.baseUnit;
res.h = convertUnit(res.h) + this.configObj.curConfig.baseUnit;
}
$id('se-img-prop').setAttribute('dialog', 'close');
$id('se-img-prop').setAttribute('title', this.svgCanvas.getDocumentTitle());
$id('se-img-prop').setAttribute('width', res.w);
$id('se-img-prop').setAttribute('height', res.h);
$id('se-img-prop').setAttribute('save', this.configObj.pref('img_save'));
// Lose focus for select elements when changed (Allows keyboard shortcuts to work better)
const selElements = document.querySelectorAll("select");
Array.from(selElements).forEach(function(element) {
element.addEventListener('change', function(evt) {
evt.currentTarget.blur();
});
});
// fired when user wants to move elements to another layer
let promptMoveLayerOnce = false;
$id('selLayerNames').addEventListener('change', function(evt) {
const destLayer = evt.currentTarget.options[evt.currentTarget.selectedIndex].value;
const confirmStr = self.i18next.t('notification.QmoveElemsToLayer').replace('%s', destLayer);
/**
* @param {boolean} ok
* @returns {void}
*/
const moveToLayer = (ok) => {
if (!ok) { return; }
promptMoveLayerOnce = true;
self.svgCanvas.moveSelectedToLayer(destLayer);
self.svgCanvas.clearSelection();
self.layersPanel.populateLayers();
};
if (destLayer) {
if (promptMoveLayerOnce) {
moveToLayer(true);
} else {
const ok = seConfirm(confirmStr);
if (!ok) {
return;
}
moveToLayer(true);
}
}
});
$id('tool_font_family').addEventListener('change', function(evt) {
self.svgCanvas.setFontFamily(evt.detail.value);
});
$id('seg_type').addEventListener('change', function(evt) {
self.svgCanvas.setSegType(evt.currentTarget.value);
});
function addListenerMulti(element, eventNames, listener) {
var events = eventNames.split(' ');
for (var i=0, iLen=events.length; i<iLen; i++) {
element.addEventListener(events[i], listener, false);
}
}
addListenerMulti($id('text'), 'keyup input', function(evt){
self.svgCanvas.setTextContent(evt.currentTarget.value);
});
$id('image_url').addEventListener('change', function(evt) {
self.setImageURL(evt.currentTarget.value);
});
$id('link_url').addEventListener('change', function(evt) {
if (evt.currentTarget.value.length) {
self.svgCanvas.setLinkURL(evt.currentTarget.value);
} else {
self.svgCanvas.removeHyperlink();
}
});
$id('g_title').addEventListener('change', function(evt) {
self.svgCanvas.setGroupTitle(evt.currentTarget.value);
});
const wArea = this.workarea;
let lastX = null, lastY = null,
panning = false, keypan = false;
$id('svgcanvas').addEventListener('mouseup', function(evt) {
if (panning === false) { return true; }
wArea.scrollLeft -= (evt.clientX - lastX);
wArea.scrollTop -= (evt.clientY - lastY);
lastX = evt.clientX;
lastY = evt.clientY;
if (evt.type === 'mouseup') { panning = false; }
return false;
});
// eslint-disable-next-line sonarjs/no-identical-functions
$id('svgcanvas').addEventListener('mousemove', function(evt) {
if (panning === false) { return true; }
wArea.scrollLeft -= (evt.clientX - lastX);
wArea.scrollTop -= (evt.clientY - lastY);
lastX = evt.clientX;
lastY = evt.clientY;
if (evt.type === 'mouseup') { panning = false; }
return false;
});
$id('svgcanvas').addEventListener('mousedown', function(evt) {
if (evt.button === 1 || keypan === true) {
panning = true;
lastX = evt.clientX;
lastY = evt.clientY;
return false;
}
return true;
});
window.addEventListener('mouseup', function() {
panning = false;
});
document.addEventListener('keydown', (e) => {
if (e.target.nodeName !== 'BODY') return;
if(e.code.toLowerCase() === 'space'){
this.svgCanvas.spaceKey = keypan = true;
e.preventDefault();
} else if((e.key.toLowerCase() === 'shift') && (this.svgCanvas.getMode() === 'zoom')){
this.workarea.style.cursor = zoomOutIcon;
e.preventDefault();
} else {
return;
}
});
document.addEventListener('keyup', (e) => {
if (e.target.nodeName !== 'BODY') return;
if(e.code.toLowerCase() === 'space'){
this.svgCanvas.spaceKey = keypan = false;
e.preventDefault();
} else if((e.key.toLowerCase() === 'shift') && (this.svgCanvas.getMode() === 'zoom')){
this.workarea.style.cursor = zoomInIcon;
e.preventDefault();
} else {
return;
}
});
/**
* @function module:SVGthis.setPanning
* @param {boolean} active
* @returns {void}
*/
this.setPanning = (active) => {
this.svgCanvas.spaceKey = keypan = active;
};
let inp;
/**
*
* @returns {void}
*/
const unfocus = () => {
inp.blur();
};
const liElems = document.getElementById('svg_editor').querySelectorAll('button, select, input:not(#text)');
Array.prototype.forEach.call(liElems, function(el){
el.addEventListener("focus", (e) => {
inp = e.currentTarget;
this.uiContext = 'toolbars';
this.workarea.addEventListener('mousedown', unfocus);
});
el.addEventListener("blur", () => {
this.uiContext = 'canvas';
this.workarea.removeEventListener('mousedown', unfocus);
// Go back to selecting text if in textedit mode
if (this.svgCanvas.getMode() === 'textedit') {
$id('text').focus();
}
});
});
// ref: https://stackoverflow.com/a/1038781
function getWidth() {
return Math.max(
document.body.scrollWidth,
document.documentElement.scrollWidth,
document.body.offsetWidth,
document.documentElement.offsetWidth,
document.documentElement.clientWidth
);
}
function getHeight() {
return Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
document.documentElement.clientHeight
);
}
const winWh = {
width: getWidth(),
height: getHeight()
};
window.addEventListener('resize', () => {
Object.entries(winWh).forEach(([ type, val ]) => {
const curval = (type === 'width') ? window.innerWidth - 15 : window.innerHeight;
this.workarea['scroll' + (type === 'width' ? 'Left' : 'Top')] -= (curval - val) / 2;
winWh[type] = curval;
});
});
this.workarea.addEventListener('scroll', () => {
// TODO: jQuery's scrollLeft/Top() wouldn't require a null check
this.rulers.manageScroll();
});
$id('url_notice').addEventListener('click', () => {
seAlert(this.title);
});
$id('stroke_width').value = this.configObj.curConfig.initStroke.width;
$id('opacity').value = this.configObj.curConfig.initOpacity * 100;
const elements = document.getElementsByClassName("push_button");
Array.from(elements).forEach(function(element) {
element.addEventListener('mousedown', function(event) {
if (!event.currentTarget.classList.contains('disabled')) {
event.currentTarget.classList.add('push_button_pressed');
event.currentTarget.classList.remove('push_button');
}
});
element.addEventListener('mouseout', function(event) {
event.currentTarget.classList.add('push_button');
event.currentTarget.classList.remove('push_button_pressed');
});
element.addEventListener('mouseup', function(event) {
event.currentTarget.classList.add('push_button');
event.currentTarget.classList.remove('push_button_pressed');
});
});
this.layersPanel.populateLayers();
const centerCanvas = () => {
// this centers the canvas vertically in the this.workarea (horizontal handled in CSS)
this.workarea.style.lineHeight = this.workarea.style.height;
};
addListenerMulti(window, 'load resize', centerCanvas);
// Prevent browser from erroneously repopulating fields
const inputEles = document.querySelectorAll('input');
Array.from(inputEles).forEach(function(inputEle) {
inputEle.setAttribute('autocomplete', 'off');
});
const selectEles = document.querySelectorAll('select');
Array.from(selectEles).forEach(function(inputEle) {
inputEle.setAttribute('autocomplete', 'off');
});
$id('se-svg-editor-dialog').addEventListener('change', function (e) {
if (e?.detail?.copy === 'click') {
this.cancelOverlays(e);
} else if (e?.detail?.dialog === 'closed') {
this.hideSourceEditor();
} else {
this.saveSourceEditor(e);
}
}.bind(this));
$id('se-cmenu_canvas').addEventListener('change', function (e) {
const action = e?.detail?.trigger;
switch (action) {
case 'delete':
this.svgCanvas.deleteSelectedElements();
break;
case 'cut':
this.cutSelected();
break;
case 'copy':
this.copySelected();
break;
case 'paste':
this.svgCanvas.pasteElements();
break;
case 'paste_in_place':
this.svgCanvas.pasteElements('in_place');
break;
case 'group':
case 'group_elements':
this.svgCanvas.groupSelectedElements();
break;
case 'ungroup':
this.svgCanvas.ungroupSelectedElement();
break;
case 'move_front':
this.svgCanvas.moveToTopSelectedElement();
break;
case 'move_up':
this.moveUpDownSelected('Up');
break;
case 'move_down':
this.moveUpDownSelected('Down');
break;
case 'move_back':
this.svgCanvas.moveToBottomSelectedElement();
break;
default:
if (hasCustomHandler(action)) {
getCustomHandler(action).call();
}
break;
}
}.bind(this));
// Select given tool
this.ready(function () {
const preTool = $id(`tool_${this.configObj.curConfig.initTool}`);
const regTool = $id(this.configObj.curConfig.initTool);
const selectTool = $id('tool_select');
const $editDialog = $id('se-edit-prefs');
if (preTool) {
preTool.click();
} else if (regTool) {
regTool.click();
} else {
selectTool.click();
}
if (this.configObj.curConfig.wireframe) {
$id('tool_wireframe').click();
}
$id('rulers').style.display = (this.configObj.curConfig.showRulers) ? 'block' : 'none';
if (this.configObj.curConfig.showRulers) {
$editDialog.setAttribute('showrulers', true);
}
if (this.configObj.curConfig.baseUnit) {
$editDialog.setAttribute('baseunit', this.configObj.curConfig.baseUnit);
}
if (this.configObj.curConfig.gridSnapping) {
$editDialog.setAttribute('gridsnappingon', true);
}
if (this.configObj.curConfig.snappingStep) {
$editDialog.setAttribute('gridsnappingstep', this.configObj.curConfig.snappingStep);
}
if (this.configObj.curConfig.gridColor) {
$editDialog.setAttribute('gridcolor', this.configObj.curConfig.gridColor);
}
}.bind(this));
// zoom
$id('zoom').value = (this.svgCanvas.getZoom() * 100).toFixed(1);
this.canvMenu.setAttribute('disableallmenu', true);
this.canvMenu.setAttribute('enablemenuitems', '#delete,#cut,#copy');
this.enableOrDisableClipboard();
window.addEventListener('storage', function (e) {
if (e.key !== 'svgedit_clipboard') { return; }
this.enableOrDisableClipboard();
}.bind(this));
window.addEventListener('beforeunload', function (e) {
// Suppress warning if page is empty
if (undoMgr.getUndoStackSize() === 0) {
this.showSaveWarning = false;
}
// showSaveWarning is set to 'false' when the page is saved.
if (!this.configObj.curConfig.no_save_warning && this.showSaveWarning) {
// Browser already asks question about closing the page
e.returnValue = this.i18next.t('notification.unsavedChanges'); // Firefox needs this when beforeunload set by addEventListener (even though message is not used)
return this.i18next.t('notification.unsavedChanges');
}
return true;
}.bind(this));
// Use HTML5 File API: http://www.w3.org/TR/FileAPI/
// if browser has HTML5 File API support, then we will show the open menu item
// and provide a file input to click. When that change event fires, it will
// get the text contents of the file and send it to the canvas
if (window.FileReader) {
/**
* @param {Event} e
* @returns {void}
*/
const editorObj = this;
const importImage = function (e) {
document.getElementById('se-prompt-dialog').title = editorObj.i18next.t('notification.loadingImage');
e.stopPropagation();
e.preventDefault();
const file = (e.type === 'drop') ? e.dataTransfer.files[0] : this.files[0];
if (!file) {
document.getElementById('se-prompt-dialog').setAttribute('close', true);
return;
}
if (!file.type.includes('image')) {
return;
}
// Detected an image
// svg handling
let reader;
if (file.type.includes('svg')) {
reader = new FileReader();
reader.onloadend = function (ev) {
const newElement = editorObj.svgCanvas.importSvgString(ev.target.result, true);
editorObj.svgCanvas.ungroupSelectedElement();
editorObj.svgCanvas.ungroupSelectedElement();
editorObj.svgCanvas.groupSelectedElements();
editorObj.svgCanvas.alignSelectedElements('m', 'page');
editorObj.svgCanvas.alignSelectedElements('c', 'page');
// highlight imported element, otherwise we get strange empty selectbox
editorObj.svgCanvas.selectOnly([ newElement ]);
document.getElementById('se-prompt-dialog').setAttribute('close', true);
};
reader.readAsText(file);
} else {
// bitmap handling
reader = new FileReader();
reader.onloadend = function ({ target: { result } }) {
/**
* Insert the new image until we know its dimensions.
* @param {Float} imageWidth
* @param {Float} imageHeight
* @returns {void}
*/
const insertNewImage = function (imageWidth, imageHeight) {
const newImage = editorObj.svgCanvas.addSVGElementFromJson({
element: 'image',
attr: {
x: 0,
y: 0,
width: imageWidth,
height: imageHeight,
id: editorObj.svgCanvas.getNextId(),
style: 'pointer-events:inherit'
}
});
editorObj.svgCanvas.setHref(newImage, result);
editorObj.svgCanvas.selectOnly([ newImage ]);
editorObj.svgCanvas.alignSelectedElements('m', 'page');
editorObj.svgCanvas.alignSelectedElements('c', 'page');
editorObj.topPanelHandlers.updateContextPanel();
document.getElementById('se-prompt-dialog').setAttribute('close', true);
};
// create dummy img so we know the default dimensions
let imgWidth = 100;
let imgHeight = 100;
const img = new Image();
img.style.opacity = 0;
img.addEventListener('load', () => {
imgWidth = img.offsetWidth || img.naturalWidth || img.width;
imgHeight = img.offsetHeight || img.naturalHeight || img.height;
insertNewImage(imgWidth, imgHeight);
});
img.src = result;
};
reader.readAsDataURL(file);
}
};
this.workarea.addEventListener('dragenter', this.onDragEnter);
this.workarea.addEventListener('dragover', this.onDragOver);
this.workarea.addEventListener('dragleave', this.onDragLeave);
this.workarea.addEventListener('drop', importImage);
const imgImport = document.createElement('input');
imgImport.type="file";
imgImport.addEventListener('change', importImage);
window.addEventListener('importImages', () => imgImport.click());
}
this.updateCanvas(true);
// Load extensions
this.extAndLocaleFunc();
// Defer injection to wait out initial menu processing. This probably goes
// away once all context menu behavior is brought to context menu.
this.ready(() => {
injectExtendedContextMenuItemsIntoDom();
});
// run callbacks stored by this.ready
await this.runCallbacks();
window.addEventListener('message', this.messageListener.bind(this));
}
/**
* @fires module:svgcanvas.SvgCanvas#event:ext_addLangData
* @fires module:svgcanvas.SvgCanvas#event:ext_langReady
* @fires module:svgcanvas.SvgCanvas#event:ext_langChanged
* @fires module:svgcanvas.SvgCanvas#event:extensions_added
* @returns {Promise<module:locale.LangAndData>} Resolves to result of {@link module:locale.readLang}
*/
async extAndLocaleFunc () {
this.$svgEditor.style.visibility = 'visible';
try {
// load standard extensions
await Promise.all(
this.configObj.curConfig.extensions.map(async (extname) => {
/**
* @tutorial ExtensionDocs
* @typedef {PlainObject} module:SVGthis.ExtensionObject
* @property {string} [name] Name of the extension. Used internally; no need for i18n. Defaults to extension name without beginning "ext-" or ending ".js".
* @property {module:svgcanvas.ExtensionInitCallback} [init]
*/
try {
/**
* @type {module:SVGthis.ExtensionObject}
*/
// eslint-disable-next-line no-unsanitized/method
const imported = await import(`./extensions/${encodeURIComponent(extname)}/${encodeURIComponent(extname)}.js`);
const { name = extname, init: initfn } = imported.default;
return this.addExtension(name, (initfn && initfn.bind(this)), { $, langParam: 'en' }); /** @todo change to current lng */
} catch (err) {
// Todo: Add config to alert any errors
console.error('Extension failed to load: ' + extname + '; ', err);
return undefined;
}
})
);
// load user extensions (given as pathNames)
await Promise.all(
this.configObj.curConfig.userExtensions.map(async (extPathName) => {
/**
* @tutorial ExtensionDocs
* @typedef {PlainObject} module:SVGthis.ExtensionObject
* @property {string} [name] Name of the extension. Used internally; no need for i18n. Defaults to extension name without beginning "ext-" or ending ".js".
* @property {module:svgcanvas.ExtensionInitCallback} [init]
*/
try {
/**
* @type {module:SVGthis.ExtensionObject}
*/
// eslint-disable-next-line no-unsanitized/method
const imported = await import(encodeURI(extPathName));
const { name, init: initfn } = imported.default;
return this.addExtension(name, (initfn && initfn.bind(this)), { $ });
} catch (err) {
// Todo: Add config to alert any errors
console.error('Extension failed to load: ' + extPathName + '; ', err);
return undefined;
}
})
);
this.svgCanvas.bind(
'extensions_added',
/**
* @param {external:Window} _win
* @param {module:svgcanvas.SvgCanvas#event:extensions_added} _data
* @listens module:SvgCanvas#event:extensions_added
* @returns {void}
*/
(_win, _data) => {
this.extensionsAdded = true;
this.setAll();
if (this.storagePromptState === 'ignore') {
this.updateCanvas(true);
}
this.messageQueue.forEach(
/**
* @param {module:svgcanvas.SvgCanvas#event:message} messageObj
* @fires module:svgcanvas.SvgCanvas#event:message
* @returns {void}
*/
(messageObj) => {
this.svgCanvas.call('message', messageObj);
}
);
}
);
this.svgCanvas.call('extensions_added');
} catch (err) {
// Todo: Report errors through the UI
console.error(err);
}
}
/**
* @param {PlainObject} info
* @param {any} info.data
* @param {string} info.origin
* @fires module:svgcanvas.SvgCanvas#event:message
* @returns {void}
*/
messageListener ({ data, origin }) {
const messageObj = { data, origin };
if (!this.extensionsAdded) {
this.messageQueue.push(messageObj);
} else {
// Extensions can handle messages at this stage with their own
// canvas `message` listeners
this.svgCanvas.call('message', messageObj);
}
}
}
export default EditorStartup;

400
src/editor/MainMenu.js Normal file
View File

@@ -0,0 +1,400 @@
/* globals seConfirm, seAlert */
import SvgCanvas from "../svgcanvas/svgcanvas.js";
import { convertUnit, isValidUnit } from '../common/units.js';
import { isChrome } from '../common/browser.js';
const { $id } = SvgCanvas;
const homePage = 'https://github.com/SVG-Edit/svgedit';
/**
*
*/
class MainMenu {
/**
* @param {PlainObject} editor svgedit handler
*/
constructor(editor) {
this.editor = editor;
/**
* @type {Integer}
*/
this.exportWindowCt = 0;
}
/**
* @fires module:svgcanvas.SvgCanvas#event:ext_onNewDocument
* @returns {void}
*/
async clickClear() {
const [ x, y ] = this.editor.configObj.curConfig.dimensions;
const ok = await seConfirm(this.editor.i18next.t('notification.QwantToClear'));
if (ok === "Cancel") {
return;
}
this.editor.leftPanel.clickSelect();
this.editor.svgCanvas.clear();
this.editor.svgCanvas.setResolution(x, y);
this.editor.updateCanvas(true);
this.editor.zoomImage();
this.editor.layersPanel.populateLayers();
this.editor.topPanel.updateContextPanel();
this.editor.svgCanvas.runExtensions("onNewDocument");
}
/**
*
* @returns {void}
*/
hideDocProperties() {
const $imgDialog = document.getElementById("se-img-prop");
$imgDialog.setAttribute("dialog", "close");
$imgDialog.setAttribute("save", this.editor.configObj.pref("img_save"));
this.editor.docprops = false;
}
/**
*
* @returns {void}
*/
hidePreferences() {
const $editDialog = document.getElementById("se-edit-prefs");
$editDialog.setAttribute("dialog", "close");
this.editor.configObj.preferences = false;
}
/**
* @param {Event} e
* @returns {boolean} Whether there were problems saving the document properties
*/
saveDocProperties(e) {
// set title
const { title, w, h, save } = e.detail;
// set document title
this.editor.svgCanvas.setDocumentTitle(title);
if (w !== "fit" && !isValidUnit("width", w)) {
seAlert(this.editor.i18next.t('notification.invalidAttrValGiven'));
return false;
}
if (h !== "fit" && !isValidUnit("height", h)) {
seAlert(this.editor.i18next.t('notification.invalidAttrValGiven'));
return false;
}
if (!this.editor.svgCanvas.setResolution(w, h)) {
seAlert(this.editor.i18next.t('notification.noContentToFitTo'));
return false;
}
// Set image save option
this.editor.configObj.pref("img_save", save);
this.editor.updateCanvas();
this.hideDocProperties();
return true;
}
/**
* Save user preferences based on current values in the UI.
* @param {Event} e
* @function module:SVGthis.savePreferences
* @returns {Promise<void>}
*/
async savePreferences(e) {
const {
lang,
bgcolor,
bgurl,
gridsnappingon,
gridsnappingstep,
gridcolor,
showrulers,
baseunit
} = e.detail;
// Set background
this.editor.setBackground(bgcolor, bgurl);
// set language
if (lang && lang !== this.editor.configObj.pref("lang")) {
this.editor.configObj.pref("lang", lang);
seAlert('Changing the language needs reload');
}
// set grid setting
this.editor.configObj.curConfig.gridSnapping = gridsnappingon;
this.editor.configObj.curConfig.snappingStep = gridsnappingstep;
this.editor.configObj.curConfig.gridColor = gridcolor;
this.editor.configObj.curConfig.showRulers = showrulers;
$id('rulers').style.display = (this.editor.configObj.curConfig.showRulers) ? 'block' : 'none';
if (this.editor.configObj.curConfig.showRulers) {
this.editor.rulers.updateRulers();
}
this.editor.configObj.curConfig.baseUnit = baseunit;
this.editor.svgCanvas.setConfig(this.editor.configObj.curConfig);
this.editor.updateCanvas();
this.hidePreferences();
}
/**
*
* @returns {void}
*/
clickSave() {
// In the future, more options can be provided here
const saveOpts = {
images: this.editor.configObj.pref("img_save"),
round_digits: 6
};
this.editor.svgCanvas.save(saveOpts);
}
/**
*
* @param e
* @returns {Promise<void>} Resolves to `undefined`
*/
async clickExport(e) {
if (e?.detail?.trigger !== "ok" || e?.detail?.imgType === undefined) {
return;
}
const imgType = e?.detail?.imgType;
const quality = e?.detail?.quality ? e?.detail?.quality / 100 : 1;
// Open placeholder window (prevents popup)
let exportWindowName;
/**
*
* @returns {void}
*/
const openExportWindow = () => {
const loadingImage = this.editor.i18next.t('notification.loadingImage');
if (this.editor.configObj.curConfig.exportWindowType === "new") {
this.editor.exportWindowCt++;
}
this.editor.exportWindowName =
this.editor.configObj.curConfig.canvasName + this.editor.exportWindowCt;
let popHTML, popURL;
if (this.editor.loadingURL) {
popURL = this.editor.loadingURL;
} else {
popHTML = `<!DOCTYPE html><html>
<head>
<meta charset="utf-8">
<title>${loadingImage}</title>
</head>
<body><h1>${loadingImage}</h1></body>
<html>`;
if (typeof URL !== "undefined" && URL.createObjectURL) {
const blob = new Blob([ popHTML ], { type: "text/html" });
popURL = URL.createObjectURL(blob);
} else {
popURL = "data:text/html;base64;charset=utf-8," + popHTML;
}
this.editor.loadingURL = popURL;
}
this.editor.exportWindow = window.open(
popURL,
this.editor.exportWindowName
);
};
const chrome = isChrome();
if (imgType === "PDF") {
if (!this.editor.customExportPDF && !chrome) {
openExportWindow();
}
this.editor.svgCanvas.exportPDF(exportWindowName);
} else {
if (!this.editor.customExportImage) {
openExportWindow();
}
/* const results = */ await this.editor.svgCanvas.rasterExport(
imgType,
quality,
this.editor.exportWindowName
);
}
}
/**
* By default, this.editor.svgCanvas.open() is a no-op. It is up to an extension
* mechanism (opera widget, etc.) to call `setCustomHandlers()` which
* will make it do something.
* @returns {void}
*/
clickOpen() {
this.editor.svgCanvas.open();
}
/**
*
* @returns {void}
*/
// eslint-disable-next-line class-methods-use-this
clickImport() {
/* empty fn */
}
/**
*
* @returns {void}
*/
showDocProperties() {
if (this.editor.docprops) {
return;
}
this.editor.docprops = true;
const $imgDialog = document.getElementById("se-img-prop");
// update resolution option with actual resolution
const resolution = this.editor.svgCanvas.getResolution();
if (this.editor.configObj.curConfig.baseUnit !== "px") {
resolution.w =
convertUnit(resolution.w) + this.editor.configObj.curConfig.baseUnit;
resolution.h =
convertUnit(resolution.h) + this.editor.configObj.curConfig.baseUnit;
}
$imgDialog.setAttribute("save", this.editor.configObj.pref("img_save"));
$imgDialog.setAttribute("width", resolution.w);
$imgDialog.setAttribute("height", resolution.h);
$imgDialog.setAttribute("title", this.editor.svgCanvas.getDocumentTitle());
$imgDialog.setAttribute("dialog", "open");
}
/**
*
* @returns {void}
*/
showPreferences() {
if (this.editor.configObj.preferences) {
return;
}
this.editor.configObj.preferences = true;
const $editDialog = document.getElementById("se-edit-prefs");
// Update background color with current one
const canvasBg = this.editor.configObj.curPrefs.bkgd_color;
const url = this.editor.configObj.pref("bkgd_url");
if (url) {
$editDialog.setAttribute("bgurl", url);
}
$editDialog.setAttribute(
"gridsnappingon",
this.editor.configObj.curConfig.gridSnapping
);
$editDialog.setAttribute(
"gridsnappingstep",
this.editor.configObj.curConfig.snappingStep
);
$editDialog.setAttribute(
"gridcolor",
this.editor.configObj.curConfig.gridColor
);
$editDialog.setAttribute("canvasbg", canvasBg);
$editDialog.setAttribute("dialog", "open");
}
/**
*
* @returns {void}
*/
// eslint-disable-next-line class-methods-use-this
openHomePage() {
window.open(homePage, "_blank");
}
/**
* @type {module}
*/
init() {
// add Top panel
const template = document.createElement("template");
const { i18next } = this.editor;
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<se-menu id="main_button" label="SVG-Edit" src="./images/logo.svg" alt="logo">
<!-- File-like buttons: New, Save, Source -->
<se-menu-item id="tool_clear" label="${i18next.t('tools.new_doc')}" shortcut="N" src="./images/new.svg">
</se-menu-item>
<se-menu-item id="tool_open" label="${i18next.t('tools.open_doc')}" src="./images/open.svg">
</se-menu-item>
<se-menu-item id="tool_save" label="${i18next.t('tools.save_doc')}" shortcut="S" src="./images/saveImg.svg">
</se-menu-item>
<se-menu-item id="tool_import" label="${i18next.t('tools.import_doc')}" src="./images/importImg.svg"></se-menu-item>
<se-menu-item id="tool_export" label="${i18next.t('tools.export_img')}" src="./images/export.svg"></se-menu-item>
<se-menu-item id="tool_docprops" label="${i18next.t('tools.docprops')}" shortcut="D" src="./images/docprop.svg">
</se-menu-item>
<se-menu-item id="tool_editor_prefs" label="${i18next.t('config.editor_prefs')}" src="./images/editPref.svg">
</se-menu-item>
<se-menu-item id="tool_editor_homepage" label="${i18next.t('tools.editor_homepage')}" src="./images/logo.svg">
</se-menu-item>
</se-menu>
`;
this.editor.$svgEditor.append(template.content.cloneNode(true));
// register action to main menu entries
/**
* Associate all button actions as well as non-button keyboard shortcuts.
*/
$id("tool_clear").addEventListener("click", this.clickClear.bind(this));
$id("tool_open").addEventListener("click", (e) => {
e.preventDefault();
this.clickOpen();
window.dispatchEvent(new CustomEvent("openImage"));
});
$id("tool_import").addEventListener("click", () => {
this.clickImport();
window.dispatchEvent(new CustomEvent("importImages"));
});
$id("tool_save").addEventListener(
"click",
function() {
const $editorDialog = document.getElementById("se-svg-editor-dialog");
const editingsource = $editorDialog.getAttribute("dialog") === "open";
if (editingsource) {
this.saveSourceEditor();
} else {
this.clickSave();
}
}.bind(this)
);
// this.clickExport.bind(this)
$id("tool_export").addEventListener("click", function() {
document
.getElementById("se-export-dialog")
.setAttribute("dialog", "open");
});
$id("se-export-dialog").addEventListener(
"change",
this.clickExport.bind(this)
);
$id("tool_docprops").addEventListener(
"click",
this.showDocProperties.bind(this)
);
$id("tool_editor_prefs").addEventListener(
"click",
this.showPreferences.bind(this)
);
$id("tool_editor_homepage").addEventListener(
"click",
this.openHomePage.bind(this)
);
$id("se-img-prop").addEventListener(
"change",
function(e) {
if (e.detail.dialog === "closed") {
this.hideDocProperties();
} else {
this.saveDocProperties(e);
}
}.bind(this)
);
$id("se-edit-prefs").addEventListener(
"change",
function(e) {
if (e.detail.dialog === "closed") {
this.hidePreferences();
} else {
this.savePreferences(e);
}
}.bind(this)
);
}
}
export default MainMenu;

202
src/editor/Rulers.js Normal file
View File

@@ -0,0 +1,202 @@
import { getTypeMap } from '../common/units.js';
import rulersTemplate from './templates/rulersTemplate.js';
/**
*
*/
class Rulers {
/**
* @type {Module}
*/
constructor (editor) {
// Make [1,2,5] array
this.rulerIntervals = [];
for (let i = 0.1; i < 1e5; i *= 10) {
this.rulerIntervals.push(i);
this.rulerIntervals.push(2 * i);
this.rulerIntervals.push(5 * i);
}
this.svgCanvas = editor.svgCanvas;
this.editor = editor;
// add rulers component to the DOM
document.body.append(rulersTemplate.content.cloneNode(true));
}
/**
* @type {Module}
*/
manageScroll () {
const rulerX = document.getElementById('ruler_x');
const rulerY = document.getElementById('ruler_y');
if (rulerX) rulerX.scrollLeft = this.editor.workarea.scrollLeft;
if (rulerY) rulerY.scrollTop = this.editor.workarea.scrollTop;
}
/**
*
* @param {HTMLDivElement} [scanvas]
* @param {Float} [zoom]
* @returns {void}
*/
updateRulers (scanvas, zoom) {
if (!zoom) { zoom = this.svgCanvas.getZoom(); }
if (!scanvas) { scanvas = document.getElementById('svgcanvas'); }
let d, i;
const limit = 30000;
const contentElem = this.svgCanvas.getContentElem();
const units = getTypeMap();
const unit = units[this.editor.configObj.curConfig.baseUnit]; // 1 = 1px
// draw x ruler then y ruler
for (d = 0; d < 2; d++) {
const isX = (d === 0);
const dim = isX ? 'x' : 'y';
const lentype = isX ? 'width' : 'height';
const contentDim = Number(contentElem.getAttribute(dim));
const { $id } = this.svgCanvas;
const $hcanvOrig = $id('ruler_' + dim).querySelector('canvas');
// Bit of a hack to fully clear the canvas in Safari & IE9
const $hcanv = $hcanvOrig.cloneNode(true);
// eslint-disable-next-line no-unsanitized/property
$hcanvOrig.replaceWith($hcanv);
const hcanv = $hcanv;
// Set the canvas size to the width of the container
let rulerLen;
if(lentype === 'width'){
rulerLen = parseFloat(getComputedStyle(scanvas, null).width.replace("px", ""));
} else if(lentype === 'height'){
rulerLen = parseFloat(getComputedStyle(scanvas, null).height.replace("px", ""));
}
const totalLen = rulerLen;
hcanv.parentNode.style[lentype] = totalLen + 'px';
let ctx = hcanv.getContext('2d');
let ctxArr, num, ctxArrNum;
ctx.fillStyle = 'rgb(200,0,0)';
ctx.fillRect(0, 0, hcanv.width, hcanv.height);
// Remove any existing canvasses
const elements = Array.prototype.filter.call($hcanv.parentNode.children, function(child){
return child !== $hcanv;
});
Array.from(elements).forEach(function(element) {
element.remove();
});
// Create multiple canvases when necessary (due to browser limits)
if (rulerLen >= limit) {
ctxArrNum = Number.parseInt(rulerLen / limit) + 1;
ctxArr = [];
ctxArr[0] = ctx;
let copy;
for (i = 1; i < ctxArrNum; i++) {
hcanv[lentype] = limit;
copy = hcanv.cloneNode(true);
hcanv.parentNode.append(copy);
ctxArr[i] = copy.getContext('2d');
}
copy[lentype] = rulerLen % limit;
// set copy width to last
rulerLen = limit;
}
hcanv[lentype] = rulerLen;
const uMulti = unit * zoom;
// Calculate the main number interval
const rawM = 50 / uMulti;
let multi = 1;
for (i = 0; i < this.rulerIntervals.length; i++) {
num = this.rulerIntervals[i];
multi = num;
if (rawM <= num) {
break;
}
}
const bigInt = multi * uMulti;
ctx.font = '9px sans-serif';
let rulerD = ((contentDim / uMulti) % multi) * uMulti;
let labelPos = rulerD - bigInt;
// draw big intervals
let ctxNum = 0;
while (rulerD < totalLen) {
labelPos += bigInt;
// const realD = rulerD - contentDim; // Currently unused
const curD = Math.round(rulerD) + 0.5;
if (isX) {
ctx.moveTo(curD, 15);
ctx.lineTo(curD, 0);
} else {
ctx.moveTo(15, curD);
ctx.lineTo(0, curD);
}
num = (labelPos - contentDim) / uMulti;
let label;
if (multi >= 1) {
label = Math.round(num);
} else {
const decs = String(multi).split('.')[1].length;
label = num.toFixed(decs);
}
// Change 1000s to Ks
if (label !== 0 && label !== 1000 && label % 1000 === 0) {
label = (label / 1000) + 'K';
}
if (isX) {
ctx.fillText(label, rulerD + 2, 8);
} else {
// draw label vertically
const str = String(label).split('');
for (i = 0; i < str.length; i++) {
ctx.fillText(str[i], 1, (rulerD + 9) + i * 9);
}
}
const part = bigInt / 10;
// draw the small intervals
for (i = 1; i < 10; i++) {
let subD = Math.round(rulerD + part * i) + 0.5;
if (ctxArr && subD > rulerLen) {
ctxNum++;
ctx.stroke();
if (ctxNum >= ctxArrNum) {
i = 10;
rulerD = totalLen;
continue;
}
ctx = ctxArr[ctxNum];
rulerD -= limit;
subD = Math.round(rulerD + part * i) + 0.5;
}
// odd lines are slighly longer
const lineNum = (i % 2) ? 12 : 10;
if (isX) {
ctx.moveTo(subD, 15);
ctx.lineTo(subD, lineNum);
} else {
ctx.moveTo(15, subD);
ctx.lineTo(lineNum, subD);
}
}
rulerD += bigInt;
}
ctx.strokeStyle = '#000';
ctx.stroke();
}
}
}
export default Rulers;

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
<link rel="icon" type="image/png" href="images/logo.png"/>
<link rel="icon" type="image/png" href="images/logo.svg"/>
<link rel="stylesheet" href="svgedit.css"/>
<title>Browser does not support SVG | SVG-edit</title>
<style>
@@ -35,7 +35,7 @@
</style>
</head>
<body>
<img id="logo" src="images/logo.png" width="48" height="48" alt="SVG-edit logo" />
<img id="logo" src="images/logo.svg" width="48" height="48" alt="SVG-edit logo" />
<p>Sorry, but your browser does not support SVG. Below is a list of
alternate browsers and versions that support SVG and SVG-edit
(from <a href="https://caniuse.com/#cats=SVG">caniuse.com</a>).

View File

@@ -0,0 +1,144 @@
import { jGraduate } from './jgraduate/jQuery.jGraduate.js';
/**
*
*/
class PaintBox {
/**
* @param {string|Element|external:jQuery} container
* @param {"fill"} type
*/
constructor (container, type) {
// set up gradients to be used for the buttons
const svgdocbox = new DOMParser().parseFromString(
`<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14">
<rect
fill="#000000" opacity="1" width="14" height="14"/>
<defs><linearGradient id="gradbox_${PaintBox.ctr++}"/></defs>
</svg>`,
'text/xml'
);
let docElem = svgdocbox.documentElement;
docElem = document.importNode(docElem, true);
container.appendChild(docElem);
this.rect = docElem.firstElementChild;
this.defs = docElem.getElementsByTagName('defs')[0];
this.grad = this.defs.firstElementChild;
// this.paint = new $.jGraduate.Paint({solidColor: color});
this.type = type;
}
/**
* @param {module:jGraduate~Paint} paint
* @returns {void}
*/
setPaint (paint) {
this.paint = paint;
const ptype = paint.type;
const opac = paint.alpha / 100;
let fillAttr = 'none';
switch (ptype) {
case 'solidColor':
fillAttr = (paint[ptype] !== 'none') ? '#' + paint[ptype] : paint[ptype];
break;
case 'linearGradient':
case 'radialGradient': {
this.grad.remove();
this.grad = paint[ptype];
this.defs.appendChild(this.grad);
const id = this.grad.id = 'gradbox_' + this.type;
fillAttr = 'url(#' + id + ')';
break;
}
}
this.rect.setAttribute('fill', fillAttr);
this.rect.setAttribute('opacity', opac);
}
/**
* @param {PlainObject} svgCanvas
* @param {string} color
* @param {Float} opac
* @param {string} type
* @returns {module:jGraduate~Paint}
*/
static getPaint (svgCanvas, color, opac, type) {
// update the editor's fill paint
const opts = { alpha: opac };
if (color.startsWith('url(#')) {
let refElem = svgCanvas.getRefElem(color);
refElem = (refElem) ? refElem.cloneNode(true) : document.querySelectorAll('#' + type + '_color defs *')[0];
opts[refElem.tagName] = refElem;
} else if (color.startsWith('#')) {
opts.solidColor = color.substr(1);
} else {
opts.solidColor = 'none';
}
return new jGraduate.Paint(opts);
}
/**
* @param {PlainObject} svgcanvas
* @param {PlainObject} selectedElement
* @returns {any}
*/
update (svgcanvas, selectedElement) {
if (!selectedElement) { return null; }
const { type } = this;
switch (selectedElement.tagName) {
case 'use':
case 'image':
case 'foreignObject':
// These elements don't have fill or stroke, so don't change
// the current value
return null;
case 'g':
case 'a': {
const childs = selectedElement.getElementsByTagName('*');
let gPaint = null;
for (let i = 0, len = childs.length; i < len; i++) {
const elem = childs[i];
const p = elem.getAttribute(type);
if (i === 0) {
gPaint = p;
} else if (gPaint !== p) {
gPaint = null;
break;
}
}
if (gPaint === null) {
// No common color, don't update anything
this._paintColor = null;
return null;
}
this._paintColor = gPaint;
this._paintOpacity = 1;
break;
} default: {
this._paintOpacity = Number.parseFloat(selectedElement.getAttribute(type + '-opacity'));
if (Number.isNaN(this._paintOpacity)) {
this._paintOpacity = 1.0;
}
const defColor = type === 'fill' ? 'black' : 'none';
this._paintColor = selectedElement.getAttribute(type) || defColor;
}
}
this._paintOpacity *= 100;
const paint = PaintBox.getPaint(svgcanvas, this._paintColor, this._paintOpacity, type);
// update the rect inside #fill_color/#stroke_color
this.setPaint(paint);
return (paint);
}
}
PaintBox.ctr = 0;
export default PaintBox;

View File

@@ -0,0 +1,12 @@
import './seButton.js';
import './seFlyingButton.js';
import './seExplorerButton.js';
import './seZoom.js';
import './seInput.js';
import './seSpinInput.js';
import './sePalette.js';
import './seMenu.js';
import './seMenuItem.js';
import './seList.js';
import './seListItem.js';
import './seColorPicker.js';

View File

@@ -0,0 +1,391 @@
/* eslint-disable max-len */
/* eslint-disable no-bitwise */
/**
* @external Math
*/
/**
* @memberof external:Math
* @param {Float} value
* @param {Float} precision
* @returns {Float}
*/
function toFixedNumeric (value, precision) {
if (precision === undefined) precision = 0;
return Math.round(value * (10 ** precision)) / (10 ** precision);
}
/**
* Whether a value is `null` or `undefined`.
* @param {any} val
* @returns {boolean}
*/
const isNullish = (val) => {
return val === null || val === undefined;
};
/**
* Controls for all the input elements for the typing in color values.
*/
export default class ColorValuePicker {
/**
* @param {external:jQuery} picker
* @param {external:jQuery.jPicker.Color} color
* @param {external:jQuery.fn.$.fn.jPicker} bindedHex
* @param {Float} alphaPrecision
*/
constructor (picker, color, bindedHex, alphaPrecision) {
const that = this; // private properties and methods
const inputs = picker.querySelectorAll('td.Text input');
// input box key down - use arrows to alter color
/**
*
* @param {Event} e
* @returns {Event|false|void}
*/
function keyDown (e) {
if (e.target.value === '' && e.target !== hex && ((!isNullish(bindedHex) && e.target !== bindedHex) || isNullish(bindedHex))) return undefined;
if (!validateKey(e)) return e;
switch (e.target) {
case red:
switch (e.keyCode) {
case 38:
red.value = setValueInRange.call(that, (red.value << 0) + 1, 0, 255);
color.val('r', red.value, e.target);
return false;
case 40:
red.value = setValueInRange.call(that, (red.value << 0) - 1, 0, 255);
color.val('r', red.value, e.target);
return false;
}
break;
case green:
switch (e.keyCode) {
case 38:
green.value = setValueInRange.call(that, (green.value << 0) + 1, 0, 255);
color.val('g', green.value, e.target);
return false;
case 40:
green.value = setValueInRange.call(that, (green.value << 0) - 1, 0, 255);
color.val('g', green.value, e.target);
return false;
}
break;
case blue:
switch (e.keyCode) {
case 38:
blue.value = setValueInRange.call(that, (blue.value << 0) + 1, 0, 255);
color.val('b', blue.value, e.target);
return false;
case 40:
blue.value = setValueInRange.call(that, (blue.value << 0) - 1, 0, 255);
color.val('b', blue.value, e.target);
return false;
}
break;
case alpha:
switch (e.keyCode) {
case 38:
alpha.value = setValueInRange.call(that, Number.parseFloat(alpha.value) + 1, 0, 100);
color.val('a', toFixedNumeric((alpha.value * 255) / 100, alphaPrecision), e.target);
return false;
case 40:
alpha.value = setValueInRange.call(that, Number.parseFloat(alpha.value) - 1, 0, 100);
color.val('a', toFixedNumeric((alpha.value * 255) / 100, alphaPrecision), e.target);
return false;
}
break;
case hue:
switch (e.keyCode) {
case 38:
hue.value = setValueInRange.call(that, (hue.value << 0) + 1, 0, 360);
color.val('h', hue.value, e.target);
return false;
case 40:
hue.value =setValueInRange.call(that, (hue.value << 0) - 1, 0, 360);
color.val('h', hue.value, e.target);
return false;
}
break;
case saturation:
switch (e.keyCode) {
case 38:
saturation.value = setValueInRange.call(that, (saturation.value << 0) + 1, 0, 100);
color.val('s', saturation.value, e.target);
return false;
case 40:
saturation.value = setValueInRange.call(that, (saturation.value << 0) - 1, 0, 100);
color.val('s', saturation.value, e.target);
return false;
}
break;
case value:
switch (e.keyCode) {
case 38:
value.value = setValueInRange.call(that, (value.value << 0) + 1, 0, 100);
color.val('v', value.value, e.target);
return false;
case 40:
value.value = setValueInRange.call(that, (value.value << 0) - 1, 0, 100);
color.val('v', value.value, e.target);
return false;
}
break;
}
return undefined;
}
// input box key up - validate value and set color
/**
* @param {Event} e
* @returns {Event|void}
* @todo Why is this returning an event?
*/
function keyUp (e) {
if (e.target.value === '' && e.target !== hex &&
((!isNullish(bindedHex) && e.target !== bindedHex) ||
isNullish(bindedHex))) return undefined;
if (!validateKey(e)) return e;
switch (e.target) {
case red:
red.value = setValueInRange.call(that, red.value, 0, 255);
color.val('r', red.value, e.target);
break;
case green:
green.value = setValueInRange.call(that, green.value, 0, 255);
color.val('g', green.value, e.target);
break;
case blue:
blue.value = setValueInRange.call(that, blue.value, 0, 255);
color.val('b', blue.value, e.target);
break;
case alpha:
alpha.value = setValueInRange.call(that, alpha.value, 0, 100);
color.val('a', toFixedNumeric((alpha.value * 255) / 100, alphaPrecision), e.target);
break;
case hue:
hue.value = setValueInRange.call(that, hue.value, 0, 360);
color.val('h', hue.value, e.target);
break;
case saturation:
saturation.value = setValueInRange.call(that, saturation.value, 0, 100);
color.val('s', saturation.value, e.target);
break;
case value:
value.value = setValueInRange.call(that, value.value, 0, 100);
color.val('v', value.value, e.target);
break;
case hex:
hex.value = hex.value.replace(/[^a-fA-F\d]/g, '').toLowerCase().substring(0, 6);
bindedHex && bindedHex.val(hex.value);
color.val('hex', hex.value !== '' ? hex.value : null, e.target);
break;
case bindedHex:
bindedHex.value = bindedHex.value.replace(/[^a-fA-F\d]/g, '').toLowerCase().substring(0, 6);
hex.val(bindedHex.value);
color.val('hex', bindedHex.value !== '' ? bindedHex.value : null, e.target);
break;
case ahex:
ahex.value = ahex.value.replace(/[^a-fA-F\d]/g, '').toLowerCase().substring(0, 2);
color.val('a', !isNullish(ahex.value) ? Number.parseInt(ahex.value, 16) : null, e.target);
break;
}
return undefined;
}
// input box blur - reset to original if value empty
/**
* @param {Event} e
* @returns {void}
*/
function blur (e) {
if (!isNullish(color.value)) {
switch (e.target) {
case red:
color.value = 'r';
red.value = color.value;
break;
case green:
color.value = 'g';
green.value = color.value;
break;
case blue:
color.value = 'b';
blue.value = color.value;
break;
case alpha:
color.value = 'a';
alpha.value = toFixedNumeric((color.value * 100) / 255, alphaPrecision);
break;
case hue:
color.value = 'h';
hue.value = color.value;
break;
case saturation:
color.value = 's';
saturation.value = color.value;
break;
case value:
color.value = 'v';
value.value = color.value;
break;
case hex:
case bindedHex:
color.value = 'hex';
hex.value = color.value;
bindedHex.value = color.value;
break;
case ahex:
color.value = 'ahex';
ahex.value = color.value.substring(6);
break;
}
}
}
/**
* @param {Event} e
* @returns {boolean}
*/
function validateKey (e) {
switch (e.keyCode) {
case 9:
case 16:
case 29:
case 37:
case 39:
return false;
case 'c'.charCodeAt():
case 'v'.charCodeAt():
if (e.ctrlKey) return false;
}
return true;
}
/**
* Constrain value within range.
* @param {Float|string} value
* @param {Float} min
* @param {Float} max
* @returns {Float|string} Returns a number or numeric string
*/
function setValueInRange (value, min, max) {
if (value === '' || isNaN(value)) return min;
if (value > max) return max;
if (value < min) return min;
return value;
}
/**
* @param {external:jQuery} ui
* @param {Element} context
* @returns {void}
*/
function colorChanged (ui, context) {
const all = ui.val('all');
if (context !== red) red.value = (!isNullish(all) ? all.r : '');
if (context !== green) green.value = (!isNullish(all) ? all.g : '');
if (context !== blue) blue.value = (!isNullish(all) ? all.b : '');
if (alpha && context !== alpha) alpha.value = (!isNullish(all) ? toFixedNumeric((all.a * 100) / 255, alphaPrecision) : '');
if (context !== hue) hue.value = (!isNullish(all) ? all.h : '');
if (context !== saturation) saturation.value = (!isNullish(all) ? all.s : '');
if (context !== value) value.value = (!isNullish(all) ? all.v : '');
if (context !== hex && ((bindedHex && context !== bindedHex) || !bindedHex)) hex.value = (!isNullish(all) ? all.hex : '');
if (bindedHex && context !== bindedHex && context !== hex) bindedHex.value = (!isNullish(all) ? all.hex : '');
if (ahex && context !== ahex) ahex.value = (!isNullish(all) ? all.ahex.substring(6) : '');
}
/**
* Unbind all events and null objects.
* @returns {void}
*/
function destroy () {
red.removeEventListener('keyup', keyUp);
green.removeEventListener('keyup', keyUp);
blue.removeEventListener('keyup', keyUp);
hue.removeEventListener('keyup', keyUp);
saturation.removeEventListener('keyup', keyUp);
value.removeEventListener('keyup', keyUp);
hex.removeEventListener('keyup', keyUp);
red.removeEventListener('blur', blur);
green.removeEventListener('blur', blur);
blue.removeEventListener('blur', blur);
hue.removeEventListener('blur', blur);
saturation.removeEventListener('blur', blur);
value.removeEventListener('blur', blur);
hex.removeEventListener('blur', blur);
red.removeEventListener('keydown', keyDown);
green.removeEventListener('keydown', keyDown);
blue.removeEventListener('keydown', keyDown);
hue.removeEventListener('keydown', keyDown);
saturation.removeEventListener('keydown', keyDown);
value.removeEventListener('keydown', keyDown);
if (alpha !== null) {
alpha.removeEventListener('keyup', keyUp);
alpha.removeEventListener('blur', blur);
alpha.removeEventListener('keydown', keyDown);
}
if (ahex !== null) {
ahex.removeEventListener('keyup', keyUp);
ahex.removeEventListener('blur', blur);
}
if (bindedHex !== null) {
bindedHex.removeEventListener('keyup', keyUp);
bindedHex.removeEventListener('blur', blur);
}
color.unbind(colorChanged);
red = null;
green = null;
blue = null;
alpha = null;
hue = null;
saturation = null;
value = null;
hex = null;
ahex = null;
}
let
red = inputs[3],
green = inputs[4],
blue = inputs[5],
alpha = inputs.length > 7 ? inputs[6] : null,
hue = inputs[0],
saturation = inputs[1],
value = inputs[2],
hex = inputs[(inputs.length > 7) ? 7 : 6],
ahex = inputs.length > 7 ? inputs[8] : null;
Object.assign(that, { destroy });
red.addEventListener('keyup', keyUp);
green.addEventListener('keyup', keyUp);
blue.addEventListener('keyup', keyUp);
hue.addEventListener('keyup', keyUp);
saturation.addEventListener('keyup', keyUp);
value.addEventListener('keyup', keyUp);
hex.addEventListener('keyup', keyUp);
red.addEventListener('blur', blur);
green.addEventListener('blur', blur);
blue.addEventListener('blur', blur);
hue.addEventListener('blur', blur);
saturation.addEventListener('blur', blur);
value.addEventListener('blur', blur);
hex.addEventListener('blur', blur);
red.addEventListener('keydown', keyDown);
green.addEventListener('keydown', keyDown);
blue.addEventListener('keydown', keyDown);
hue.addEventListener('keydown', keyDown);
saturation.addEventListener('keydown', keyDown);
value.addEventListener('keydown', keyDown);
if (alpha !== null) {
alpha.addEventListener('keyup', keyUp);
alpha.addEventListener('blur', blur);
alpha.addEventListener('keydown', keyDown);
}
if (ahex !== null) {
ahex.addEventListener('keyup', keyUp);
ahex.addEventListener('blur', blur);
}
if (bindedHex !== null) {
bindedHex.addEventListener('keyup', keyUp);
bindedHex.addEventListener('blur', blur);
}
color.bind(colorChanged);
}
}

View File

@@ -0,0 +1,338 @@
/* eslint-disable no-bitwise */
import { findPos } from './Util.js';
/**
* Whether a value is `null` or `undefined`.
* @param {any} val
* @returns {boolean}
*/
const isNullish = (val) => {
return val === null || val === undefined;
};
/**
* Encapsulate slider functionality for the ColorMap and ColorBar -
* could be useful to use a jQuery UI draggable for this with certain extensions.
* @memberof module:jPicker
*/
export default class Slider {
/**
* @param {external:jQuery} bar
* @param {module:jPicker.SliderOptions} options
*/
constructor (bar, options) {
const that = this;
/**
* Fire events on the supplied `context`
* @param {module:jPicker.JPickerInit} context
* @returns {void}
*/
function fireChangeEvents (context) {
changeEvents.forEach((changeEvent) => {
changeEvent.call(that, that, context);
});
}
/**
* Bind the mousedown to the bar not the arrow for quick snapping to the clicked location.
* @param {external:jQuery.Event} e
* @returns {void}
*/
function mouseDown (e) {
const off = findPos(bar);
offset = { l: off.left | 0, t: off.top | 0 };
clearTimeout(timeout);
// using setTimeout for visual updates - once the style is updated the browser will re-render internally allowing the next Javascript to run
timeout = setTimeout(function () {
setValuesFromMousePosition.call(that, e);
}, 0);
// Bind mousemove and mouseup event to the document so it responds when dragged of of the bar - we will unbind these when on mouseup to save processing
document.addEventListener('mousemove', mouseMove);
document.addEventListener('mouseup', mouseUp);
e.preventDefault(); // don't try to select anything or drag the image to the desktop
}
/**
* Set the values as the mouse moves.
* @param {external:jQuery.Event} e
* @returns {false}
*/
function mouseMove (e) {
clearTimeout(timeout);
timeout = setTimeout(function () {
setValuesFromMousePosition.call(that, e);
}, 0);
e.stopPropagation();
e.preventDefault();
return false;
}
/**
* Unbind the document events - they aren't needed when not dragging.
* @param {external:jQuery.Event} e
* @returns {false}
*/
function mouseUp (e) {
document.removeEventListener('mousemove', mouseMove);
document.removeEventListener('mouseup', mouseUp);
e.stopPropagation();
e.preventDefault();
return false;
}
/**
* Calculate mouse position and set value within the current range.
* @param {Event} e
* @returns {void}
*/
function setValuesFromMousePosition (e) {
const barW = bar.w, // local copies for YUI compressor
barH = bar.h;
let locX = e.pageX - offset.l,
locY = e.pageY - offset.t;
// keep the arrow within the bounds of the bar
if (locX < 0) locX = 0;
else if (locX > barW) locX = barW;
if (locY < 0) locY = 0;
else if (locY > barH) locY = barH;
val.call(that, 'xy', {
x: ((locX / barW) * rangeX) + minX,
y: ((locY / barH) * rangeY) + minY
});
}
/**
*
* @returns {void}
*/
function draw () {
const
barW = bar.w,
barH = bar.h,
arrowW = arrow.w,
arrowH = arrow.h;
let arrowOffsetX = 0,
arrowOffsetY = 0;
setTimeout(function () {
if (rangeX > 0) { // range is greater than zero
// constrain to bounds
if (x === maxX) arrowOffsetX = barW;
else arrowOffsetX = ((x / rangeX) * barW) | 0;
}
if (rangeY > 0) { // range is greater than zero
// constrain to bounds
if (y === maxY) arrowOffsetY = barH;
else arrowOffsetY = ((y / rangeY) * barH) | 0;
}
// if arrow width is greater than bar width, center arrow and prevent horizontal dragging
if (arrowW >= barW) arrowOffsetX = (barW >> 1) - (arrowW >> 1); // number >> 1 - superfast bitwise divide by two and truncate (move bits over one bit discarding lowest)
else arrowOffsetX -= arrowW >> 1;
// if arrow height is greater than bar height, center arrow and prevent vertical dragging
if (arrowH >= barH) arrowOffsetY = (barH >> 1) - (arrowH >> 1);
else arrowOffsetY -= arrowH >> 1;
// set the arrow position based on these offsets
arrow.style.left = arrowOffsetX + 'px';
arrow.style.top = arrowOffsetY + 'px';
});
}
/**
* Get or set a value.
* @param {?("xy"|"x"|"y")} name
* @param {module:math.XYObject} value
* @param {module:jPicker.Slider} context
* @returns {module:math.XYObject|Float|void}
*/
function val (name, value, context) {
const set = value !== undefined;
if (!set) {
if (isNullish(name)) name = 'xy';
switch (name.toLowerCase()) {
case 'x': return x;
case 'y': return y;
case 'xy':
default: return { x, y };
}
}
if (!isNullish(context) && context === that) return undefined;
let changed = false;
let newX, newY;
if (isNullish(name)) name = 'xy';
switch (name.toLowerCase()) {
case 'x':
newX = (value && ((value.x && value.x | 0) || value | 0)) || 0;
break;
case 'y':
newY = (value && ((value.y && value.y | 0) || value | 0)) || 0;
break;
case 'xy':
default:
newX = (value && value.x && value.x | 0) || 0;
newY = (value && value.y && value.y | 0) || 0;
break;
}
if (!isNullish(newX)) {
if (newX < minX) newX = minX;
else if (newX > maxX) newX = maxX;
if (x !== newX) {
x = newX;
changed = true;
}
}
if (!isNullish(newY)) {
if (newY < minY) newY = minY;
else if (newY > maxY) newY = maxY;
if (y !== newY) {
y = newY;
changed = true;
}
}
changed && fireChangeEvents.call(that, context || that);
return undefined;
}
/**
* @typedef {PlainObject} module:jPicker.MinMaxRangeX
* @property {Float} minX
* @property {Float} maxX
* @property {Float} rangeX
*/
/**
* @typedef {PlainObject} module:jPicker.MinMaxRangeY
* @property {Float} minY
* @property {Float} maxY
* @property {Float} rangeY
*/
/**
* @typedef {module:jPicker.MinMaxRangeY|module:jPicker.MinMaxRangeX} module:jPicker.MinMaxRangeXY
*/
/**
*
* @param {"minx"|"maxx"|"rangex"|"miny"|"maxy"|"rangey"|"all"} name
* @param {module:jPicker.MinMaxRangeXY} value
* @returns {module:jPicker.MinMaxRangeXY|module:jPicker.MinMaxRangeX|module:jPicker.MinMaxRangeY|void}
*/
function range (name, value) {
const set = value !== undefined;
if (!set) {
if (isNullish(name)) name = 'all';
switch (name.toLowerCase()) {
case 'minx': return minX;
case 'maxx': return maxX;
case 'rangex': return { minX, maxX, rangeX };
case 'miny': return minY;
case 'maxy': return maxY;
case 'rangey': return { minY, maxY, rangeY };
case 'all':
default: return { minX, maxX, rangeX, minY, maxY, rangeY };
}
}
let // changed = false,
newMinX,
newMaxX,
newMinY,
newMaxY;
if (isNullish(name)) name = 'all';
switch (name.toLowerCase()) {
case 'minx':
newMinX = (value && ((value.minX && value.minX | 0) || value | 0)) || 0;
break;
case 'maxx':
newMaxX = (value && ((value.maxX && value.maxX | 0) || value | 0)) || 0;
break;
case 'rangex':
newMinX = (value && value.minX && value.minX | 0) || 0;
newMaxX = (value && value.maxX && value.maxX | 0) || 0;
break;
case 'miny':
newMinY = (value && ((value.minY && value.minY | 0) || value | 0)) || 0;
break;
case 'maxy':
newMaxY = (value && ((value.maxY && value.maxY | 0) || value | 0)) || 0;
break;
case 'rangey':
newMinY = (value && value.minY && value.minY | 0) || 0;
newMaxY = (value && value.maxY && value.maxY | 0) || 0;
break;
case 'all':
default:
newMinX = (value && value.minX && value.minX | 0) || 0;
newMaxX = (value && value.maxX && value.maxX | 0) || 0;
newMinY = (value && value.minY && value.minY | 0) || 0;
newMaxY = (value && value.maxY && value.maxY | 0) || 0;
break;
}
if (!isNullish(newMinX) && minX !== newMinX) {
minX = newMinX;
rangeX = maxX - minX;
}
if (!isNullish(newMaxX) && maxX !== newMaxX) {
maxX = newMaxX;
rangeX = maxX - minX;
}
if (!isNullish(newMinY) && minY !== newMinY) {
minY = newMinY;
rangeY = maxY - minY;
}
if (!isNullish(newMaxY) && maxY !== newMaxY) {
maxY = newMaxY;
rangeY = maxY - minY;
}
return undefined;
}
/**
* @param {GenericCallback} callback
* @returns {void}
*/
function bind (callback) { // eslint-disable-line promise/prefer-await-to-callbacks
if (typeof callback === 'function') changeEvents.push(callback);
}
/**
* @param {GenericCallback} callback
* @returns {void}
*/
function unbind (callback) { // eslint-disable-line promise/prefer-await-to-callbacks
if (typeof callback !== 'function') return;
let i;
while ((i = changeEvents.includes(callback))) changeEvents.splice(i, 1);
}
/**
*
* @returns {void}
*/
function destroy () {
// unbind all possible events and null objects
document.removeEventListener('mousemove', mouseMove);
document.removeEventListener('mouseup', mouseUp);
bar.removeEventListener('mousedown', mouseDown);
bar = null;
arrow = null;
changeEvents = null;
}
let offset,
timeout,
x = 0,
y = 0,
minX = 0,
maxX = 100,
rangeX = 100,
minY = 0,
maxY = 100,
rangeY = 100,
arrow = bar.querySelector('img'), // the arrow image to drag
changeEvents = [];
Object.assign(that, {
val,
range,
bind,
unbind,
destroy
});
// initialize this control
arrow.src = options.arrow && options.arrow.image;
arrow.w = (options.arrow && options.arrow.width) || parseFloat(getComputedStyle(arrow, null).width.replace("px", ""));
arrow.h = (options.arrow && options.arrow.height) || parseFloat(getComputedStyle(arrow, null).height.replace("px", ""));
bar.w = (options.map && options.map.width) || parseFloat(getComputedStyle(bar, null).width.replace("px", ""));
bar.h = (options.map && options.map.height) || parseFloat(getComputedStyle(bar, null).height.replace("px", ""));
bar.addEventListener('mousedown', mouseDown);
bind.call(that, draw);
}
}

View File

@@ -0,0 +1,218 @@
/* eslint-disable sonarjs/no-collapsible-if */
/**
* @param {any} obj
* @returns {any}
*/
export function findPos(obj) {
let curleft = 0;
let curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj == obj.offsetParent);
return { left: curleft, top: curtop };
}
return { left: curleft, top: curtop };
}
export function isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
export function mergeDeep(target, source) {
let output = Object.assign({}, target);
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach((key) => {
if (isObject(source[key])) {
if (!(key in target))
Object.assign(output, { [key]: source[key] });
else
output[key] = mergeDeep(target[key], source[key]);
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}
/**
* Get the closest matching element up the DOM tree.
* @param {Element} elem Starting element
* @param {String} selector Selector to match against (class, ID, data attribute, or tag)
* @return {Boolean|Element} Returns null if not match found
*/
export function getClosest(elem, selector) {
const firstChar = selector.charAt(0);
const supports = 'classList' in document.documentElement;
let attribute, value;
// If selector is a data attribute, split attribute from value
if (firstChar === '[') {
selector = selector.substr(1, selector.length - 2);
attribute = selector.split('=');
if (attribute.length > 1) {
value = true;
attribute[1] = attribute[1].replace(/"/g, '').replace(/'/g, '');
}
}
// Get closest match
for (; elem && elem !== document && elem.nodeType === 1; elem = elem.parentNode) {
// If selector is a class
if (firstChar === '.') {
if (supports) {
if (elem.classList.contains(selector.substr(1))) {
return elem;
}
} else {
if (new RegExp('(^|\\s)' + selector.substr(1) + '(\\s|$)').test(elem.className)) {
return elem;
}
}
}
// If selector is an ID
if (firstChar === '#') {
if (elem.id === selector.substr(1)) {
return elem;
}
}
// If selector is a data attribute
if (firstChar === '[') {
if (elem.hasAttribute(attribute[0])) {
if (value) {
if (elem.getAttribute(attribute[0]) === attribute[1]) {
return elem;
}
} else {
return elem;
}
}
}
// If selector is a tag
if (elem.tagName.toLowerCase() === selector) {
return elem;
}
}
return null;
}
/**
* Get all DOM element up the tree that contain a class, ID, or data attribute
* @param {Node} elem The base element
* @param {String} selector The class, id, data attribute, or tag to look for
* @return {Array} Null if no match
*/
export function getParents(elem, selector) {
const parents = [];
let firstChar;
if ( selector ) {
firstChar = selector.charAt(0);
}
// Get matches
for ( ; elem && elem !== document; elem = elem.parentNode ) {
if ( selector ) {
// If selector is a class
if ( firstChar === '.' ) {
if ( elem.classList.contains( selector.substr(1) ) ) {
parents.push( elem );
}
}
// If selector is an ID
if ( firstChar === '#' ) {
if ( elem.id === selector.substr(1) ) {
parents.push( elem );
}
}
// If selector is a data attribute
if ( firstChar === '[' ) {
if ( elem.hasAttribute( selector.substr(1, selector.length - 1) ) ) {
parents.push( elem );
}
}
// If selector is a tag
if ( elem.tagName.toLowerCase() === selector ) {
parents.push( elem );
}
} else {
parents.push( elem );
}
}
// Return parents if any exist
if ( parents.length === 0 ) {
return null;
} else {
return parents;
}
}
export function getParentsUntil(elem, parent, selector) {
let parents = [];
let parentType;
let selectorType;
if ( parent ) {
parentType = parent.charAt(0);
}
if ( selector ) {
selectorType = selector.charAt(0);
}
// Get matches
for ( ; elem && elem !== document; elem = elem.parentNode ) {
// Check if parent has been reached
if ( parent ) {
// If parent is a class
if ( parentType === '.' ) {
if ( elem.classList.contains( parent.substr(1) ) ) {
break;
}
}
// If parent is an ID
if ( parentType === '#' ) {
if ( elem.id === parent.substr(1) ) {
break;
}
}
// If parent is a data attribute
if ( parentType === '[' ) {
if ( elem.hasAttribute( parent.substr(1, parent.length - 1) ) ) {
break;
}
}
// If parent is a tag
if ( elem.tagName.toLowerCase() === parent ) {
break;
}
}
if ( selector ) {
// If selector is a class
if ( selectorType === '.' ) {
if ( elem.classList.contains( selector.substr(1) ) ) {
parents.push( elem );
}
}
// If selector is an ID
if ( selectorType === '#' ) {
if ( elem.id === selector.substr(1) ) {
parents.push( elem );
}
}
// If selector is a data attribute
if ( selectorType === '[' ) {
if ( elem.hasAttribute( selector.substr(1, selector.length - 1) ) ) {
parents.push( elem );
}
}
// If selector is a tag
if ( elem.tagName.toLowerCase() === selector ) {
parents.push( elem );
}
} else {
parents.push( elem );
}
}
// Return parents if any exist
if ( parents.length === 0 ) {
return null;
} else {
return parents;
}
}

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 265 B

After

Width:  |  Height:  |  Size: 265 B

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 473 B

View File

Before

Width:  |  Height:  |  Size: 80 B

After

Width:  |  Height:  |  Size: 80 B

View File

Before

Width:  |  Height:  |  Size: 81 B

After

Width:  |  Height:  |  Size: 81 B

View File

Before

Width:  |  Height:  |  Size: 93 B

After

Width:  |  Height:  |  Size: 93 B

View File

Before

Width:  |  Height:  |  Size: 141 B

After

Width:  |  Height:  |  Size: 141 B

View File

Before

Width:  |  Height:  |  Size: 144 B

After

Width:  |  Height:  |  Size: 144 B

View File

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 146 B

View File

Before

Width:  |  Height:  |  Size: 80 B

After

Width:  |  Height:  |  Size: 80 B

View File

Before

Width:  |  Height:  |  Size: 76 B

After

Width:  |  Height:  |  Size: 76 B

View File

Before

Width:  |  Height:  |  Size: 93 B

After

Width:  |  Height:  |  Size: 93 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
/**
*
*/
export default class Paint {
/**
* @param {module:jGraduate.jGraduatePaintOptions} [opt]
*/
constructor (opt) {
const options = opt || {};
this.alpha = isNaN(options.alpha) ? 100 : options.alpha;
// copy paint object
if (options.copy) {
/**
* @name module:jGraduate~Paint#type
* @type {"none"|"solidColor"|"linearGradient"|"radialGradient"}
*/
this.type = options.copy.type;
/**
* Represents opacity (0-100).
* @name module:jGraduate~Paint#alpha
* @type {Float}
*/
this.alpha = options.copy.alpha;
/**
* Represents #RRGGBB hex of color.
* @name module:jGraduate~Paint#solidColor
* @type {string}
*/
this.solidColor = null;
/**
* @name module:jGraduate~Paint#linearGradient
* @type {SVGLinearGradientElement}
*/
this.linearGradient = null;
/**
* @name module:jGraduate~Paint#radialGradient
* @type {SVGRadialGradientElement}
*/
this.radialGradient = null;
switch (this.type) {
case 'none':
break;
case 'solidColor':
this.solidColor = options.copy.solidColor;
break;
case 'linearGradient':
this.linearGradient = options.copy.linearGradient.cloneNode(true);
break;
case 'radialGradient':
this.radialGradient = options.copy.radialGradient.cloneNode(true);
break;
}
// create linear gradient paint
} else if (options.linearGradient) {
this.type = 'linearGradient';
this.solidColor = null;
this.radialGradient = null;
this.linearGradient = options.linearGradient.cloneNode(true);
// create linear gradient paint
} else if (options.radialGradient) {
this.type = 'radialGradient';
this.solidColor = null;
this.linearGradient = null;
this.radialGradient = options.radialGradient.cloneNode(true);
// create solid color paint
} else if (options.solidColor) {
this.type = 'solidColor';
this.solidColor = options.solidColor;
// create empty paint
} else {
this.type = 'none';
this.solidColor = null;
this.linearGradient = null;
this.radialGradient = null;
}
}
}

View File

@@ -0,0 +1,227 @@
// import {isMac} from '../../common/browser.js';
// if (isMac() && !window.opera) 'Ctrl+' 'Cmd+'
const template = document.createElement('template');
template.innerHTML = `
<style>
:host(:hover) :not(.disabled)
{
background-color: var(--icon-bg-color-hover);
}
div
{
height: 24px;
width: 24px;
margin: 2px 1px 4px;
padding: 3px;
background-color: var(--icon-bg-color);
cursor: pointer;
border-radius: 3px;
}
.small {
width: 14px;
height: 14px;
padding: 1px;
border-radius: 1px;
}
img {
border: none;
width: 100%;
height: 100%;
}
.pressed {
background-color: var(--icon-bg-color-hover);
}
.disabled {
opacity: 0.3;
cursor: default;
}
</style>
<div title="title">
<img src="./images/logo.svg" alt="icon">
</div>
`;
/**
* @class ToolButton
*/
export class ToolButton extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
// locate the component
this.$div = this._shadowRoot.querySelector('div');
this.$img = this._shadowRoot.querySelector('img');
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'title', 'src', 'pressed', 'disabled', 'size', 'style' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'title':
{
const shortcut = this.getAttribute('shortcut');
this.$div.setAttribute('title', `${newValue} ${shortcut ? `[${shortcut}]` : ''}`);
}
break;
case 'style':
this.$div.style = newValue;
break;
case 'src':
this.$img.setAttribute('src', newValue);
break;
case 'pressed':
if (newValue) {
this.$div.classList.add('pressed');
} else {
this.$div.classList.remove('pressed');
}
break;
case 'size':
if (newValue === 'small') {
this.$div.classList.add('small');
} else {
this.$div.classList.remove('small');
}
break;
case 'disabled':
if (newValue) {
this.$div.classList.add('disabled');
} else {
this.$div.classList.remove('disabled');
}
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get title () {
return this.getAttribute('title');
}
/**
* @function set
* @returns {void}
*/
set title (value) {
this.setAttribute('title', value);
}
/**
* @function get
* @returns {any}
*/
get pressed () {
return this.hasAttribute('pressed');
}
/**
* @function set
* @returns {void}
*/
set pressed (value) {
// boolean value => existence = true
if (value) {
this.setAttribute('pressed', 'true');
} else {
this.removeAttribute('pressed', '');
}
}
/**
* @function get
* @returns {any}
*/
get disabled () {
return this.hasAttribute('disabled');
}
/**
* @function set
* @returns {void}
*/
set disabled (value) {
// boolean value => existence = true
if (value) {
this.setAttribute('disabled', 'true');
} else {
this.removeAttribute('disabled', '');
}
}
/**
* @function get
* @returns {any}
*/
get src () {
return this.getAttribute('src');
}
/**
* @function set
* @returns {void}
*/
set src (value) {
this.setAttribute('src', value);
}
/**
* @function get
* @returns {any}
*/
get size () {
return this.getAttribute('size');
}
/**
* @function set
* @returns {void}
*/
set size (value) {
this.setAttribute('size', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
// capture shortcuts
const shortcut = this.getAttribute('shortcut');
if (shortcut) {
// register the keydown event
document.addEventListener('keydown', (e) => {
// only track keyboard shortcuts for the body containing the SVG-Editor
if (e.target.nodeName !== 'BODY') return;
// normalize key
const key = `${(e.metaKey) ? 'meta+' : ''}${(e.ctrlKey) ? 'ctrl+' : ''}${e.key.toUpperCase()}`;
if (shortcut !== key) return;
// launch the click event
this.click();
e.preventDefault();
});
}
}
}
// Register
customElements.define('se-button', ToolButton);

View File

@@ -0,0 +1,807 @@
/* eslint-disable max-len */
/* gl#bals svgEditor */
import { jGraduate, jGraduateMethod } from './jgraduate/jQuery.jGraduate.js';
import PaintBox from './PaintBox.js';
const template = document.createElement('template');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<style>
.jPicker .Icon {
display: inline-block;
height: 24px;
position: relative;
text-align: left;
width: 25px
}
.jPicker .Icon span.Color, .jPicker .Icon span.Alpha {
background-position: 2px 2px;
display: block;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%
}
.jPicker .Icon span.Image {
background-repeat: no-repeat;
cursor: pointer;
display: block;
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%
}
.jPicker.Container {
z-index: 10
}
table.jPicker {
background-color: #efefef;
border: 1px outset #666;
font-family: Arial, Helvetica, Sans-Serif;
font-size: 12px!important;
margin: 0;
padding: 5px;
width: 545px;
z-index: 20
}
.jPicker .Move {
background-color: #ddd;
border-color: #fff #666 #666 #fff;
border-style: solid;
border-width: 1px;
cursor: move;
height: 12px;
padding: 0
}
.jPicker .Title {
font-size: 11px!important;
font-weight: bold;
margin: -2px 0 0 0;
padding: 0;
text-align: center;
width: 100%
}
.jPicker div.Map {
border-bottom: 2px solid #fff;
border-left: 2px solid #9a9a9a;
border-right: 2px solid #fff;
border-top: 2px solid #9a9a9a;
cursor: crosshair;
height: 260px;
margin: 0 5px 0 5px;
overflow: hidden;
padding: 0;
position: relative;
width: 260px
}
.jPicker div[class="Map"] {
height: 256px;
width: 256px
}
.jPicker div.Bar {
border-bottom: 2px solid #fff;
border-left: 2px solid #9a9a9a;
border-right: 2px solid #fff;
border-top: 2px solid #9a9a9a;
cursor: n-resize;
height: 260px;
margin: 12px 10px 0 5px;
overflow: hidden;
padding: 0;
position: relative;
width: 24px
}
.jPicker div[class="Bar"] {
height: 256px;
width: 20px
}
.jPicker .Map .Map1, .jPicker .Map .Map2, .jPicker .Map .Map3, .jPicker .Bar .Map1, .jPicker .Bar .Map2, .jPicker .Bar .Map3, .jPicker .Bar .Map4, .jPicker .Bar .Map5, .jPicker .Bar .Map6 {
background-color: transparent;
background-image: none;
display: block;
left: 0;
position: absolute;
top: 0
}
.jPicker .Map .Map1, .jPicker .Map .Map2, .jPicker .Map .Map3 {
height: 2596px;
width: 256px
}
.jPicker .Bar .Map1, .jPicker .Bar .Map2, .jPicker .Bar .Map3, .jPicker .Bar .Map4 {
height: 3896px;
width: 20px
}
.jPicker .Bar .Map5, .jPicker .Bar .Map6 {
height: 256px;
width: 20px
}
.jPicker .Map .Map1, .jPicker .Map .Map2, .jPicker .Bar .Map6 {
background-repeat: no-repeat
}
.jPicker .Map .Map3, .jPicker .Bar .Map5 {
background-repeat: repeat
}
.jPicker .Bar .Map1, .jPicker .Bar .Map2, .jPicker .Bar .Map3, .jPicker .Bar .Map4 {
background-repeat: repeat-x
}
.jPicker .Map .Arrow {
display: block;
position: absolute
}
.jPicker .Bar .Arrow {
display: block;
left: 0;
position: absolute
}
.jPicker .Preview {
font-size: 9px;
text-align: center
}
.jPicker .Preview div {
border: 2px inset #eee;
height: 62px;
margin: 0 auto;
padding: 0;
width: 62px
}
.jPicker .Preview div span {
border: 1px solid #000;
display: block;
height: 30px;
margin: 0 auto;
padding: 0;
width: 60px
}
.jPicker .Preview .Active {
border-bottom-width: 0
}
.jPicker .Preview .Current {
border-top-width: 0;
cursor: pointer
}
.jPicker .Button {
text-align: center;
width: 115px
}
.jPicker .Button input {
width: 100px
}
.jPicker .Button .Ok {
margin: 12px 0 5px 0
}
.jPicker td.Radio {
margin: 0;
padding: 0;
width: 31px
}
.jPicker td.Radio input {
margin: 0 5px 0 0;
padding: 0
}
.jPicker td.Text {
font-size: 12px!important;
height: 22px;
margin: 0;
padding: 0;
text-align: left;
width: 70px
}
.jPicker tr.Hex td.Text {
width: 100px
}
.jPicker td.Text input {
background-color: #fff;
border: 1px inset #aaa;
height: 19px;
margin: 0 0 0 5px;
text-align: left;
width: 30px
}
.jPicker td[class="Text"] input {
height: 15px
}
.jPicker tr.Hex td.Text input.Hex {
width: 50px
}
.jPicker tr.Hex td.Text input.AHex {
width: 20px
}
.jPicker .Grid {
text-align: center;
width: 114px
}
.jPicker .Grid span.QuickColor {
border: 1px inset #aaa;
cursor: pointer;
display: inline-block;
height: 15px;
line-height: 15px;
margin: 0;
padding: 0;
width: 19px
}
.jPicker .Grid span[class="QuickColor"] {
width: 17px
}
/*
* jGraduate Default CSS
*
* Copyright (c) 2010 Jeff Schiller
* http://blog.codedread.com/
*
* Copyright (c) 2010 Alexis Deveria
* http://a.deveria.com/
*
* Licensed under the MIT License
*/
h2.jGraduate_Title {
font-family: Arial, Helvetica, Sans-Serif;
font-size: 11px !important;
font-weight: bold;
margin: -13px 0px 0px 0px;
padding: 0px;
text-align: center;
}
.jGraduate_Picker {
font-family: Arial, Helvetica, Sans-Serif;
font-size: 12px;
border-style: solid;
border-color: lightgrey black black lightgrey;
border-width: 1px;
background-color: #EFEFEF;
position: absolute;
padding: 10px;
}
.jGraduate_tabs li {
background-color: #ccc;
display: inline;
border: solid 1px grey;
padding: 3px;
margin: 2px;
cursor: pointer;
}
li.jGraduate_tab_current {
background-color: #EFEFEF;
display: inline;
padding: 3px;
margin: 2px;
border: solid 1px black;
cursor: pointer;
}
.jGraduate_colPick {
display: none;
}
.jGraduate_gradPick {
display: none;
border: outset 1px #666;
padding: 10px 7px 5px 5px;
overflow: auto;
}
.jGraduate_gradPick {
display: none;
border: outset 1px #666;
padding: 10px 7px 5px 5px;
overflow: auto;
/* position: relative;*/
}
.jGraduate_tabs {
position: relative;
background-color: #EFEFEF;
padding: 0px;
margin: 0px;
margin-bottom: 5px;
}
div.jGraduate_Swatch {
float: left;
margin: 8px;
}
div.jGraduate_GradContainer {
border: 2px inset #EEE;
background-image: url(./components/jgraduate/images/map-opacity.png);
background-position: 0px 0px;
height: 256px;
width: 256px;
position: relative;
}
div.jGraduate_GradContainer div.grad_coord {
background: #000;
border: 1px solid #fff;
border-radius: 5px;
-moz-border-radius: 5px;
width: 10px;
height: 10px;
position: absolute;
margin: -5px -5px;
top: 0;
left: 0;
text-align: center;
font-size: xx-small;
line-height: 10px;
color: #fff;
text-decoration: none;
cursor: pointer;
-moz-user-select: none;
-webkit-user-select: none;
}
.jGraduate_AlphaArrows {
position: absolute;
margin-top: -10px;
margin-left: 250.5px;
}
div.jGraduate_Opacity {
border: 2px inset #eee;
margin-top: 14px;
background-color: black;
background-image: url(../images/Maps.png);
background-position: 0px -2816px;
height: 20px;
cursor: ew-resize;
}
div.jGraduate_StopSlider {
/* border: 2px inset #eee;*/
margin: 0 0 0 -10px;
width: 276px;
overflow: visible;
background: #efefef;
height: 45px;
cursor: pointer;
}
div.jGraduate_StopSection {
width: 120px;
text-align: center;
}
input.jGraduate_Ok, input.jGraduate_Cancel {
display: block;
width: 100px;
margin-left: -4px;
margin-right: -4px;
}
input.jGraduate_Ok {
margin: 9px -4px 5px -4px;
}
.colorBox {
float: left;
height: 16px;
width: 16px;
border: 1px solid #808080;
cursor: pointer;
margin: 4px 4px 4px 30px;
}
.colorBox + label {
float: left;
margin-top: 7px;
}
label.jGraduate_Form_Heading {
position: relative;
top: 10px;
background-color: #EFEFEF;
padding: 2px;
font-weight: bold;
font-size: 13px;
}
div.jGraduate_Form_Section {
border-style: solid;
border-width: 1px;
border-color: grey;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
padding: 15px 5px 5px 5px;
margin: 5px 2px;
width: 110px;
text-align: center;
overflow: auto;
}
div.jGraduate_Form_Section label {
padding: 0 2px;
}
div.jGraduate_StopSection input[type=text],
div.jGraduate_Slider input[type=text] {
width: 33px;
}
div.jGraduate_LightBox {
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: #000;
opacity: 0.5;
display: none;
}
div.jGraduate_stopPicker {
position: absolute;
display: none;
background: #E8E8E8;
}
.jGraduate_gradPick {
width: 535px;
}
.jGraduate_gradPick div.jGraduate_OpacField {
position: absolute;
left: 0;
bottom: 5px;
/*
width: 270px;
left: 284px;
width: 266px;
height: 200px;
top: 167px;
margin: -3px 3px 0px 4px;
*/
}
.jGraduate_gradPick .jGraduate_Form {
float: left;
width: 270px;
position: absolute;
left: 284px;
width: 266px;
height: 200px;
top: 167px;
margin: -3px 3px 0px 10px;
}
.jGraduate_gradPick .jGraduate_Points {
position: static;
width: 150px;
margin-left: 0;
}
.jGraduate_SpreadMethod {
position: absolute;
right: 8px;
top: 100px;
}
.jGraduate_Colorblocks {
display: table;
border-spacing: 0 5px;
}
.jGraduate_colorblock {
display: table-row;
}
.jGraduate_Colorblocks .jGraduate_colorblock > * {
display: table-cell;
vertical-align: middle;
margin: 0;
float: none;
}
.jGraduate_gradPick div.jGraduate_StopSection {
float: left;
width: 133px;
margin-top: -8px;
}
.jGraduate_gradPick .jGraduate_Form_Section {
padding-top: 9px;
}
.jGraduate_Slider {
text-align: center;
float: left;
width: 100%;
}
.jGraduate_Slider .jGraduate_Form_Section {
border: none;
width: 250px;
padding: 0 2px;
overflow: visible;
}
.jGraduate_Slider label {
display: inline-block;
float: left;
line-height: 50px;
padding: 0;
}
.jGraduate_Slider label.prelabel {
width: 40px;
text-align: left;
}
.jGraduate_SliderBar {
width: 140px;
float: left;
margin-right: 5px;
border:1px solid #BBB;
height:20px;
margin-top:14px;
margin-left:5px;
position: relative;
}
div.jGraduate_Slider input {
margin-top: 5px;
}
div.jGraduate_Slider img {
top: 0;
left: 0;
position: absolute;
margin-top: -10px;
cursor:ew-resize;
}
.jGraduate_gradPick .jGraduate_OkCancel {
position: absolute;
top: 39px;
right: 10px;
width: 113px;
}
.jGraduate_OpacField {
position: absolute;
right: -10px;
bottom: 0;
}
#logo {
height: 18px;
width: 18px;
}
#block {
height: 13px;
width: 14px;
float: right;
background-color: darkgrey;
}
#picker {
background: var(--input-color);
height: 19px;
line-height: 19px;
border-radius: 3px;
width: 52px;
display: flex;
align-items: center;
margin-right: 4px;
margin-top: 1px;
justify-content: space-evenly;
}
#color_picker {
z-index: 1000;
top: -350px;
}
</style>
<div id="picker">
<img src="./images/logo.svg" alt="icon" id="logo">
<label for="color" title="#svgEditor.i18next.t('config.change_xxx_color')}" id="label"></label>
<div id="block">
</div>
</div>
<!-- hidden div -->
<div id="color_picker"></div>
`;
/**
* @class SeColorPicker
*/
export class SeColorPicker extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$logo = this._shadowRoot.getElementById('logo');
this.$label = this._shadowRoot.getElementById('label');
this.$block = this._shadowRoot.getElementById('block');
this.paintBox = null;
this.$picker = this._shadowRoot.getElementById('picker');
this.$color_picker = this._shadowRoot.getElementById('color_picker');
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'label', 'src', 'type' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'src':
this.$logo.setAttribute('src', newValue);
break;
case 'label':
this.setAttribute('title', newValue);
break;
case 'type':
this.$label.setAttribute('title', 'config.pick_paint_opavity');
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get label () {
return this.$label.getAttribute('title');
}
/**
* @function set
* @returns {void}
*/
set label (value) {
this.setAttribute('label', value);
}
/**
* @function get
* @returns {any}
*/
get type () {
return this.getAttribute('type');
}
/**
* @function set
* @returns {void}
*/
set type (value) {
this.setAttribute('type', value);
}
/**
* @function get
* @returns {any}
*/
get src () {
return this.getAttribute('src');
}
/**
* @function set
* @returns {void}
*/
set src (value) {
this.setAttribute('src', value);
}
/**
* @param {PlainObject} svgCanvas
* @param {PlainObject} selectedElement
* @param {bool} apply
* @returns {void}
*/
update (svgCanvas, selectedElement, apply) {
const paint = this.paintBox.update(svgCanvas, selectedElement);
if (paint && apply) {
const changeEvent = new CustomEvent('change', { detail: {
paint
} });
this.dispatchEvent(changeEvent);
}
}
/**
* @param {PlainObject} paint
* @returns {void}
*/
setPaint (paint) {
this.paintBox.setPaint(paint);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
this.paintBox = new PaintBox(this.$block, this.type);
this.$picker.addEventListener('click', () => {
let { paint } = this.paintBox;
jGraduateMethod(
this.$color_picker,
{
images: { clientPath: './components/jgraduate/images/' },
paint,
window: { pickerTitle: this.label },
newstop: 'inverse'
},
(p) => {
paint = new jGraduate.Paint(p);
this.setPaint(paint);
const changeEvent = new CustomEvent('change', { detail: {
paint
} });
this.dispatchEvent(changeEvent);
this.$color_picker.style.display = 'none';
},
() => {
this.$color_picker.style.display = 'none';
}
);
});
}
}
// Register
customElements.define('se-colorpicker', SeColorPicker);

View File

@@ -0,0 +1,178 @@
import ListComboBox from 'elix/define/ListComboBox.js';
import { defaultState } from 'elix/src/base/internal.js';
import { templateFrom, fragmentFrom } from 'elix/src/core/htmlLiterals.js';
import { internal } from 'elix';
import NumberSpinBox from '../dialogs/se-elix/define/NumberSpinBox.js';
/**
* @class Dropdown
*/
class Dropdown extends ListComboBox {
/**
* @function get
* @returns {PlainObject}
*/
get [defaultState] () {
return Object.assign(super[defaultState], {
inputPartType: NumberSpinBox,
src: './images/logo.svg',
inputsize: '100%'
});
}
/**
* @function get
* @returns {PlainObject}
*/
get [internal.template] () {
const result = super[internal.template];
const source = result.content.getElementById('source');
// add a icon before our dropdown
source.prepend(fragmentFrom.html`
<img src="./images/logo.svg" alt="icon" width="18" height="18"></img>
`.cloneNode(true));
// change the style so it fits in our toolbar
result.content.append(
templateFrom.html`
<style>
[part~="source"] {
grid-template-columns: 20px 1fr auto;
}
::slotted(*) {
padding: 4px;
background: #E8E8E8;
border: 1px solid #5a6162;
width: 100%;
}
[part~="popup"] {
width: 150%;
}
</style>
`.content
);
return result;
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'title', 'src', 'inputsize', 'value' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'title':
// this.$span.setAttribute('title', `${newValue} ${shortcut ? `[${shortcut}]` : ''}`);
break;
case 'src':
this.src = newValue;
break;
case 'inputsize':
this.inputsize = newValue;
break;
default:
super.attributeChangedCallback(name, oldValue, newValue);
break;
}
}
/**
* @function [internal.render]
* @param {PlainObject} changed
* @returns {void}
*/
[internal.render] (changed) {
super[internal.render](changed);
if (this[internal.firstRender]) {
this.$img = this.shadowRoot.querySelector('img');
this.$input = this.shadowRoot.getElementById('input');
}
if (changed.src) {
this.$img.setAttribute('src', this[internal.state].src);
}
if (changed.inputsize) {
this.$input.shadowRoot.querySelector('[part~="input"]').style.width = this[internal.state].inputsize;
}
if (changed.inputPartType) {
// Wire up handler on new input.
this.addEventListener('close', (e) => {
e.preventDefault();
const value = e.detail?.closeResult?.getAttribute('value');
if (value) {
const closeEvent = new CustomEvent('change', { detail: { value } });
this.dispatchEvent(closeEvent);
}
});
}
}
/**
* @function src
* @returns {string} src
*/
get src () {
return this[internal.state].src;
}
/**
* @function src
* @returns {void}
*/
set src (src) {
this[internal.setState]({ src });
}
/**
* @function inputsize
* @returns {string} src
*/
get inputsize () {
return this[internal.state].inputsize;
}
/**
* @function src
* @returns {void}
*/
set inputsize (inputsize) {
this[internal.setState]({ inputsize });
}
/**
* @function value
* @returns {string} src
*/
get value () {
return this[internal.state].value;
}
/**
* @function value
* @returns {void}
*/
set value (value) {
this[internal.setState]({ value });
}
}
// Register
customElements.define('se-dropdown', Dropdown);
/*
{TODO
min: 0.001, max: 10000, step: 50, stepfunc: stepZoom,
function stepZoom (elem, step) {
const origVal = Number(elem.value);
if (origVal === 0) { return 100; }
const sugVal = origVal + step;
if (step === 0) { return origVal; }
if (origVal >= 100) {
return sugVal;
}
if (sugVal >= origVal) {
return origVal * 2;
}
return origVal / 2;
}
*/

View File

@@ -0,0 +1,315 @@
/* eslint-disable no-unsanitized/property */
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
position:relative;
}
.menu-button:hover, se-button:hover, .menu-item:hover
{
background-color: var(--icon-bg-color-hover);
}
img {
border: none;
width: 24px;
height: 24px;
}
.overall.pressed .button-icon,
.overall.pressed,
.menu-item.pressed {
background-color: var(--icon-bg-color-hover) !important;
}
.overall.pressed .menu-button {
background-color: var(--icon-bg-color-hover) !important;
}
.disabled {
opacity: 0.3;
cursor: default;
}
.menu-button {
height: 24px;
width: 24px;
margin: 2px 1px 4px;
padding: 3px;
background-color: var(--icon-bg-color);
cursor: pointer;
position: relative;
border-radius: 3px;
overflow: hidden;
}
.handle {
height: 8px;
width: 8px;
background-image: url(./images/handle.svg);
position:absolute;
bottom: 0px;
right: 0px;
}
.button-icon {
}
.menu {
position: absolute;
top:0px;
left:204px;
background: none !important;
display:none;
}
.image-lib {
position: absolute;
top: 0px;
left:34px;
background: #E8E8E8;
display: none;
flex-wrap: wrap;
flex-direction: row;
width: 170px;
}
.menu-item {
line-height: 1em;
padding: 0.5em;
border: 1px solid #5a6162;
background: #E8E8E8;
margin-bottom: -1px;
white-space: nowrap;
}
.open-lib {
display: inline-flex;
}
.open {
display: block;
}
.overall {
background: none !important;
}
</style>
<div class="overall">
<div class="menu-button">
<img class="button-icon" src="./images/logo.svg" alt="icon">
<div class="handle"></div>
</div>
<div class="image-lib"">
<se-button></se-button>
</div>
<div class="menu">
<div class="menu-item">menu</div>
</div>
</div>
`;
/**
* @class ExplorerButton
*/
export class ExplorerButton extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
// locate the component
this.$button = this._shadowRoot.querySelector('.menu-button');
this.$overall = this._shadowRoot.querySelector('.overall');
this.$img = this._shadowRoot.querySelector('.menu-button img');
this.$menu = this._shadowRoot.querySelector('.menu');
this.$handle = this._shadowRoot.querySelector('.handle');
this.$lib = this._shadowRoot.querySelector('.image-lib');
this.files = [];
this.request = new XMLHttpRequest();
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'title', 'pressed', 'disabled', 'lib', 'src' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
async attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'title':
{
const shortcut = this.getAttribute('shortcut');
this.$button.setAttribute('title', `${newValue} [${shortcut}]`);
}
break;
case 'pressed':
if (newValue) {
this.$overall.classList.add('pressed');
} else {
this.$overall.classList.remove('pressed');
}
break;
case 'disabled':
if (newValue) {
this.$div.classList.add('disabled');
} else {
this.$div.classList.remove('disabled');
}
break;
case 'lib':
try {
const response = await fetch(`${newValue}index.json`);
const json = await response.json();
const { lib } = json;
this.$menu.innerHTML = lib.map((menu, i) => (
`<div data-menu="${menu}" class="menu-item ${(i === 0) ? 'pressed' : ''} ">${menu}</div>`
)).join('');
await this.updateLib(lib[0]);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
break;
case 'src':
this.$img.setAttribute('src', newValue);
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get title () {
return this.getAttribute('title');
}
/**
* @function set
* @returns {void}
*/
set title (value) {
this.setAttribute('title', value);
}
/**
* @function get
* @returns {any}
*/
get pressed () {
return this.hasAttribute('pressed');
}
/**
* @function set
* @returns {void}
*/
set pressed (value) {
// boolean value => existence = true
if (value) {
this.setAttribute('pressed', 'true');
} else {
this.removeAttribute('pressed', '');
}
}
/**
* @function get
* @returns {any}
*/
get disabled () {
return this.hasAttribute('disabled');
}
/**
* @function set
* @returns {void}
*/
set disabled (value) {
// boolean value => existence = true
if (value) {
this.setAttribute('disabled', 'true');
} else {
this.removeAttribute('disabled', '');
}
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
// capture click event on the button to manage the logic
const onClickHandler = (ev) => {
ev.stopPropagation();
switch (ev.target.nodeName) {
case 'SE-EXPLORERBUTTON':
this.$menu.classList.add('open');
this.$lib.classList.add('open-lib');
break;
case 'SE-BUTTON':
// change to the current action
this.currentAction = ev.target;
this.$img.setAttribute('src', this.currentAction.getAttribute('src'));
this.dataset.draw = this.data[this.currentAction.dataset.shape];
this._shadowRoot.querySelectorAll('.image-lib [pressed]').forEach((b) => { b.pressed = false; });
this.currentAction.setAttribute('pressed', 'pressed');
// and close the menu
this.$menu.classList.remove('open');
this.$lib.classList.remove('open-lib');
break;
case 'DIV':
if (ev.target.classList[0] === 'handle') {
// this is a click on the handle so let's open/close the menu.
this.$menu.classList.toggle('open');
this.$lib.classList.toggle('open-lib');
} else {
this._shadowRoot.querySelectorAll('.menu > .pressed').forEach((b) => { b.classList.remove('pressed'); });
ev.target.classList.add('pressed');
this.updateLib(ev.target.dataset.menu);
}
break;
default:
// eslint-disable-next-line no-console
console.error('unknown nodeName for:', ev.target, ev.target.className);
}
};
// capture event from slots
this.addEventListener('click', onClickHandler);
this.$menu.addEventListener('click', onClickHandler);
this.$lib.addEventListener('click', onClickHandler);
this.$handle.addEventListener('click', onClickHandler);
}
/**
* @function updateLib
* @param {string} lib
* @returns {void}
*/
async updateLib (lib) {
const libDir = this.getAttribute('lib');
try {
// initialize buttons for all shapes defined for this library
const response = await fetch(`${libDir}${lib}.json`);
const json = await response.json();
this.data = json.data;
const size = json.size ?? 300;
const fill = json.fill ? '#333' : 'none';
const off = size * 0.05;
const vb = [ -off, -off, size + off * 2, size + off * 2 ].join(' ');
const stroke = json.fill ? 0 : (size / 30);
this.$lib.innerHTML = Object.entries(this.data).map(([ key, path ]) => {
const encoded = btoa(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<svg viewBox="${vb}"><path fill="${fill}" stroke="#000" stroke-width="${stroke}" d="${path}"></path></svg>
</svg>`);
return `<se-button data-shape="${key}"src="data:image/svg+xml;base64,${encoded}"></se-button>`;
}).join('');
} catch (error) {
// eslint-disable-next-line no-console
console.error(`could not read file:${libDir}${lib}.json`, error);
}
}
}
// Register
customElements.define('se-explorerbutton', ExplorerButton);

View File

@@ -0,0 +1,279 @@
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
position:relative;
}
.overall:hover *
{
background-color: var(--icon-bg-color-hover);
}
img {
border: none;
width: 24px;
height: 24px;
}
.overall.pressed .button-icon,
.overall.pressed .handle {
background-color: var(--icon-bg-color-hover) !important;
}
.overall.pressed .menu-button {
background-color: var(--icon-bg-color-hover) !important;
}
.disabled {
opacity: 0.3;
cursor: default;
}
.menu-button {
height: 24px;
width: 24px;
margin: 2px 1px 4px;
padding: 3px;
background-color: var(--icon-bg-color);
cursor: pointer;
position: relative;
border-radius: 3px;
overflow: hidden;
}
.handle {
height: 8px;
width: 8px;
background-image: url(./images/handle.svg);
position:absolute;
bottom: 0px;
right: 0px;
}
.button-icon {
}
.menu {
position: absolute;
top:-2px;
left:32px;
background: none !important;
display:none;
}
.open {
display: flex;
}
.menu-item {
align-content: flex-start;
height: 24px;
width: 24px;
top:0px;
left:0px;
}
.overall {
background: none !important;
}
</style>
<div class="overall">
<div class="menu-button">
<img class="button-icon" src="./images/logo.svg" alt="icon">
<div class="handle"></div>
</div>
<div class="menu">
<slot></slot>
</div>
</div>
`;
/**
* @class FlyingButton
*/
export class FlyingButton extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
// locate the component
this.$button = this._shadowRoot.querySelector('.menu-button');
this.$handle = this._shadowRoot.querySelector('.handle');
this.$overall = this._shadowRoot.querySelector('.overall');
this.$img = this._shadowRoot.querySelector('img');
this.$menu = this._shadowRoot.querySelector('.menu');
// the last element of the div is the slot
// we retrieve all elements added in the slot (i.e. se-buttons)
this.$elements = this.$menu.lastElementChild.assignedElements();
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'title', 'pressed', 'disabled', 'opened' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'title':
{
const shortcut = this.getAttribute('shortcut');
this.$button.setAttribute('title', `${newValue} [${shortcut}]`);
}
break;
case 'pressed':
if (newValue) {
this.$overall.classList.add('pressed');
} else {
this.$overall.classList.remove('pressed');
}
break;
case 'opened':
if (newValue) {
this.$menu.classList.add('open');
} else {
this.$menu.classList.remove('open');
}
break;
case 'disabled':
if (newValue) {
this.$div.classList.add('disabled');
} else {
this.$div.classList.remove('disabled');
}
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get title () {
return this.getAttribute('title');
}
/**
* @function set
* @returns {void}
*/
set title (value) {
this.setAttribute('title', value);
}
/**
* @function get
* @returns {any}
*/
get pressed () {
return this.hasAttribute('pressed');
}
/**
* @function set
* @returns {void}
*/
set pressed (value) {
// boolean value => existence = true
if (value) {
this.setAttribute('pressed', 'true');
} else {
this.removeAttribute('pressed', '');
// close also the menu if open
this.removeAttribute('opened');
}
}
/**
* @function get
* @returns {any}
*/
get opened () {
return this.hasAttribute('opened');
}
/**
* @function set
* @returns {void}
*/
set opened (value) {
// boolean value => existence = true
if (value) {
this.setAttribute('opened', 'opened');
} else {
this.removeAttribute('opened');
}
}
/**
* @function get
* @returns {any}
*/
get disabled () {
return this.hasAttribute('disabled');
}
/**
* @function set
* @returns {void}
*/
set disabled (value) {
// boolean value => existence = true
if (value) {
this.setAttribute('disabled', 'true');
} else {
this.removeAttribute('disabled', '');
}
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
// initialize currentAction with the first slot of the list
this.activeSlot = this.shadowRoot.querySelector('slot').assignedElements()[0];
this.$img.setAttribute('src', this.activeSlot.getAttribute('src'));
// capture click event on the button to manage the logic
const onClickHandler = (ev) => {
ev.stopPropagation();
switch (ev.target.nodeName) {
case 'SE-FLYINGBUTTON':
if (this.pressed) {
this.setAttribute('opened', 'opened');
} else {
// launch current action
this.activeSlot.click();
this.setAttribute('pressed', 'pressed');
}
break;
case 'SE-BUTTON':
// change to the current action
this.$img.setAttribute('src', ev.target.getAttribute('src'));
this.activeSlot = ev.target;
this.setAttribute('pressed', 'pressed');
// and close the menu
this.$menu.classList.remove('open');
break;
case 'DIV':
// this is a click on the handle so let's open/close the menu.
if (this.opened) {
this.removeAttribute('opened');
} else {
this.setAttribute('opened', 'opened');
}
break;
default:
// eslint-disable-next-line no-console
console.error('unkonw nodeName for:', ev.target, ev.target.className);
}
};
// capture event from slots
this.addEventListener('click', onClickHandler);
this.$handle.addEventListener('click', onClickHandler);
}
}
// Register
customElements.define('se-flyingbutton', FlyingButton);

View File

@@ -0,0 +1,158 @@
import 'elix/define/Input.js';
const template = document.createElement('template');
template.innerHTML = `
<style>
img {
top: 2px;
left: 4px;
position: relative;
}
span {
bottom: 1px;
right: -4px;
position: relative;
margin-right: 4px;
color: #fff;
}
elix-input {
background-color: var(--input-color);
border-radius: 3px;
}
</style>
<img src="./images/logo.svg" alt="icon" width="12" height="12" />
<span id="label">label</span>
<elix-input></elix-input>
`;
/**
* @class SEInput
*/
export class SEInput extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
// locate the component
this.$img = this._shadowRoot.querySelector('img');
this.$label = this.shadowRoot.getElementById('label');
this.$event = new CustomEvent('change');
this.$input = this._shadowRoot.querySelector('elix-input');
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'value', 'label', 'src', 'size' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'src':
this.$img.setAttribute('src', newValue);
this.$label.remove();
break;
case 'size':
this.$input.setAttribute('size', newValue);
break;
case 'label':
this.$label.textContent = newValue;
this.$img.remove();
break;
case 'value':
this.$input.value = newValue;
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get label () {
return this.getAttribute('label');
}
/**
* @function set
* @returns {void}
*/
set label (value) {
this.setAttribute('label', value);
}
/**
* @function get
* @returns {any}
*/
get value () {
return this.$input.value;
}
/**
* @function set
* @returns {void}
*/
set value (value) {
this.$input.value = value;
}
/**
* @function get
* @returns {any}
*/
get src () {
return this.getAttribute('src');
}
/**
* @function set
* @returns {void}
*/
set src (value) {
this.setAttribute('src', value);
}
/**
* @function get
* @returns {any}
*/
get size () {
return this.getAttribute('size');
}
/**
* @function set
* @returns {void}
*/
set size (value) {
this.setAttribute('size', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
this.addEventListener('change', (e) => {
e.preventDefault();
this.value = e.target.value;
});
this.dispatchEvent(this.$event);
}
}
// Register
customElements.define('se-input', SEInput);

View File

@@ -0,0 +1,140 @@
import 'elix/define/DropdownList.js';
const template = document.createElement('template');
template.innerHTML = `
<style>
elix-dropdown-list {
margin: 1px;
}
elix-dropdown-list:hover {
background-color: var(--icon-bg-color-hover);
}
::part(popup-toggle) {
display: none;
}
::slotted(*) {
padding:0;
width:100%;
}
</style>
<label>Label</label>
<elix-dropdown-list>
<slot></slot>
</elix-dropdown-list>
`;
/**
* @class SeList
*/
export class SeList extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$dropdown = this._shadowRoot.querySelector('elix-dropdown-list');
this.$label = this._shadowRoot.querySelector('label');
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'label', 'width', 'height' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'label':
this.$label.textContent = newValue;
break;
case 'height':
this.$dropdown.style.height = newValue;
break;
case 'width':
this.$dropdown.style.width = newValue;
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get label () {
return this.getAttribute('label');
}
/**
* @function set
* @returns {void}
*/
set label (value) {
this.setAttribute('label', value);
}
/**
* @function get
* @returns {any}
*/
get width () {
return this.getAttribute('width');
}
/**
* @function set
* @returns {void}
*/
set width (value) {
this.setAttribute('width', value);
}
/**
* @function get
* @returns {any}
*/
get height () {
return this.getAttribute('height');
}
/**
* @function set
* @returns {void}
*/
set height (value) {
this.setAttribute('height', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
const currentObj = this;
this.$dropdown.addEventListener('selectedindexchange', (e) => {
e.preventDefault();
if (e?.detail?.selectedIndex !== undefined) {
const value = this.$dropdown.selectedItem.getAttribute('value');
const closeEvent = new CustomEvent('change', { detail: { value } });
currentObj.dispatchEvent(closeEvent);
currentObj.value = value;
}
});
}
}
// Register
customElements.define('se-list', SeList);

View File

@@ -0,0 +1,80 @@
import 'elix/define/Option.js';
const template = document.createElement('template');
template.innerHTML = `
<style>
elix-option{
padding:0.25rem 0.125rem !important;
background-color: var(--icon-bg-color);
}
elix-option:hover{
background-color: var(--icon-bg-color-hover);
}
</style>
<elix-option aria-label="option">
<slot></slot>
</elix-option>
`;
/**
* @class SeMenu
*/
export class SeListItem extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$menuitem = this._shadowRoot.querySelector('elix-option');
this.$svg = this.$menuitem.shadowRoot.querySelector('#checkmark');
this.$svg.setAttribute('style', 'display: none;');
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'option' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
// eslint-disable-next-line sonarjs/no-small-switch
switch (name) {
case 'option':
this.$menuitem.setAttribute('option', newValue);
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get option () {
return this.getAttribute('option');
}
/**
* @function set
* @returns {void}
*/
set option (value) {
this.setAttribute('option', value);
}
}
// Register
customElements.define('se-list-item', SeListItem);

View File

@@ -0,0 +1,127 @@
import 'elix/define/MenuItem.js';
import './sePlainMenuButton.js';
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
padding: 0px;
}
elix-menu-button::part(menu) {
background-color: var(--icon-bg-color) !important;
color: #fff;
}
elix-menu-button::part(popup-toggle) {
padding: 0.25em 0.30em !important
}
:host ::slotted([current]){
background-color: var(--icon-bg-color-hover) !important;
color: #fff;
}
:host ::slotted(*){
padding: 0.25em 1.25em 0.25em 0.25em !important;
margin: 2px;
}
</style>
<elix-menu-button id="MenuButton" aria-label="Main Menu">
<slot></slot>
</elix-menu-button>
`;
/**
* @class SeMenu
*/
export class SeMenu extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$menu = this._shadowRoot.querySelector('elix-menu-button');
this.$label = this.$menu.shadowRoot.querySelector('#popupToggle').shadowRoot;
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'label', 'src' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
const image = new Image();
if (oldValue === newValue) return;
switch (name) {
case 'src':
image.src = newValue;
image.width = 24;
image.height = 24;
this.$label.prepend(image);
break;
case 'label':
this.$label.prepend(newValue);
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get label () {
return this.getAttribute('label');
}
/**
* @function set
* @returns {void}
*/
set label (value) {
this.setAttribute('label', value);
}
/**
* @function get
* @returns {any}
*/
get src () {
return this.getAttribute('src');
}
/**
* @function set
* @returns {void}
*/
set src (value) {
this.setAttribute('src', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
/* connectedCallback () {
this.$menu.addEventListener('openedchange', (e) => {
e.preventDefault();
const selectedItem = e?.detail?.closeResult;
if (selectedItem !== undefined && selectedItem?.id !== undefined) {
document.getElementById(selectedItem.id).click();
}
});
} */
}
// Register
customElements.define('se-menu', SeMenu);

View File

@@ -0,0 +1,122 @@
import 'elix/define/Menu.js';
import 'elix/define/MenuItem.js';
const template = document.createElement('template');
template.innerHTML = `
<style>
</style>
<elix-menu-item>
<div style="display:flex; align-items: center;">
<img src="./images/logo.svg" alt="icon" style="display:none;" width="24"/>
<span style="margin-left: 7px;"></span>
</div>
</elix-menu-item>
`;
/**
* @class SeMenuItem
*/
export class SeMenuItem extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$img = this._shadowRoot.querySelector('img');
this.$label = this._shadowRoot.querySelector('span');
this.$menuitem = this._shadowRoot.querySelector('elix-menu-item');
this.$svg = this.$menuitem.shadowRoot.querySelector('#checkmark');
this.$svg.setAttribute('style', 'display: none;');
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'label', 'src' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
let shortcut = '';
if (oldValue === newValue) return;
switch (name) {
case 'src':
this.$img.setAttribute('src', newValue);
this.$img.style.display = 'inline-block';
break;
case 'label':
shortcut = this.getAttribute('shortcut');
this.$label.textContent = `${newValue} ${shortcut ? `(${shortcut})` : ''}`;
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get label () {
return this.getAttribute('label');
}
/**
* @function set
* @returns {void}
*/
set label (value) {
this.setAttribute('label', value);
}
/**
* @function get
* @returns {any}
*/
get src () {
return this.getAttribute('src');
}
/**
* @function set
* @returns {void}
*/
set src (value) {
this.setAttribute('src', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
// capture shortcuts
const shortcut = this.getAttribute('shortcut');
if (shortcut) {
// register the keydown event
document.addEventListener('keydown', (e) => {
// only track keyboard shortcuts for the body containing the SVG-Editor
if (e.target.nodeName !== 'BODY') return;
// normalize key
const key = `${(e.metaKey) ? 'meta+' : ''}${(e.ctrlKey) ? 'ctrl+' : ''}${e.key.toUpperCase()}`;
if (shortcut !== key) return;
// launch the click event
if (this.id) {
document.getElementById(this.id).click();
}
e.preventDefault();
});
}
}
}
// Register
customElements.define('se-menu-item', SeMenuItem);

View File

@@ -0,0 +1,139 @@
/* eslint-disable max-len */
/* gl#bals svgEditor */
const palette = [
// Todo: Make into configuration item?
'none', '#000000', '#3f3f3f', '#7f7f7f', '#bfbfbf', '#ffffff',
'#ff0000', '#ff7f00', '#ffff00', '#7fff00',
'#00ff00', '#00ff7f', '#00ffff', '#007fff',
'#0000ff', '#7f00ff', '#ff00ff', '#ff007f',
'#7f0000', '#7f3f00', '#7f7f00', '#3f7f00',
'#007f00', '#007f3f', '#007f7f', '#003f7f',
'#00007f', '#3f007f', '#7f007f', '#7f003f',
'#ffaaaa', '#ffd4aa', '#ffffaa', '#d4ffaa',
'#aaffaa', '#aaffd4', '#aaffff', '#aad4ff',
'#aaaaff', '#d4aaff', '#ffaaff', '#ffaad4'
];
const template = document.createElement('template');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<style>
.square {
height: 15px;
width: 15px;
float: left;
}
#palette_holder {
overflow: hidden;
margin-top: 5px;
padding: 5px;
position: absolute;
right: 15px;
height: 16px;
background: #f0f0f0;
border-radius: 3px;
z-index: 2;
}
#js-se-palette {
float: left;
width: 632px;
height: 16px;
}
div.palette_item {
height: 15px;
width: 15px;
float: left;
}
div.palette_item:first-child {
background: white;
}
@media screen and (max-width:1100px) {
#palette_holder {
left: 410px;
overflow-x: scroll;
padding: 0 5px;
margin-top: 2px;
height: 30px;
}
}
@media screen and (max-width:1250px) {
#palette_holder {
left: 560px;
overflow-x: scroll;
padding: 0 5px;
margin-top: 2px;
height: 30px;
}
}
@media screen and (max-width:540px) {
#palette_holder {
left: 0px;
overflow-x: scroll;
padding: 0 5px;
margin-top: 32px;
height: 30px;
}
}
</style>
<div id="palette_holder" title="$
#{svgEditor.i18next.t('ui.palette_info')}">
<div id="js-se-palette">
</div>
</div>
`;
/**
* @class SEPalette
*/
export class SEPalette extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$strip = this._shadowRoot.querySelector('#js-se-palette');
palette.forEach((rgb) => {
const newDiv = document.createElement('div');
newDiv.classList.add('square');
if(rgb === 'none') {
const img = document.createElement('img');
img.src = `data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgY2xhc3M9InN2Z19pY29uIj48c3ZnIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgICA8bGluZSBmaWxsPSJub25lIiBzdHJva2U9IiNkNDAwMDAiIGlkPSJzdmdfOTAiIHkyPSIyNCIgeDI9IjI0IiB5MT0iMCIgeDE9IjAiLz4KICAgIDxsaW5lIGlkPSJzdmdfOTIiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2Q0MDAwMCIgeTI9IjI0IiB4Mj0iMCIgeTE9IjAiIHgxPSIyNCIvPgogIDwvc3ZnPjwvc3ZnPg==`;
img.style.width = "15px";
img.style.height = "15px";
newDiv.append(img);
} else {
newDiv.style.backgroundColor = rgb;
}
newDiv.dataset.rgb = rgb;
newDiv.addEventListener('click', (evt) => {
evt.preventDefault();
// shift key or right click for stroke
const picker = evt.shiftKey || evt.button === 2 ? 'stroke' : 'fill';
let color = newDiv.dataset.rgb;
// Webkit-based browsers returned 'initial' here for no stroke
if (color === 'none' || color === 'transparent' || color === 'initial') {
color = 'none';
}
const paletteEvent = new CustomEvent('change', { detail: { picker, color }, bubbles: false });
this.dispatchEvent(paletteEvent);
});
this.$strip.append(newDiv);
});
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
}
}
// Register
customElements.define('se-palette', SEPalette);

View File

@@ -0,0 +1,32 @@
import { template } from 'elix/src/base/internal.js';
import { fragmentFrom } from 'elix/src/core/htmlLiterals.js';
import PlainButton from 'elix/src/plain/PlainButton.js';
/**
* @class SePlainBorderButton
* Button with a border in the Plain reference design system
*
* @inherits PlainButton
*/
class SePlainBorderButton extends PlainButton {
/**
* @function get
* @returns {PlainObject}
*/
get [template] () {
const result = super[template];
result.content.append(
fragmentFrom.html`
<style>
[part~="button"] {
background: #72797A;
border: 1px solid #ccc;
}
</style>
`
);
return result;
}
}
export default SePlainBorderButton;

View File

@@ -0,0 +1,20 @@
import PlainMenuButton from 'elix/src/plain/PlainMenuButton.js';
import { defaultState } from 'elix/src/base/internal.js';
import sePlainBorderButton from './sePlainBorderButton.js';
/**
* @class ElixMenuButton
*/
export default class ElixMenuButton extends PlainMenuButton {
/**
* @function get
* @returns {PlainObject}
*/
get [defaultState] () {
return Object.assign(super[defaultState], {
sourcePartType: sePlainBorderButton
});
}
}
customElements.define('elix-menu-button', ElixMenuButton);

View File

@@ -0,0 +1,191 @@
import '../dialogs/se-elix/define/NumberSpinBox.js';
const template = document.createElement('template');
template.innerHTML = `
<style>
img {
position: relative;
right: -4px;
}
span {
bottom: -3px;
right: -4px;
position: relative;
margin-left: 4px;
margin-right: 4px;
color: #fff;
vertical-align: ;
}
elix-number-spin-box {
background-color: var(--input-color);
border-radius: 3px;
height: 20px;
margin-top: 1px;
vertical-align: top;
}
elix-number-spin-box::part(spin-button) {
padding: 0px;
}
elix-number-spin-box::part(input) {
width: 3em;
}
elix-number-spin-box{
width: 54px;
height: 24px;
}
</style>
<img src="./images/logo.svg" alt="icon" width="24" height="24" aria-labelledby="label" />
<span id="label">label</span>
<elix-number-spin-box min="1" step="1"></elix-number-spin-box>
`;
/**
* @class SESpinInput
*/
export class SESpinInput extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
// locate the component
this.$img = this._shadowRoot.querySelector('img');
this.$label = this.shadowRoot.getElementById('label');
this.$event = new CustomEvent('change');
this.$input = this._shadowRoot.querySelector('elix-number-spin-box');
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'value', 'label', 'src', 'size', 'min', 'max', 'step' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'src':
this.$img.setAttribute('src', newValue);
this.$label.remove();
break;
case 'size':
// access to the underlying input box
this.$input.shadowRoot.getElementById('input').size = newValue;
// below seems mandatory to override the default width style that takes precedence on size
this.$input.shadowRoot.getElementById('input').style.width = 'unset';
break;
case 'step':
this.$input.setAttribute('step', newValue);
break;
case 'min':
this.$input.setAttribute('min', newValue);
break;
case 'max':
this.$input.setAttribute('max', newValue);
break;
case 'label':
this.$label.textContent = newValue;
this.$img.remove();
break;
case 'value':
this.$input.value = newValue;
break;
default:
// eslint-disable-next-line no-console
console.error(`unknown attribute: ${name}`);
break;
}
}
/**
* @function get
* @returns {any}
*/
get label () {
return this.getAttribute('label');
}
/**
* @function set
* @returns {void}
*/
set label (value) {
this.setAttribute('label', value);
}
/**
* @function get
* @returns {any}
*/
get value () {
return this.$input.value;
}
/**
* @function set
* @returns {void}
*/
set value (value) {
this.$input.value = value;
}
/**
* @function get
* @returns {any}
*/
get src () {
return this.getAttribute('src');
}
/**
* @function set
* @returns {void}
*/
set src (value) {
this.setAttribute('src', value);
}
/**
* @function get
* @returns {any}
*/
get size () {
return this.getAttribute('size');
}
/**
* @function set
* @returns {void}
*/
set size (value) {
this.setAttribute('size', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
this.$input.addEventListener('change', (e) => {
e.preventDefault();
this.value = e.target.value;
this.dispatchEvent(this.$event);
});
// eslint-disable-next-line sonarjs/no-identical-functions
this.$input.addEventListener('click', (e) => {
e.preventDefault();
this.value = e.target.value;
this.dispatchEvent(this.$event);
});
}
}
// Register
customElements.define('se-spin-input', SESpinInput);

View File

@@ -0,0 +1,188 @@
import ListComboBox from 'elix/define/ListComboBox.js';
import * as internal from 'elix/src/base/internal.js';
import { templateFrom, fragmentFrom } from 'elix/src/core/htmlLiterals.js';
import NumberSpinBox from '../dialogs/se-elix/define/NumberSpinBox.js';
/**
* @class Dropdown
*/
class Zoom extends ListComboBox {
/**
* @function get
* @returns {PlainObject}
*/
get [internal.defaultState] () {
return Object.assign(super[internal.defaultState], {
inputPartType: NumberSpinBox,
src: './images/logo.svg',
inputsize: '100%'
});
}
/**
* @function get
* @returns {PlainObject}
*/
get [internal.template] () {
const result = super[internal.template];
const source = result.content.getElementById('source');
// add a icon before our dropdown
source.prepend(fragmentFrom.html`
<img src="./images/logo.svg" alt="icon" width="18" height="18"></img>
`.cloneNode(true));
// change the style so it fits in our toolbar
result.content.append(
templateFrom.html`
<style>
[part~="source"] {
grid-template-columns: 20px 1fr auto;
}
::slotted(*) {
padding: 4px;
width: 100%;
background-color: var(--icon-bg-color);
color: #fff;
}
}
[part~="popup"] {
width: 150%;
}
elix-number-spin-box {
background-color: var(--input-color);
border-radius: 3px;
height: 20px !important;
margin-top: 1px;
}
elix-number-spin-box::part(spin-button) {
padding: 0px;
}
</style>
`.content
);
return result;
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'title', 'src', 'inputsize', 'value' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'title':
// this.$span.setAttribute('title', `${newValue} ${shortcut ? `[${shortcut}]` : ''}`);
break;
case 'src':
this.src = newValue;
break;
case 'inputsize':
this.inputsize = newValue;
break;
default:
super.attributeChangedCallback(name, oldValue, newValue);
break;
}
}
/**
* @function [internal.render]
* @param {PlainObject} changed
* @returns {void}
*/
[internal.render] (changed) {
super[internal.render](changed);
if (this[internal.firstRender]) {
this.$img = this.shadowRoot.querySelector('img');
this.$input = this.shadowRoot.getElementById('input');
}
if (changed.src) {
this.$img.setAttribute('src', this[internal.state].src);
}
if (changed.inputsize) {
this.$input.shadowRoot.querySelector('[part~="input"]').style.width = this[internal.state].inputsize;
}
if (changed.inputPartType) {
// Wire up handler on new input.
this.addEventListener('close', (e) => {
e.preventDefault();
const value = e.detail?.closeResult?.getAttribute('value');
if (value) {
const closeEvent = new CustomEvent('change', { detail: { value } });
this.dispatchEvent(closeEvent);
}
});
}
}
/**
* @function src
* @returns {string} src
*/
get src () {
return this[internal.state].src;
}
/**
* @function src
* @returns {void}
*/
set src (src) {
this[internal.setState]({ src });
}
/**
* @function inputsize
* @returns {string} src
*/
get inputsize () {
return this[internal.state].inputsize;
}
/**
* @function src
* @returns {void}
*/
set inputsize (inputsize) {
this[internal.setState]({ inputsize });
}
/**
* @function value
* @returns {string} src
*/
get value () {
return this[internal.state].value;
}
/**
* @function value
* @returns {void}
*/
set value (value) {
this[internal.setState]({ value });
}
}
// Register
customElements.define('se-zoom', Zoom);
/*
{TODO
min: 0.001, max: 10000, step: 50, stepfunc: stepZoom,
function stepZoom (elem, step) {
const origVal = Number(elem.value);
if (origVal === 0) { return 100; }
const sugVal = origVal + step;
if (step === 0) { return origVal; }
if (origVal >= 100) {
return sugVal;
}
if (sugVal >= origVal) {
return origVal * 2;
}
return origVal / 2;
}
*/

View File

@@ -48,7 +48,6 @@ export const add = function (menuItem) {
throw new Error('Cannot add extension "' + menuItem.id + '", an extension by that name already exists"');
}
// Register menuItem action, see below for deferred menu dom injection
console.log('Registered contextmenu item: {id:' + menuItem.id + ', label:' + menuItem.label + '}'); // eslint-disable-line no-console
contextMenuExtensions[menuItem.id] = menuItem;
// TODO: Need to consider how to handle custom enable/disable behavior
};

View File

@@ -1,267 +0,0 @@
/**
* @file jQuery Context Menu Plugin
* Cory S.N. LaViska
* A Beautiful Site ({@link https://abeautifulsite.net/})
* Modified by Alexis Deveria
*
* More info: {@link https://abeautifulsite.net/2008/09/jquery-context-menu-plugin/}
*
* @module jQueryContextMenu
* @todo Update to latest version and adapt (and needs jQuery update as well): {@link https://github.com/swisnl/jQuery-contextMenu}
* @version 1.0.1
*
* @license (MIT OR GPL-2.0-or-later)
*
* This plugin is dual-licensed under the GNU General Public License
* and the MIT License and is copyright A Beautiful Site, LLC.
*
*/
import {isMac} from '../../common/browser.js';
/**
* @callback module:jQueryContextMenu.jQueryContextMenuListener
* @param {string} href The `href` value after the first character (for bypassing an initial `#`)
* @param {external:jQuery} srcElement The wrapped jQuery srcElement
* @param {{x: Float, y: Float, docX: Float, docY: Float}} coords
* @returns {void}
*/
/**
* @typedef {PlainObject} module:jQueryContextMenu.jQueryContextMenuConfig
* @property {string} menu
* @property {Float} inSpeed
* @property {Float} outSpeed
* @property {boolean} allowLeft
*/
/**
* Adds {@link external:jQuery.fn.contextMenu},
* {@link external:jQuery.fn.disableContextMenuItems},
* {@link external:jQuery.fn.enableContextMenuItems},
* {@link external:jQuery.fn.disableContextMenu},
* {@link external:jQuery.fn.enableContextMenu},
* {@link external:jQuery.fn.destroyContextMenu}.
* @function module:jQueryContextMenu.jQueryContextMenu
* @param {external:jQuery} $ The jQuery object to wrap (with `contextMenu`, `disableContextMenuItems`, `enableContextMenuItems`, `disableContextMenu`, `enableContextMenu`, `destroyContextMenu`)
* @returns {external:jQuery}
*/
function jQueryContextMenu ($) {
const win = $(window);
const doc = $(document);
$.extend($.fn, {
/**
* @memberof external:jQuery.fn
* @param {module:jQueryContextMenu.jQueryContextMenuConfig} o
* @param {module:jQueryContextMenu.jQueryContextMenuListener} listener
* @returns {external:jQuery}
*/
contextMenu (o, listener) {
// Defaults
if (o.menu === undefined) return false;
if (o.inSpeed === undefined) o.inSpeed = 150;
if (o.outSpeed === undefined) o.outSpeed = 75;
// 0 needs to be -1 for expected results (no fade)
if (o.inSpeed === 0) o.inSpeed = -1;
if (o.outSpeed === 0) o.outSpeed = -1;
// Loop each context menu
$(this).each(function () {
const el = $(this);
const offset = $(el).offset();
const menu = $('#' + o.menu);
// Add contextMenu class
menu.addClass('contextMenu');
// Simulate a true right click
$(this).bind('mousedown', function (evt) {
$(this).mouseup(function (e) {
const srcElement = $(this);
srcElement.unbind('mouseup');
if (!(evt.button === 2 || o.allowLeft ||
(evt.ctrlKey && isMac()))) {
return undefined;
}
e.stopPropagation();
// Hide context menus that may be showing
$('.contextMenu').hide();
// Get this context menu
if (el.hasClass('disabled')) return false;
// Detect mouse position
let x = e.pageX, y = e.pageY;
const xOff = win.width() - menu.width(),
yOff = win.height() - menu.height();
if (x > xOff - 15) x = xOff - 15;
if (y > yOff - 30) y = yOff - 30; // 30 is needed to prevent scrollbars in FF
// Show the menu
doc.unbind('click');
menu.css({top: y, left: x}).fadeIn(o.inSpeed);
// Hover events
menu.find('A').mouseover(function () {
menu.find('LI.hover').removeClass('hover');
$(this).parent().addClass('hover');
}).mouseout(function () {
menu.find('LI.hover').removeClass('hover');
});
// Keyboard
doc.keypress(function (ev) {
switch (ev.keyCode) {
case 38: // up
if (!menu.find('LI.hover').length) {
menu.find('LI:last').addClass('hover');
} else {
menu.find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover');
if (!menu.find('LI.hover').length) menu.find('LI:last').addClass('hover');
}
break;
case 40: // down
if (!menu.find('LI.hover').length) {
menu.find('LI:first').addClass('hover');
} else {
menu.find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover');
if (!menu.find('LI.hover').length) menu.find('LI:first').addClass('hover');
}
break;
case 13: // enter
menu.find('LI.hover A').trigger('click');
break;
case 27: // esc
doc.trigger('click');
break;
}
});
// When items are selected
menu.find('A').unbind('mouseup');
menu.find('LI:not(.disabled) A').mouseup(function () {
doc.unbind('click').unbind('keypress');
$('.contextMenu').hide();
if (listener) {
listener($(this).attr('href').substr(1), $(srcElement), {
x: x - offset.left, y: y - offset.top, docX: x, docY: y
});
}
return false;
});
// Hide bindings
setTimeout(function () { // Delay for Mozilla
doc.click(function () {
doc.unbind('click').unbind('keypress');
menu.fadeOut(o.outSpeed);
return false;
});
}, 0);
return undefined;
});
});
// Disable text selection
if ($.browser.mozilla) {
$('#' + o.menu).each(function () { $(this).css({MozUserSelect: 'none'}); });
} else if ($.browser.msie) {
$('#' + o.menu).each(function () { $(this).bind('selectstart.disableTextSelect', function () { return false; }); });
} else {
$('#' + o.menu).each(function () { $(this).bind('mousedown.disableTextSelect', function () { return false; }); });
}
// Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome)
$(el).add($('UL.contextMenu')).bind('contextmenu', function () { return false; });
});
return $(this);
},
/**
* Disable context menu items on the fly.
* @memberof external:jQuery.fn
* @param {void|string} o Comma-separated
* @returns {external:jQuery}
*/
disableContextMenuItems (o) {
if (o === undefined) {
// Disable all
$(this).find('LI').addClass('disabled');
return $(this);
}
$(this).each(function () {
if (o !== undefined) {
const d = o.split(',');
for (const href of d) {
$(this).find('A[href="' + href + '"]').parent().addClass('disabled');
}
}
});
return $(this);
},
/**
* Enable context menu items on the fly.
* @memberof external:jQuery.fn
* @param {void|string} o Comma-separated
* @returns {external:jQuery}
*/
enableContextMenuItems (o) {
if (o === undefined) {
// Enable all
$(this).find('LI.disabled').removeClass('disabled');
return $(this);
}
$(this).each(function () {
if (o !== undefined) {
const d = o.split(',');
for (const href of d) {
$(this).find('A[href="' + href + '"]').parent().removeClass('disabled');
}
}
});
return $(this);
},
/**
* Disable context menu(s).
* @memberof external:jQuery.fn
* @returns {external:jQuery}
*/
disableContextMenu () {
$(this).each(function () {
$(this).addClass('disabled');
});
return $(this);
},
/**
* Enable context menu(s).
* @memberof external:jQuery.fn
* @returns {external:jQuery}
*/
enableContextMenu () {
$(this).each(function () {
$(this).removeClass('disabled');
});
return $(this);
},
/**
* Destroy context menu(s).
* @memberof external:jQuery.fn
* @returns {external:jQuery}
*/
destroyContextMenu () {
// Destroy specified context menus
$(this).each(function () {
// Disable action
$(this).unbind('mousedown').unbind('mouseup');
});
return $(this);
}
});
return $;
}
export default jQueryContextMenu;

View File

@@ -0,0 +1,68 @@
import PlainAlertDialog from 'elix/src/plain/PlainAlertDialog.js';
import { template } from 'elix/src/base/internal.js';
import { fragmentFrom } from 'elix/src/core/htmlLiterals.js';
/**
* @class SePlainAlertDialog
*/
export default class SePlainAlertDialog extends PlainAlertDialog {
/**
* @function get
* @returns {PlainObject}
*/
get [template] () {
const result = super[template];
// Replace the default slot with a new default slot and a button container.
const defaultSlot = result.content.querySelector('#frameContent');
if (defaultSlot) {
defaultSlot.replaceWith(fragmentFrom.html`
<div id="alertDialogContent">
<div id="se-content-alert">
<slot></slot>
</div>
<div id="choiceButtonContainer" part="choice-button-container"></div>
</div>
`);
}
result.content.append(
fragmentFrom.html`
<style>
[part~="frame"] {
padding: 1em;
background: #CCC;
width: 300px;
border: 1px outset #777;
font-size: 0.8em;
font-family: Verdana,Helvetica,sans-serif;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
[part~="choice-button-container"] {
margin-top: 1em;
text-align: center;
}
[part~="choice-button"]:not(:first-child) {
margin-left: 0.5em;
}
#se-content-alert{
height: 95px;
background: #DDD;
overflow: auto;
text-align: left;
border: 1px solid #5a6162;
padding: 1em;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
</style>
`
);
return result;
}
}
customElements.define('se-elix-alert-dialog', SePlainAlertDialog);

View File

@@ -0,0 +1,341 @@
const template = document.createElement('template');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<style>
.contextMenu {
position: absolute;
z-index: 99999;
border: solid 1px rgba(0,0,0,.33);
background: rgba(255,255,255,.95);
padding: 5px 0;
margin: 0px;
display: none;
font: 12px/15px Lucida Sans, Helvetica, Verdana, sans-serif;
border-radius: 5px;
-moz-border-radius: 5px;
-moz-box-shadow: 2px 5px 10px rgba(0,0,0,.3);
-webkit-box-shadow: 2px 5px 10px rgba(0,0,0,.3);
box-shadow: 2px 5px 10px rgba(0,0,0,.3);
}
.contextMenu li {
list-style: none;
padding: 0px;
margin: 0px;
}
.contextMenu .shortcut {
width: 115px;
text-align:right;
float:right;
}
.contextMenu a {
-moz-user-select: none;
-webkit-user-select: none;
color: #222;
text-decoration: none;
display: block;
line-height: 20px;
height: 20px;
background-position: 6px center;
background-repeat: no-repeat;
outline: none;
padding: 0px 15px 1px 20px;
}
.contextMenu li.hover a {
background-color: #2e5dea;
color: white;
cursor: default;
}
.contextMenu li.disabled a {
color: #999;
}
.contextMenu li.hover.disabled a {
background-color: transparent;
}
.contextMenu li.separator {
border-top: solid 1px #E3E3E3;
padding-top: 5px;
margin-top: 5px;
}
</style>
<ul id="cmenu_canvas" class="contextMenu">
<li>
<a href="#cut" id="se-cut">
<span class="shortcut">META+X</span>
</a>
</li>
<li>
<a href="#copy" id="se-copy">
<span class="shortcut">META+C</span>
</a>
</li>
<li>
<a href="#paste" id="se-paste"></a>
</li>
<li>
<a href="#paste_in_place" id="se-paste-in-place"></a>
</li>
<li class="separator">
<a href="#delete" id="se-delete">
<span class="shortcut">BACKSPACE</span>
</a>
</li>
<li class="separator">
<a href="#group" id="se-group">
<span class="shortcut">G</span>
</a>
</li>
<li>
<a href="#ungroup" id="se-ungroup">
<span class="shortcut">G</span>
</a>
</li>
<li class="separator">
<a href="#move_front" id="se-move-front">
<span class="shortcut">CTRL+SHFT+]</span>
</a>
</li>
<li>
<a href="#move_up" id="se-move-up">
<span class="shortcut">CTRL+]</span>
</a>
</li>
<li>
<a href="#move_down" id="se-move-down">
<span class="shortcut">CTRL+[</span>
</a>
</li>
<li>
<a href="#move_back" id="se-move-back">
<span class="shortcut">CTRL+SHFT+[</span>
</a>
</li>
</ul>
`;
/**
* @class SeCMenuDialog
*/
export class SeCMenuDialog extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this._workarea = document.getElementById('workarea');
this.$dialog = this._shadowRoot.querySelector('#cmenu_canvas');
this.$copyLink = this._shadowRoot.querySelector('#se-copy');
this.$cutLink = this._shadowRoot.querySelector('#se-cut');
this.$pasteLink = this._shadowRoot.querySelector('#se-paste');
this.$pasteInPlaceLink = this._shadowRoot.querySelector('#se-paste-in-place');
this.$deleteLink = this._shadowRoot.querySelector('#se-delete');
this.$groupLink = this._shadowRoot.querySelector('#se-group');
this.$ungroupLink = this._shadowRoot.querySelector('#se-ungroup');
this.$moveFrontLink = this._shadowRoot.querySelector('#se-move-front');
this.$moveUpLink = this._shadowRoot.querySelector('#se-move-up');
this.$moveDownLink = this._shadowRoot.querySelector('#se-move-down');
this.$moveBackLink = this._shadowRoot.querySelector('#se-move-back');
}
/**
* @function init
* @param {any} name
* @returns {void}
*/
init (i18next) {
this.setAttribute('tools-cut', i18next.t('tools.cut'));
this.setAttribute('tools-copy', i18next.t('tools.copy'));
this.setAttribute('tools-paste', i18next.t('tools.paste'));
this.setAttribute('tools-paste_in_place', i18next.t('tools.paste_in_place'));
this.setAttribute('tools-delete', i18next.t('tools.delete'));
this.setAttribute('tools-group', i18next.t('tools.group'));
this.setAttribute('tools-ungroup', i18next.t('tools.ungroup'));
this.setAttribute('tools-move_front', i18next.t('tools.move_front'));
this.setAttribute('tools-move_up', i18next.t('tools.move_up'));
this.setAttribute('tools-move_down', i18next.t('tools.move_down'));
this.setAttribute('tools-move_back', i18next.t('tools.move_back'));
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'disableallmenu', 'enablemenuitems', 'disablemenuitems', 'tools-cut',
'tools-copy', 'tools-paste', 'tools-paste_in_place', 'tools-delete', 'tools-group',
'tools-ungroup', 'tools-move_front', 'tools-move_up', 'tools-move_down',
'tools-move_back' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
let eles = [];
let textnode;
const sdowRoot = this._shadowRoot;
switch (name) {
case 'disableallmenu':
if (newValue === 'true') {
const elesli = sdowRoot.querySelectorAll('li');
elesli.forEach(function (eleli) {
eleli.classList.add('disabled');
});
}
break;
case 'enablemenuitems':
eles = newValue.split(',');
eles.forEach(function (ele) {
const selEle = sdowRoot.querySelector('a[href*="' + ele + '"]');
selEle.parentElement.classList.remove('disabled');
});
break;
case 'disablemenuitems':
eles = newValue.split(',');
eles.forEach(function (ele) {
const selEle = sdowRoot.querySelector('a[href*="' + ele + '"]');
selEle.parentElement.classList.add('disabled');
});
break;
case 'tools-cut':
textnode = document.createTextNode(newValue);
this.$cutLink.prepend(textnode);
break;
case 'tools-copy':
textnode = document.createTextNode(newValue);
this.$copyLink.prepend(textnode);
break;
case 'tools-paste':
this.$pasteLink.textContent = newValue;
break;
case 'tools-paste_in_place':
this.$pasteInPlaceLink.textContent = newValue;
break;
case 'tools-delete':
textnode = document.createTextNode(newValue);
this.$deleteLink.prepend(textnode);
break;
case 'tools-group':
textnode = document.createTextNode(newValue);
this.$groupLink.prepend(textnode);
break;
case 'tools-ungroup':
textnode = document.createTextNode(newValue);
this.$ungroupLink.prepend(textnode);
break;
case 'tools-move_front':
textnode = document.createTextNode(newValue);
this.$moveFrontLink.prepend(textnode);
break;
case 'tools-move_up':
textnode = document.createTextNode(newValue);
this.$moveUpLink.prepend(textnode);
break;
case 'tools-move_down':
textnode = document.createTextNode(newValue);
this.$moveDownLink.prepend(textnode);
break;
case 'tools-move_back':
textnode = document.createTextNode(newValue);
this.$moveBackLink.prepend(textnode);
break;
default:
// super.attributeChangedCallback(name, oldValue, newValue);
break;
}
}
/**
* @function get
* @returns {any}
*/
get disableallmenu () {
return this.getAttribute('disableallmenu');
}
/**
* @function set
* @returns {void}
*/
set disableallmenu (value) {
this.setAttribute('disableallmenu', value);
}
/**
* @function get
* @returns {any}
*/
get enablemenuitems () {
return this.getAttribute('enablemenuitems');
}
/**
* @function set
* @returns {void}
*/
set enablemenuitems (value) {
this.setAttribute('enablemenuitems', value);
}
/**
* @function get
* @returns {any}
*/
get disablemenuitems () {
return this.getAttribute('disablemenuitems');
}
/**
* @function set
* @returns {void}
*/
set disablemenuitems (value) {
this.setAttribute('disablemenuitems', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
const current = this;
const onMenuOpenHandler = (e) => {
e.preventDefault();
current.$dialog.style.top = e.pageY + 'px';
current.$dialog.style.left = e.pageX + 'px';
current.$dialog.style.display = 'block';
};
const onMenuCloseHandler = (e) => {
if (e.button !== 2) {
current.$dialog.style.display = 'none';
}
};
const onMenuClickHandler = (e, action) => {
const triggerEvent = new CustomEvent('change', { detail: {
trigger: action
} });
this.dispatchEvent(triggerEvent);
};
this._workarea.addEventListener('contextmenu', onMenuOpenHandler);
this._workarea.addEventListener('mousedown', onMenuCloseHandler);
this.$cutLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'cut'));
this.$copyLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'copy'));
this.$pasteLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'paste'));
this.$pasteInPlaceLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'paste_in_place'));
this.$deleteLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'delete'));
this.$groupLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'group'));
this.$ungroupLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'ungroup'));
this.$moveFrontLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'move_front'));
this.$moveUpLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'move_up'));
this.$moveDownLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'move_down'));
this.$moveBackLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'move_back'));
}
}
// Register
customElements.define('se-cmenu_canvas-dialog', SeCMenuDialog);

View File

@@ -0,0 +1,218 @@
const template = document.createElement('template');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<style>
.contextMenu {
position: absolute;
z-index: 99999;
border: solid 1px rgba(0,0,0,.33);
background: rgba(255,255,255,.95);
padding: 5px 0;
margin: 0px;
display: none;
font: 12px/15px Lucida Sans, Helvetica, Verdana, sans-serif;
border-radius: 5px;
-moz-border-radius: 5px;
-moz-box-shadow: 2px 5px 10px rgba(0,0,0,.3);
-webkit-box-shadow: 2px 5px 10px rgba(0,0,0,.3);
box-shadow: 2px 5px 10px rgba(0,0,0,.3);
}
.contextMenu li {
list-style: none;
padding: 0px;
margin: 0px;
}
.contextMenu .shortcut {
width: 115px;
text-align:right;
float:right;
}
.contextMenu a {
-moz-user-select: none;
-webkit-user-select: none;
color: #222;
text-decoration: none;
display: block;
line-height: 20px;
height: 20px;
background-position: 6px center;
background-repeat: no-repeat;
outline: none;
padding: 0px 15px 1px 20px;
}
.contextMenu li.hover a {
background-color: #2e5dea;
color: white;
cursor: default;
}
.contextMenu li.disabled a {
color: #999;
}
.contextMenu li.hover.disabled a {
background-color: transparent;
}
.contextMenu li.separator {
border-top: solid 1px #E3E3E3;
padding-top: 5px;
margin-top: 5px;
}
</style>
<ul id="cmenu_layers" class="contextMenu">
<li><a href="#dupe" id="se-dupe">#{svgEditor.i18next.t('layers.dupe')}</a></li>
<li><a href="#delete" id="se-layer-delete">#{svgEditor.i18next.t('layers.del')}</a></li>
<li><a href="#merge_down" id="se-merge-down">#{svgEditor.i18next.t('layers.merge_down')}</a></li>
<li><a href="#merge_all" id="se-merge-all">#{svgEditor.i18next.t('layers.merge_all')}</a></li>
</ul>
`;
/**
* @class SeCMenuLayerDialog
*/
export class SeCMenuLayerDialog extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.source = '';
this._workarea = undefined;
this.$sidePanels = document.getElementById('sidepanels');
this.$dialog = this._shadowRoot.querySelector('#cmenu_layers');
this.$duplicateLink = this._shadowRoot.querySelector('#se-dupe');
this.$deleteLink = this._shadowRoot.querySelector('#se-layer-delete');
this.$mergeDownLink = this._shadowRoot.querySelector('#se-merge-down');
this.$mergeAllLink = this._shadowRoot.querySelector('#se-merge-all');
}
/**
* @function init
* @param {any} name
* @returns {void}
*/
init (i18next) {
this.setAttribute('layers-dupe', i18next.t('layers.dupe'));
this.setAttribute('layers-del', i18next.t('layers.del'));
this.setAttribute('layers-merge_down', i18next.t('layers.merge_down'));
this.setAttribute('layers-merge_all', i18next.t('layers.merge_all'));
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'value', 'leftclick', 'layers-dupe', 'layers-del', 'layers-merge_down', 'layers-merge_all' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
// eslint-disable-next-line sonarjs/no-small-switch
switch (name) {
case 'value':
this.source = newValue;
if (newValue !== '' && newValue !== undefined) {
this._workarea = document.getElementById(this.source);
}
break;
case 'layers-dupe':
this.$duplicateLink.textContent = newValue;
break;
case 'layers-del':
this.$deleteLink.textContent = newValue;
break;
case 'layers-merge_down':
this.$mergeDownLink.textContent = newValue;
break;
case 'layers-merge_all':
this.$mergeAllLink.textContent = newValue;
break;
default:
// super.attributeChangedCallback(name, oldValue, newValue);
break;
}
}
/**
* @function get
* @returns {any}
*/
get value () {
return this.getAttribute('value');
}
/**
* @function set
* @returns {void}
*/
set value (value) {
this.setAttribute('value', value);
}
/**
* @function get
* @returns {any}
*/
get leftclick () {
return this.getAttribute('leftclick');
}
/**
* @function set
* @returns {void}
*/
set leftclick (value) {
this.setAttribute('leftclick', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
const current = this;
const onMenuOpenHandler = (e) => {
e.preventDefault();
current.$dialog.style.top = e.pageY + 'px';
current.$dialog.style.left = e.pageX + 'px';
current.$dialog.style.display = 'block';
};
const onMenuCloseHandler = (e) => {
if (e.button !== 2) {
current.$dialog.style.display = 'none';
}
};
const onMenuClickHandler = (e, action, id) => {
const triggerEvent = new CustomEvent('change', { detail: {
trigger: action,
source: id
} });
this.dispatchEvent(triggerEvent);
current.$dialog.style.display = 'none';
};
if (this._workarea !== undefined) {
this._workarea.addEventListener('contextmenu', onMenuOpenHandler);
if (this.getAttribute('leftclick') === 'true') {
this._workarea.addEventListener('click', onMenuOpenHandler);
}
this._workarea.addEventListener('mousedown', onMenuCloseHandler);
this.$sidePanels.addEventListener('mousedown', onMenuCloseHandler);
}
this.$duplicateLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'dupe', this.source));
this.$deleteLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'delete', this.source));
this.$mergeDownLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'merge_down', this.source));
this.$mergeAllLink.addEventListener('click', (evt) => onMenuClickHandler(evt, 'merge_all', this.source));
}
}
// Register
customElements.define('se-cmenu-layers', SeCMenuLayerDialog);

View File

@@ -0,0 +1,644 @@
const template = document.createElement('template');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<style>
:not(:defined) {
display: none;
}
/* Force the scroll bar to appear so we see it hide when overlay opens. */
body::-webkit-scrollbar {
background: lightgray;
}
body::-webkit-scrollbar-thumb {
background: darkgray;
}
.toolbar_button button {
border:1px solid #dedede;
line-height:130%;
float: left;
background: #E8E8E8 none;
padding:5px 10px 5px 7px; /* Firefox */
line-height:17px; /* Safari */
margin: 5px 20px 0 0;
border: 1px #808080 solid;
border-top-color: #FFF;
border-left-color: #FFF;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
cursor: pointer;
}
.toolbar_button button:hover {
border: 1px #e0a874 solid;
border-top-color: #fcd9ba;
border-left-color: #fcd9ba;
background-color: #FFC;
}
.toolbar_button button:active {
background-color: #F4E284;
border-left: 1px solid #663300;
border-top: 1px solid #663300;
}
.toolbar_button button .svg_icon {
margin: 0 3px -3px 0 !important;
padding: 0;
border: none;
width: 16px;
height: 16px;
}
.color_block {
top: 0;
left: 0;
}
.color_block svg {
display: block;
}
#bg_blocks {
overflow: auto;
margin-left: 30px;
}
#bg_blocks .color_block {
position: static;
}
#svginfo_bg_note {
font-size: .9em;
font-style: italic;
color: #444;
}
#svg_prefs #svg_prefs_container {
padding: 10px;
background-color: #5a6162;
color: #c5c5c5;
border: 1px outset #777;
opacity: 1.0;
font-family: Verdana, Helvetica, sans-serif;
font-size: .8em;
z-index: 20001;
}
#tool_prefs_back {
margin-left: 1em;
overflow: auto;
}
#tool_prefs_save {
width: 30%;
background-color: #c79605;
margin-left: 20%;
}
#tool_prefs_cancel {
width: 30%;
background-color: #c8c8c8;
}
#svg_prefs #svg_docprops_prefs {
float: left;
width: 221px;
margin: 5px .7em;
overflow: hidden;
}
#svg_prefs_container fieldset + fieldset {
float: right;
}
#svg_prefs legend {
max-width: 195px;
}
#svg_prefs_container > fieldset > legend {
font-weight: bold;
font-size: 1.1em;
}
#svg_prefs fieldset {
padding: 5px;
margin: 5px;
border: 1px solid #DDD;
}
#svg_prefs_container label {
display: block;
margin: .5em;
}
#svg_prefs_container div.color_block {
float: left;
margin: 2px;
padding: 20px;
border: 1px solid #6f6f6f;
}
#change_background div.cur_background {
border: 2px solid blue;
padding: 19px;
}
#canvas_bg_url {
display: block;
width: 96%;
}
#svg_prefs button {
margin-top: 0;
margin-bottom: 5px;
}
</style>
<elix-dialog id="svg_prefs" aria-label="Editor Preferences" closed>
<div id="svg_prefs_container">
<div id="tool_prefs_back" class="toolbar_button">
<button id="tool_prefs_save"></button>
<button id="tool_prefs_cancel"></button>
</div>
<fieldset>
<legend id="svginfo_editor_prefs"></legend>
<label>
<span id="svginfo_lang"></span>
<!-- Source: https://en.wikipedia.org/wiki/Language_names -->
<select id="lang_select">
<option id="lang_ar" value="ar">العربية</option>
<option id="lang_cs" value="cs">Čeština</option>
<option id="lang_de" value="de">Deutsch</option>
<option id="lang_en" value="en" selected="selected">English</option>
<option id="lang_es" value="es">Español</option>
<option id="lang_fa" value="fa">فارسی</option>
<option id="lang_fr" value="fr">Français</option>
<option id="lang_fy" value="fy">Frysk</option>
<option id="lang_hi" value="hi">हिन्दी, हिंदी</option>
<option id="lang_it" value="it">Italiano</option>
<option id="lang_ja" value="ja">日本語</option>
<option id="lang_nl" value="nl">Nederlands</option>
<option id="lang_pl" value="pl">Polski</option>
<option id="lang_pt-BR" value="pt-BR">Português (BR)</option>
<option id="lang_ro" value="ro">Română</option>
<option id="lang_ru" value="ru">Русский</option>
<option id="lang_sk" value="sk">Slovenčina</option>
<option id="lang_sl" value="sl">Slovenščina</option>
<option id="lang_zh-CN" value="zh-CN">简体中文</option>
<option id="lang_zh-TW" value="zh-TW">繁體中文</option>
</select>
</label>
<label>
<span id="svginfo_icons"></span>
<select id="iconsize">
<option id="icon_small" value="s"></option>
<option id="icon_medium" value="m" selected="selected"></option>
<option id="icon_large" value="l"></option>
<option id="icon_xlarge" value="xl"></option>
</select>
</label>
<fieldset id="change_background">
<legend id="svginfo_change_background"></legend>
<div id="bg_blocks"></div>
<label>
<span id="svginfo_bg_url"></span>
<input type="text" id="canvas_bg_url" />
</label>
<p id="svginfo_bg_note"></p>
</fieldset>
<fieldset id="change_grid">
<legend id="svginfo_grid_settings"></legend>
<label for="svginfo_snap_onoff">
<span id="svginfo_snap_onoff"></span>
<input type="checkbox" value="snapping_on" id="grid_snapping_on" />
</label>
<label for="grid_snapping_step">
<span id="svginfo_snap_step"></span>
<input type="text" id="grid_snapping_step" size="3" value="10" />
</label>
<label>
<span id="svginfo_grid_color"></span>
<input type="text" id="grid_color" size="3" value="#000" />
</label>
</fieldset>
<fieldset id="units_rulers">
<legend id="svginfo_units_rulers"></legend>
<label>
<span id="svginfo_rulers_onoff"></span>
<input id="show_rulers" type="checkbox" value="show_rulers" checked="checked" />
</label>
<label>
<span id="svginfo_unit"></span>
<select id="base_unit">
<option value="px">Pixels</option>
<option value="cm">Centimeters</option>
<option value="mm">Millimeters</option>
<option value="in">Inches</option>
<option value="pt">Points</option>
<option value="pc">Picas</option>
<option value="em">Ems</option>
<option value="ex">Exs</option>
</select>
</label>
</fieldset>
</fieldset>
</div>
</elix-dialog>
`;
/**
* @class SeEditPrefsDialog
*/
export class SeEditPrefsDialog extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this.colorBlocks = [ '#FFF', '#888', '#000', 'chessboard' ];
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$dialog = this._shadowRoot.querySelector('#svg_prefs');
this.$saveBtn = this._shadowRoot.querySelector('#tool_prefs_save');
this.$cancelBtn = this._shadowRoot.querySelector('#tool_prefs_cancel');
this.$langSelect = this._shadowRoot.querySelector('#lang_select');
this.$iconSize = this._shadowRoot.querySelector('#iconsize');
this.$bgBlocks = this._shadowRoot.querySelector('#bg_blocks');
this.$bgURL = this._shadowRoot.querySelector('#canvas_bg_url');
this.$gridSnappingOn = this._shadowRoot.querySelector('#grid_snapping_on');
this.$gridSnappingStep = this._shadowRoot.querySelector('#grid_snapping_step');
this.$gridColor = this._shadowRoot.querySelector('#grid_color');
this.$showRulers = this._shadowRoot.querySelector('#show_rulers');
this.$baseUnit = this._shadowRoot.querySelector('#base_unit');
}
/**
* @function init
* @param {any} name
* @returns {void}
*/
init (i18next) {
this.setAttribute('common-ok', i18next.t('common.ok'));
this.setAttribute('common-cancel', i18next.t('common.cancel'));
this.setAttribute('config-editor_prefs', i18next.t('config.editor_prefs'));
this.setAttribute('config-language', i18next.t('config.language'));
this.setAttribute('config-icon_size', i18next.t('config.icon_size'));
this.setAttribute('config-icon_small', i18next.t('config.icon_small'));
this.setAttribute('config-icon_medium', i18next.t('config.icon_medium'));
this.setAttribute('config-icon_large', i18next.t('config.icon_large'));
this.setAttribute('config-icon_xlarge', i18next.t('config.icon_xlarge'));
this.setAttribute('config-background', i18next.t('config.background'));
this.setAttribute('common-url', i18next.t('common.url'));
this.setAttribute('config-editor_bg_note', i18next.t('config.editor_bg_note'));
this.setAttribute('config-grid', i18next.t('config.grid'));
this.setAttribute('config-snapping_onoff', i18next.t('config.snapping_onoff'));
this.setAttribute('config-snapping_stepsize', i18next.t('config.snapping_stepsize'));
this.setAttribute('config-grid_color', i18next.t('config.grid_color'));
this.setAttribute('config-units_and_rulers', i18next.t('config.units_and_rulers'));
this.setAttribute('config-show_rulers', i18next.t('config.show_rulers'));
this.setAttribute('config-base_unit', i18next.t('config.base_unit'));
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
// eslint-disable-next-line max-len
return [ 'dialog', 'lang', 'iconsize', 'canvasbg', 'bgurl', 'gridsnappingon', 'gridsnappingstep', 'gridcolor', 'showrulers', 'baseunit', 'common-ok', 'common-cancel', 'config-editor_prefs', 'config-language', 'config-icon_size', 'config-icon_small', 'config-icon_medium', 'config-icon_large', 'config-icon_xlarge', 'config-background', 'common-url', 'config-editor_bg_note', 'config-grid', 'config-snapping_onoff', 'config-snapping_stepsize', 'config-grid_color', 'config-units_and_rulers', 'config-show_rulers', 'config-base_unit' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
const blocks = this.$bgBlocks.querySelectorAll('div');
const curBg = 'cur_background';
let node;
switch (name) {
case 'dialog':
if (newValue === 'open') {
this.$dialog.open();
} else {
this.$dialog.close();
}
break;
case 'lang':
this.$langSelect.value = newValue;
break;
case 'iconsize':
this.$iconSize.value = newValue;
break;
case 'canvasbg':
if (!newValue) {
if (blocks.length > 0) {
blocks[0].classList.add(curBg);
}
} else {
blocks.forEach(function (blk) {
const isBg = blk.dataset.bgColor === newValue;
if (isBg) {
blk.classList.add(curBg);
} else {
blk.classList.remove(curBg);
}
});
}
break;
case 'bgurl':
this.$bgURL.value = newValue;
break;
case 'gridsnappingon':
if (newValue === 'true') {
this.$gridSnappingOn.checked = true;
} else if (newValue === 'false') {
this.$gridSnappingOn.checked = false;
}
break;
case 'gridsnappingstep':
this.$gridSnappingStep.value = newValue;
break;
case 'gridcolor':
this.$gridColor.value = newValue;
break;
case 'showrulers':
if (newValue === 'true') {
this.$showRulers.checked = true;
} else if (newValue === 'false') {
this.$showRulers.checked = false;
}
break;
case 'baseunit':
this.$baseUnit.value = newValue;
break;
case 'common-ok':
this.$saveBtn.textContent = newValue;
break;
case 'common-cancel':
this.$cancelBtn.textContent = newValue;
break;
case 'config-editor_prefs':
node = this._shadowRoot.querySelector('#svginfo_editor_prefs');
node.textContent = newValue;
break;
case 'config-language':
node = this._shadowRoot.querySelector('#svginfo_lang');
node.textContent = newValue;
break;
case 'config-icon_size':
node = this._shadowRoot.querySelector('#svginfo_icons');
node.textContent = newValue;
break;
case 'config-icon_small':
node = this._shadowRoot.querySelector('#icon_small');
node.textContent = newValue;
break;
case 'config-icon_medium':
node = this._shadowRoot.querySelector('#icon_medium');
node.textContent = newValue;
break;
case 'config-icon_large':
node = this._shadowRoot.querySelector('#icon_large');
node.textContent = newValue;
break;
case 'config-icon_xlarge':
node = this._shadowRoot.querySelector('#icon_xlarge');
node.textContent = newValue;
break;
case 'config-background':
node = this._shadowRoot.querySelector('#svginfo_change_background');
node.textContent = newValue;
break;
case 'common-url':
node = this._shadowRoot.querySelector('#svginfo_bg_url');
node.textContent = newValue;
break;
case 'config-editor_bg_note':
node = this._shadowRoot.querySelector('#svginfo_bg_note');
node.textContent = newValue;
break;
case 'config-grid':
node = this._shadowRoot.querySelector('#svginfo_grid_settings');
node.textContent = newValue;
break;
case 'config-snapping_onoff':
node = this._shadowRoot.querySelector('#svginfo_snap_onoff');
node.textContent = newValue;
break;
case 'config-snapping_stepsize':
node = this._shadowRoot.querySelector('#svginfo_snap_step');
node.textContent = newValue;
break;
case 'config-grid_color':
node = this._shadowRoot.querySelector('#svginfo_grid_color');
node.textContent = newValue;
break;
case 'config-units_and_rulers':
node = this._shadowRoot.querySelector('#svginfo_units_rulers');
node.textContent = newValue;
break;
case 'config-show_rulers':
node = this._shadowRoot.querySelector('#svginfo_rulers_onoff');
node.textContent = newValue;
break;
case 'config-base_unit':
node = this._shadowRoot.querySelector('#svginfo_unit');
node.textContent = newValue;
break;
default:
super.attributeChangedCallback(name, oldValue, newValue);
break;
}
}
/**
* @function get
* @returns {any}
*/
get lang () {
return this.getAttribute('lang');
}
/**
* @function set
* @returns {void}
*/
set lang (value) {
this.setAttribute('lang', value);
}
/**
* @function get
* @returns {any}
*/
get iconsize () {
return this.getAttribute('iconsize');
}
/**
* @function set
* @returns {void}
*/
set iconsize (value) {
this.setAttribute('iconsize', value);
}
/**
* @function get
* @returns {any}
*/
get canvasbg () {
return this.getAttribute('canvasbg');
}
/**
* @function set
* @returns {void}
*/
set canvasbg (value) {
this.setAttribute('canvasbg', value);
}
/**
* @function get
* @returns {any}
*/
get bgurl () {
return this.getAttribute('bgurl');
}
/**
* @function set
* @returns {void}
*/
set bgurl (value) {
this.setAttribute('bgurl', value);
}
/**
* @function get
* @returns {any}
*/
get dialog () {
return this.getAttribute('dialog');
}
/**
* @function set
* @returns {void}
*/
set dialog (value) {
this.setAttribute('dialog', value);
}
/**
* @function get
* @returns {any}
*/
get gridsnappingon () {
return this.getAttribute('gridsnappingon');
}
/**
* @function set
* @returns {void}
*/
set gridsnappingon (value) {
this.setAttribute('gridsnappingon', value);
}
/**
* @function get
* @returns {any}
*/
get gridsnappingstep () {
return this.getAttribute('gridsnappingstep');
}
/**
* @function set
* @returns {void}
*/
set gridsnappingstep (value) {
this.setAttribute('gridsnappingstep', value);
}
/**
* @function get
* @returns {any}
*/
get gridcolor () {
return this.getAttribute('gridcolor');
}
/**
* @function set
* @returns {void}
*/
set gridcolor (value) {
this.setAttribute('gridcolor', value);
}
/**
* @function get
* @returns {any}
*/
get showrulers () {
return this.getAttribute('showrulers');
}
/**
* @function set
* @returns {void}
*/
set showrulers (value) {
this.setAttribute('showrulers', value);
}
/**
* @function get
* @returns {any}
*/
get baseunit () {
return this.getAttribute('baseunit');
}
/**
* @function set
* @returns {void}
*/
set baseunit (value) {
this.setAttribute('baseunit', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
const onCancelHandler = () => {
const closeEvent = new CustomEvent('change', { detail: {
dialog: 'closed'
} });
this.dispatchEvent(closeEvent);
};
const onSaveHandler = () => {
const color = this.$bgBlocks.querySelector('.cur_background').dataset.bgColor || '#FFF';
const closeEvent = new CustomEvent('change', { detail: {
lang: this.$langSelect.value,
dialog: 'close',
iconsize: this.$iconSize.value,
bgcolor: color,
bgurl: this.$bgURL.value,
gridsnappingon: this.$gridSnappingOn.checked,
gridsnappingstep: this.$gridSnappingStep.value,
showrulers: this.$showRulers.checked,
baseunit: this.$baseUnit.value
} });
this.dispatchEvent(closeEvent);
};
// Set up editor background functionality
const currentObj = this;
this.colorBlocks.forEach(function (e) {
const newdiv = document.createElement('div');
if (e === 'chessboard') {
newdiv.dataset.bgColor = e;
// eslint-disable-next-line max-len
newdiv.style.backgroundImage = 'url(data:image/gif;base64,R0lGODlhEAAQAIAAAP///9bW1iH5BAAAAAAALAAAAAAQABAAAAIfjG+gq4jM3IFLJgpswNly/XkcBpIiVaInlLJr9FZWAQA7)';
newdiv.classList.add('color_block');
} else {
newdiv.dataset.bgColor = e; // setAttribute('data-bgcolor', e);
newdiv.style.backgroundColor = e;
newdiv.classList.add('color_block');
}
currentObj.$bgBlocks.append(newdiv);
});
const blocks = this.$bgBlocks.querySelectorAll('div');
const curBg = 'cur_background';
blocks.forEach(function (blk) {
blk.addEventListener('click', function () {
blocks.forEach((el) => el.classList.remove(curBg));
blk.classList.add(curBg);
});
});
this.$saveBtn.addEventListener('click', onSaveHandler);
this.$cancelBtn.addEventListener('click', onCancelHandler);
this.$dialog.addEventListener('close', onCancelHandler);
}
}
// Register
customElements.define('se-edit-prefs-dialog', SeEditPrefsDialog);

View File

@@ -0,0 +1,207 @@
import './se-elix/define/NumberSpinBox.js';
const template = document.createElement('template');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<style>
#dialog_content {
margin: 10px 10px 5px 10px;
background: #5a6162;
overflow: auto;
text-align: left;
border: 1px solid #c8c8c8;
}
#dialog_content p, #dialog_content select, #dialog_content label {
margin: 10px;
line-height: 0.3em;
}
#dialog_container {
font-family: Verdana;
text-align: center;
left: 50%;
top: 50%;
max-width: 400px;
z-index: 50001;
background: #5a6162;
border: 1px outset #777;
font-family:Verdana,Helvetica,sans-serif;
font-size:0.8em;
}
#dialog_container, #dialog_content {
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
#dialog_buttons input[type=text] {
width: 90%;
display: block;
margin: 0 0 5px 11px;
}
#dialog_buttons input[type=button] {
margin: 0 1em;
}
.se-select{
text-align: center;
}
elix-number-spin-box{
margin-left: 15px;
}
</style>
<elix-dialog id="export_box" aria-label="export svg" closed>
<div class="overlay"></div>
<div id="dialog_container">
<div id="dialog_content">
<p class="se-select" id="export_select"></p>
<p class="se-select">
<select id="se-storage-pref">
<option value="PNG">PNG</option>
<option value="JPEG">JPEG</option>
<option value="BMP">BMP</option>
<option value="WEBP">WEBP</option>
<option value="PDF">PDF</option>
</select>
</p>
<p id="se-quality"><elix-number-spin-box min="-1" max="101" step="5" value="100"></elix-number-spin-box></p>
</div>
<div id="dialog_buttons">
<button id="export_ok"></button>
<button id="export_cancel"></button>
</div>
</div>
</elix-dialog>
`;
/**
* @class SeExportDialog
*/
export class SeExportDialog extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$dialog = this._shadowRoot.querySelector('#export_box');
this.$okBtn = this._shadowRoot.querySelector('#export_ok');
this.$cancelBtn = this._shadowRoot.querySelector('#export_cancel');
this.$exportOption = this._shadowRoot.querySelector('#se-storage-pref');
this.$qualityCont = this._shadowRoot.querySelector('#se-quality');
this.$input = this._shadowRoot.querySelector('elix-number-spin-box');
this.value = 1;
}
/**
* @function init
* @param {any} name
* @returns {void}
*/
init (i18next) {
this.setAttribute('common-ok', i18next.t('common.ok'));
this.setAttribute('common-cancel', i18next.t('common.cancel'));
this.setAttribute('ui-quality', i18next.t('ui.quality'));
this.setAttribute('ui-export_type_label', i18next.t('ui.export_type_label'));
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'dialog', 'common-ok', 'common-cancel', 'ui-quality', 'ui-export_type_label' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
// eslint-disable-next-line sonarjs/no-small-switch
let node;
switch (name) {
case 'dialog':
if (newValue === 'open') {
this.$dialog.open();
} else {
this.$dialog.close();
}
break;
case 'common-ok':
this.$okBtn.textContent = newValue;
break;
case 'common-cancel':
this.$cancelBtn.textContent = newValue;
break;
case 'ui-quality':
node = this._shadowRoot.querySelector('#se-quality');
node.prepend(newValue);
break;
case 'ui-export_type_label':
node = this._shadowRoot.querySelector('#export_select');
node.textContent = newValue;
break;
default:
// super.attributeChangedCallback(name, oldValue, newValue);
break;
}
}
/**
* @function get
* @returns {any}
*/
get dialog () {
return this.getAttribute('dialog');
}
/**
* @function set
* @returns {void}
*/
set dialog (value) {
this.setAttribute('dialog', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
this.$input.addEventListener('change', (e) => {
e.preventDefault();
this.value = e.target.value;
});
this.$input.addEventListener('click', (e) => {
e.preventDefault();
this.value = e.target.value;
});
const onSubmitHandler = (e, action) => {
if (action === 'cancel') {
document.getElementById('se-export-dialog').setAttribute('dialog', 'close');
} else {
const triggerEvent = new CustomEvent('change', { detail: {
trigger: action,
imgType: this.$exportOption.value,
quality: this.value
} });
this.dispatchEvent(triggerEvent);
}
};
const onChangeHandler = (e) => {
if (e.target.value === 'PDF') {
this.$qualityCont.style.display = 'none';
} else {
this.$qualityCont.style.display = 'block';
}
};
this.$okBtn.addEventListener('click', (evt) => onSubmitHandler(evt, 'ok'));
this.$cancelBtn.addEventListener('click', (evt) => onSubmitHandler(evt, 'cancel'));
this.$exportOption.addEventListener('change', (evt) => onChangeHandler(evt));
}
}
// Register
customElements.define('se-export-dialog', SeExportDialog);

View File

@@ -0,0 +1,448 @@
import { isValidUnit } from '../../common/units.js';
const template = document.createElement('template');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<style>
:not(:defined) {
display: none;
}
/* Force the scroll bar to appear so we see it hide when overlay opens. */
body::-webkit-scrollbar {
background: lightgray;
}
body::-webkit-scrollbar-thumb {
background: darkgray;
}
#svg_docprops #svg_docprops_container {
padding: 10px;
background-color: #5a6162;
color: #c5c5c5;
border: 1px outset #777;
opacity: 1.0;
font-family: Verdana, Helvetica, sans-serif;
font-size: .8em;
z-index: 20001;
}
#svg_docprops .error {
border: 1px solid red;
padding: 3px;
}
#svg_docprops #resolution {
max-width: 14em;
}
#tool_docprops_back {
margin-left: 1em;
overflow: auto;
}
#svg_docprops_container #svg_docprops_docprops {
float: left;
width: 221px;
margin: 5px .7em;
overflow: hidden;
}
#svg_docprops legend {
max-width: 195px;
}
#svg_docprops_docprops > legend {
font-weight: bold;
font-size: 1.1em;
}
#svg_docprops_container fieldset {
padding: 5px;
margin: 5px;
border: 1px solid #DDD;
}
#svg_docprops_container label {
display: block;
margin: .5em;
}
</style>
<elix-dialog id="svg_docprops" aria-label="Sample dialog" closed>
<div id="svg_docprops_container">
<div id="tool_docprops_back" class="toolbar_button">
<button id="tool_docprops_save"></button>
<button id="tool_docprops_cancel"></button>
</div>
<fieldset id="svg_docprops_docprops">
<legend id="svginfo_image_props"></legend>
<label>
<span id="svginfo_title"></span>
<input type="text" id="canvas_title" />
</label>
<fieldset id="change_resolution">
<legend id="svginfo_dim"></legend>
<label>
<span id="svginfo_width"></span>
<input type="text" id="canvas_width" size="6" />
</label>
<label>
<span id="svginfo_height"></span>
<input type="text" id="canvas_height" size="6" />
</label>
<label>
<select id="resolution">
<option id="selectedPredefined" selected="selected"></option>
<option>640x480</option>
<option>800x600</option>
<option>1024x768</option>
<option>1280x960</option>
<option>1600x1200</option>
<option id="fitToContent" value="content"></option>
</select>
</label>
</fieldset>
<fieldset id="image_save_opts">
<legend id="includedImages"></legend>
<label>
<input type="radio" id="image_embed" name="image_opt" value="embed" checked="checked" />
<span id="image_opt_embed"></span>
</label>
<label>
<input type="radio" id="image_ref" name="image_opt" value="ref" />
<span id="image_opt_ref"></span>
</label>
</fieldset>
</fieldset>
</div>
</elix-dialog>
`;
/**
* @class SeImgPropDialog
*/
export class SeImgPropDialog extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this.eventlisten = false;
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$saveBtn = this._shadowRoot.querySelector('#tool_docprops_save');
this.$cancelBtn = this._shadowRoot.querySelector('#tool_docprops_cancel');
this.$resolution = this._shadowRoot.querySelector('#resolution');
this.$canvasTitle = this._shadowRoot.querySelector('#canvas_title');
this.$canvasWidth = this._shadowRoot.querySelector('#canvas_width');
this.$canvasHeight = this._shadowRoot.querySelector('#canvas_height');
this.$imageOptEmbed = this._shadowRoot.querySelector('#image_embed');
this.$imageOptRef = this._shadowRoot.querySelector('#image_ref');
this.$dialog = this._shadowRoot.querySelector('#svg_docprops');
}
/**
* @function init
* @param {any} name
* @returns {void}
*/
init (i18next) {
this.setAttribute('common-ok', i18next.t('common.ok'));
this.setAttribute('common-cancel', i18next.t('common.cancel'));
this.setAttribute('config-image_props', i18next.t('config.image_props'));
this.setAttribute('config-doc_title', i18next.t('config.doc_title'));
this.setAttribute('config-doc_dims', i18next.t('config.doc_dims'));
this.setAttribute('common-width', i18next.t('common.width'));
this.setAttribute('common-height', i18next.t('common.height'));
this.setAttribute('config-select_predefined', i18next.t('config.select_predefined'));
this.setAttribute('tools-fit-to-content', i18next.t('tools.fitToContent'));
this.setAttribute('config-included_images', i18next.t('config.included_images'));
this.setAttribute('config-image_opt_embed', i18next.t('config.image_opt_embed'));
this.setAttribute('config-image_opt_ref', i18next.t('config.image_opt_ref'));
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'title', 'width', 'height', 'save', 'dialog', 'embed', 'common-ok',
'common-cancel', 'config-image_props', 'config-doc_title', 'config-doc_dims',
'common-width', 'common-height', 'config-select_predefined',
'tools-fit-to-content', 'config-included_images', 'config-image_opt_embed',
'config-image_opt_ref' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
let node ;
switch (name) {
case 'title':
this.$canvasTitle.value = newValue;
break;
case 'width':
if (newValue === 'fit') {
this.$canvasWidth.removeAttribute('disabled');
this.$canvasWidth.value = 100;
this.$canvasHeight.removeAttribute('disabled');
this.$canvasHeight.value = 100;
} else {
this.$canvasWidth.value = newValue;
}
break;
case 'height':
if (newValue === 'fit') {
this.$canvasWidth.removeAttribute('disabled');
this.$canvasWidth.value = 100;
this.$canvasHeight.removeAttribute('disabled');
this.$canvasHeight.value = 100;
} else {
this.$canvasHeight.value = newValue;
}
break;
case 'dialog':
if (this.eventlisten) {
if (newValue === 'open') {
this.$dialog.open();
} else {
this.$dialog.close();
}
}
break;
case 'save':
if (newValue === 'ref') {
this.$imageOptEmbed.setAttribute('checked', false);
this.$imageOptRef.setAttribute('checked', true);
} else {
this.$imageOptEmbed.setAttribute('checked', true);
this.$imageOptRef.setAttribute('checked', false);
}
break;
case 'embed':
if (newValue.includes('one')) {
const data = newValue.split('|');
if (data.length > 1) {
this._shadowRoot.querySelector('#image_opt_embed').setAttribute('title', data[1]);
this._shadowRoot.querySelector('#image_opt_embed').setAttribute('disabled', 'disabled');
this._shadowRoot.querySelector('#image_opt_embed').style.color = '#666';
}
}
break;
case 'common-ok':
this.$saveBtn.textContent = newValue;
break;
case 'common-cancel':
this.$cancelBtn.textContent = newValue;
break;
case 'config-image_props':
node = this._shadowRoot.querySelector('#svginfo_image_props');
node.textContent = newValue;
break;
case 'config-doc_title':
node = this._shadowRoot.querySelector('#svginfo_title');
node.textContent = newValue;
break;
case 'config-doc_dims':
node = this._shadowRoot.querySelector('#svginfo_dim');
node.textContent = newValue;
break;
case 'common-width':
node = this._shadowRoot.querySelector('#svginfo_width');
node.textContent = newValue;
break;
case 'common-height':
node = this._shadowRoot.querySelector('#svginfo_height');
node.textContent = newValue;
break;
case 'config-select_predefined':
node = this._shadowRoot.querySelector('#selectedPredefined');
node.textContent = newValue;
break;
case 'tools-fit-to-content':
node = this._shadowRoot.querySelector('#fitToContent');
node.textContent = newValue;
break;
case 'config-included_images':
node = this._shadowRoot.querySelector('#includedImages');
node.textContent = newValue;
break;
case 'config-image_opt_embed':
node = this._shadowRoot.querySelector('#image_opt_embed');
node.textContent = newValue;
break;
case 'config-image_opt_ref':
node = this._shadowRoot.querySelector('#image_opt_ref');
node.textContent = newValue;
break;
default:
super.attributeChangedCallback(name, oldValue, newValue);
break;
}
}
/**
* @function get
* @returns {any}
*/
get title () {
return this.getAttribute('title');
}
/**
* @function set
* @returns {void}
*/
set title (value) {
this.setAttribute('title', value);
}
/**
* @function get
* @returns {any}
*/
get width () {
return this.getAttribute('width');
}
/**
* @function set
* @returns {void}
*/
set width (value) {
this.setAttribute('width', value);
}
/**
* @function get
* @returns {any}
*/
get height () {
return this.getAttribute('height');
}
/**
* @function set
* @returns {void}
*/
set height (value) {
this.setAttribute('height', value);
}
/**
* @function get
* @returns {any}
*/
get save () {
return this.getAttribute('save');
}
/**
* @function set
* @returns {void}
*/
set save (value) {
this.setAttribute('save', value);
}
/**
* @function get
* @returns {any}
*/
get dialog () {
return this.getAttribute('dialog');
}
/**
* @function set
* @returns {void}
*/
set dialog (value) {
this.setAttribute('dialog', value);
}
/**
* @function get
* @returns {any}
*/
get embed () {
return this.getAttribute('embed');
}
/**
* @function set
* @returns {void}
*/
set embed (value) {
this.setAttribute('embed', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
const onChangeHandler = (ev) => {
if (!ev.target.selectedIndex) {
if (this.$canvasWidth.getAttribute('value') === 'fit') {
this.$canvasWidth.removeAttribute('disabled');
this.$canvasWidth.value = 100;
this.$canvasHeight.removeAttribute('disabled');
this.$canvasHeight.value = 100;
}
} else if (ev.target.value === 'content') {
this.$canvasWidth.setAttribute('disabled', 'disabled');
this.$canvasWidth.value = 'fit';
this.$canvasHeight.setAttribute('disabled', 'disabled');
this.$canvasHeight.value = 'fit';
} else {
const dims = ev.target.value.split('x');
this.$canvasWidth.value = dims[0];
this.$canvasWidth.removeAttribute('disabled');
this.$canvasHeight.value = dims[1];
this.$canvasHeight.removeAttribute('disabled');
}
};
const onSaveHandler = () => {
let saveOpt = '';
const w = this.$canvasWidth.value;
const h = this.$canvasHeight.value;
if (w !== 'fit' && !isValidUnit('width', w)) {
this.$canvasWidth.parentElement.classList.add('error');
} else {
this.$canvasWidth.parentElement.classList.remove('error');
}
if (h !== 'fit' && !isValidUnit('height', w)) {
this.$canvasHeight.parentElement.classList.add('error');
} else {
this.$canvasHeight.parentElement.classList.remove('error');
}
if (this.$imageOptEmbed.getAttribute('checked') === 'true') {
saveOpt = 'embed';
}
if (this.$imageOptRef.getAttribute('checked') === 'true') {
saveOpt = 'ref';
}
const closeEvent = new CustomEvent('change', { detail: {
title: this.$canvasTitle.value,
w: this.$canvasWidth.value,
h: this.$canvasHeight.value,
save: saveOpt,
dialog: 'close'
} });
this.$canvasWidth.removeAttribute('disabled');
this.$canvasHeight.removeAttribute('disabled');
this.$resolution.selectedIndex = 0;
this.dispatchEvent(closeEvent);
};
const onCancelHandler = () => {
const closeEvent = new CustomEvent('change', { detail: {
dialog: 'closed'
} });
this.$canvasWidth.removeAttribute('disabled');
this.$canvasHeight.removeAttribute('disabled');
this.$resolution.selectedIndex = 0;
this.dispatchEvent(closeEvent);
};
this.$resolution.addEventListener('change', onChangeHandler);
this.$saveBtn.addEventListener('click', onSaveHandler);
this.$cancelBtn.addEventListener('click', onCancelHandler);
this.$dialog.addEventListener('close', onCancelHandler);
this.eventlisten = true;
}
}
// Register
customElements.define('se-img-prop-dialog', SeImgPropDialog);

View File

@@ -0,0 +1,11 @@
import 'elix/define/Dialog.js';
import './imagePropertiesDialog.js';
import './editorPreferencesDialog.js';
import './svgSourceDialog.js';
import './cmenuDialog.js';
import './cmenuLayersDialog.js';
import './seSelectDialog.js';
import './seConfirmDialog.js';
import './sePromptDialog.js';
import './seAlertDialog.js';
import './exportDialog.js';

View File

@@ -0,0 +1,7 @@
import PlainNumberSpinBox from '../src/plain/PlainNumberSpinBox.js';
/**
* @class ElixNumberSpinBox
*/
export default class ElixNumberSpinBox extends PlainNumberSpinBox {}
customElements.define('elix-number-spin-box', ElixNumberSpinBox);

View File

@@ -0,0 +1,247 @@
/* eslint-disable class-methods-use-this */
import {
defaultState,
setState,
state,
stateEffects
} from 'elix/src/base/internal.js';
import {
SpinBox
} from 'elix/src/base/SpinBox.js';
/**
* @class NumberSpinBox
*/
class NumberSpinBox extends SpinBox {
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (name === 'max') {
this.max = parseFloat(newValue);
} else if (name === 'min') {
this.min = parseFloat(newValue);
} else if (name === 'step') {
this.step = parseFloat(newValue);
} else {
super.attributeChangedCallback(name, oldValue, newValue);
}
}
/**
* @function observedAttributes
* @returns {any} observed
*/
get [defaultState] () {
return Object.assign(super[defaultState], {
max: null,
min: null,
step: 1
});
}
/**
* @function formatValue
* Format the numeric value as a string.
*
* This is used after incrementing/decrementing the value to reformat the
* value as a string.
*
* @param {number} value
* @param {number} precision
* @returns {number}
*/
formatValue (value, precision) {
return Number(value).toFixed(precision);
}
/**
* The maximum allowable value of the `value` property.
*
* @type {number|null}
* @default 1
*/
get max () {
return this[state].max;
}
/**
* The maximum allowable value of the `value` property.
*
* @type {number|null}
* @default 1
*/
set max (max) {
this[setState]({
max
});
}
/**
* The minimum allowable value of the `value` property.
*
* @type {number|null}
* @default 1
*/
get min () {
return this[state].min;
}
/**
* @function set
* @returns {void}
*/
set min (min) {
this[setState]({
min
});
}
/**
* @function parseValue
* @param {number} value
* @param {number} precision
* @returns {int}
*/
parseValue (value, precision) {
const parsed = precision === 0 ? parseInt(value) : parseFloat(value);
return isNaN(parsed) ? 0 : parsed;
}
/**
* @function stateEffects
* @param {any} state
* @param {any} changed
* @returns {any}
*/
[stateEffects] (state, changed) {
const effects = super[stateEffects];
// If step changed, calculate its precision (number of digits after
// the decimal).
if (changed.step) {
const {
step
} = state;
const decimalRegex = /\.(\d)+$/;
const match = decimalRegex.exec(String(step));
const precision = match && match[1] ? match[1].length : 0;
Object.assign(effects, {
precision
});
}
if (changed.max || changed.min || changed.value) {
// The value is valid if it falls between the min and max.
// TODO: We need a way to let other classes/mixins on the prototype chain
// contribute to validity -- if someone else thinks the value is invalid,
// we should respect that, even if the value falls within the min/max
// bounds.
const {
max,
min,
precision,
value
} = state;
const parsed = parseInt(value, precision);
if (value !== '' && isNaN(parsed)) {
Object.assign(effects, {
valid: false,
validationMessage: 'Value must be a number'
});
} else if (!(max === null || parsed <= max)) {
Object.assign(effects, {
valid: false,
validationMessage: `Value must be less than or equal to ${max}.`
});
} else if (!(min === null || parsed >= min)) {
Object.assign(effects, {
valid: false,
validationMessage: `Value must be greater than or equal to ${min}.`
});
} else {
Object.assign(effects, {
valid: true,
validationMessage: ''
});
}
// We can only go up if we're below max.
Object.assign(effects, {
canGoUp: isNaN(parsed) || state.max === null || parsed <= state.max
});
// We can only go down if we're above min.
Object.assign(effects, {
canGoDown: isNaN(parsed) || state.min === null || parsed >= state.min
});
}
return effects;
}
/**
* @function get
* @returns {any}
*/
get step () {
return this[state].step;
}
/**
* @function set
* @returns {void}
*/
set step (step) {
if (!isNaN(step)) {
this[setState]({
step
});
}
}
/**
* @function stepDown
* @returns {void}
*/
stepDown () {
super.stepDown();
const {
max,
precision,
value
} = this[state];
let result = this.parseValue(value, precision) - this.step;
if (max !== null) {
result = Math.min(result, max);
}
const {
min
} = this[state];
if (min === null || result >= min) {
this.value = this.formatValue(result, precision);
}
}
/**
* @function stepUp
* @returns {void}
*/
stepUp () {
super.stepUp();
const {
min,
precision,
value
} = this[state];
let result = this.parseValue(value, precision) + this.step;
if (min !== null) {
result = Math.max(result, min);
}
const {
max
} = this[state];
if (max === null || result <= max) {
this.value = this.formatValue(result, precision);
}
}
}
export default NumberSpinBox;

View File

@@ -0,0 +1,9 @@
import PlainSpinBoxMixin from 'elix/src/plain/PlainSpinBoxMixin.js';
import NumberSpinBox from '../base/NumberSpinBox.js';
/**
* @class PlainNumberSpinBox
*/
class PlainNumberSpinBox extends PlainSpinBoxMixin(NumberSpinBox) {}
export default PlainNumberSpinBox;

View File

@@ -0,0 +1,10 @@
import SePlainAlertDialog from './SePlainAlertDialog.js';
const seAlert = (text) => {
const dialog = new SePlainAlertDialog();
dialog.textContent = text;
dialog.choices = [ 'Ok' ];
dialog.open();
};
window.seAlert = seAlert;

View File

@@ -0,0 +1,12 @@
import SePlainAlertDialog from './SePlainAlertDialog.js';
const seConfirm = async (text, choices) => {
const dialog = new SePlainAlertDialog();
dialog.textContent = text;
dialog.choices = (choices === undefined) ? [ 'Ok', 'Cancel' ] : choices;
dialog.open();
const response = await dialog.whenClosed();
return response.choice;
};
window.seConfirm = seConfirm;

View File

@@ -0,0 +1,83 @@
import SePlainAlertDialog from './SePlainAlertDialog.js';
/**
* @class SePromptDialog
*/
export class SePromptDialog extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this.dialog = new SePlainAlertDialog();
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'title', 'close' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
switch (name) {
case 'title':
if (this.dialog.opened) {
this.dialog.close();
}
this.dialog.textContent = newValue;
this.dialog.choices = [ 'Cancel' ];
this.dialog.open();
break;
case 'close':
if (this.dialog.opened) {
this.dialog.close();
}
break;
default:
// eslint-disable-next-line no-console
console.error('unknown attr for:', name, 'newValue =', newValue);
break;
}
}
/**
* @function get
* @returns {any}
*/
get title () {
return this.getAttribute('title');
}
/**
* @function set
* @returns {void}
*/
set title (value) {
this.setAttribute('title', value);
}
/**
* @function get
* @returns {any}
*/
get close () {
return this.getAttribute('close');
}
/**
* @function set
* @returns {void}
*/
set close (value) {
this.setAttribute('close', value);
}
}
// Register
customElements.define('se-prompt-dialog', SePromptDialog);

View File

@@ -0,0 +1,12 @@
import SePlainAlertDialog from './SePlainAlertDialog.js';
const seSelect = async (text, choices) => {
const dialog = new SePlainAlertDialog();
dialog.textContent = text;
dialog.choices = choices;
dialog.open();
const response = await dialog.whenClosed();
return response.choice;
};
window.seSelect = seSelect;

View File

@@ -0,0 +1,265 @@
const template = document.createElement('template');
// eslint-disable-next-line no-unsanitized/property
template.innerHTML = `
<style>
:not(:defined) {
display: none;
}
/* Force the scroll bar to appear so we see it hide when overlay opens. */
body::-webkit-scrollbar {
background: lightgray;
}
body::-webkit-scrollbar-thumb {
background: darkgray;
}
#svg_source_editor #svg_source_container {
background-color: #5a6162;
color: #c5c5c5;
opacity: 1.0;
text-align: center;
border: 1px outset #777;
z-index: 6;
}
#save_output_btns {
display: none;
text-align: left;
}
#save_output_btns p {
margin: .5em 1.5em;
display: inline-block;
}
#svg_source_editor form {
width: 100%;
}
#svg_source_editor #svg_source_textarea {
padding: 5px;
font-size: 12px;
min-height: 200px;
width: 95%;
height: 95%;
}
#svg_source_editor #tool_source_back {
text-align: left;
height: 30px;
}
#tool_source_save {
width: 20%;
background-color: #c79605;
margin-left: 30%;
margin-top: 5px;
}
#tool_source_cancel {
width: 20%;
background-color: #c8c8c8;
}
</style>
<elix-dialog id="svg_source_editor" aria-label="SVG Source Editor" closed>
<div id="svg_source_container">
<div id="tool_source_back" class="toolbar_button">
<button id="tool_source_save"></button>
<button id="tool_source_cancel"></button>
</div>
<div id="save_output_btns">
<p id="copy_save_note"></p>
<button id="copy_save_done"></button>
</div>
<form>
<textarea id="svg_source_textarea" spellcheck="false" rows="5" cols="80"></textarea>
</form>
</div>
</elix-dialog>
`;
/**
* @class SeSvgSourceEditorDialog
*/
export class SeSvgSourceEditorDialog extends HTMLElement {
/**
* @function constructor
*/
constructor () {
super();
// create the shadowDom and insert the template
this._shadowRoot = this.attachShadow({ mode: 'open' });
this._shadowRoot.append(template.content.cloneNode(true));
this.$dialog = this._shadowRoot.querySelector('#svg_source_editor');
this.$copyBtn = this._shadowRoot.querySelector('#copy_save_done');
this.$saveBtn = this._shadowRoot.querySelector('#tool_source_save');
this.$cancelBtn = this._shadowRoot.querySelector('#tool_source_cancel');
this.$sourceTxt = this._shadowRoot.querySelector('#svg_source_textarea');
this.$copySec = this._shadowRoot.querySelector('#save_output_btns');
this.$applySec = this._shadowRoot.querySelector('#tool_source_back');
}
/**
* @function init
* @param {any} name
* @returns {void}
*/
init (i18next) {
this.setAttribute('tools-source_save', i18next.t('tools.source_save'));
this.setAttribute('common-cancel', i18next.t('common.cancel'));
this.setAttribute('notification-source_dialog_note', i18next.t('notification.source_dialog_note'));
this.setAttribute('config-done', i18next.t('config.done'));
}
/**
* @function observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return [ 'dialog', 'value', 'applysec', 'copysec', 'tools-source_save', 'common-cancel', 'notification-source_dialog_note', 'config-done' ];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
let node;
switch (name) {
case 'dialog':
if (newValue === 'open') {
this.$sourceTxt.focus();
this.$dialog.open();
} else {
this.$dialog.close();
this.$sourceTxt.blur();
}
break;
case 'applysec':
if (newValue === 'false') {
this.$applySec.style.display = 'none';
} else {
this.$applySec.style.display = 'block';
}
break;
case 'copysec':
if (newValue === 'false') {
this.$copySec.style.display = 'none';
} else {
this.$copySec.style.display = 'block';
}
break;
case 'value':
this.$sourceTxt.value = newValue;
break;
case 'tools-source_save':
this.$saveBtn.textContent = newValue;
break;
case 'common-cancel':
this.$cancelBtn.textContent = newValue;
break;
case 'notification-source_dialog_note':
node = this._shadowRoot.querySelector('#copy_save_note');
node.textContent = newValue;
break;
case 'config-done':
this.$copyBtn.textContent = newValue;
break;
default:
super.attributeChangedCallback(name, oldValue, newValue);
break;
}
}
/**
* @function get
* @returns {any}
*/
get dialog () {
return this.getAttribute('dialog');
}
/**
* @function set
* @returns {void}
*/
set dialog (value) {
this.setAttribute('dialog', value);
}
/**
* @function get
* @returns {any}
*/
get value () {
return this.getAttribute('value');
}
/**
* @function set
* @returns {void}
*/
set value (value) {
this.setAttribute('value', value);
}
/**
* @function get
* @returns {any}
*/
get applysec () {
return this.getAttribute('applysec');
}
/**
* @function set
* @returns {void}
*/
set applysec (value) {
this.setAttribute('applysec', value);
}
/**
* @function get
* @returns {any}
*/
get copysec () {
return this.getAttribute('copysec');
}
/**
* @function set
* @returns {void}
*/
set copysec (value) {
this.setAttribute('copysec', value);
}
/**
* @function connectedCallback
* @returns {void}
*/
connectedCallback () {
const onCancelHandler = () => {
const closeEvent = new CustomEvent('change', { detail: {
dialog: 'closed'
} });
this.dispatchEvent(closeEvent);
};
const onCopyHandler = () => {
const closeEvent = new CustomEvent('change', {
detail: {
copy: 'click',
value: this.$sourceTxt.value
}
});
this.dispatchEvent(closeEvent);
};
const onSaveHandler = () => {
const closeEvent = new CustomEvent('change', { detail: {
value: this.$sourceTxt.value,
dialog: 'close'
} });
this.dispatchEvent(closeEvent);
};
this.$copyBtn.addEventListener('click', onCopyHandler);
this.$saveBtn.addEventListener('click', onSaveHandler);
this.$cancelBtn.addEventListener('click', onCancelHandler);
this.$dialog.addEventListener('close', onCancelHandler);
}
}
// Register
customElements.define('se-svg-source-editor-dialog', SeSvgSourceEditorDialog);

View File

@@ -1,12 +1,9 @@
/* globals jQuery */
/**
* Attaches items to DOM for Embedded SVG support.
* @module EmbeddedSVGEditDOM
*/
import EmbeddedSVGEdit from './embedapi.js';
import {isChrome} from '../common/browser.js';
const $ = jQuery;
import { isChrome } from '../common/browser.js';
let svgCanvas = null;
@@ -18,10 +15,10 @@ let svgCanvas = null;
function handleSvgData (data, error) {
if (error) {
// Todo: This should be replaced with a general purpose dialog alert library call
alert('error ' + error); // eslint-disable-line no-alert
alert('error ' + error);
} else {
// Todo: This should be replaced with a general purpose dialog alert library call
alert('Congratulations. Your SVG string is back in the host page, do with it what you will\n\n' + data); // eslint-disable-line no-alert
alert('Congratulations. Your SVG string is back in the host page, do with it what you will\n\n' + data);
}
}
@@ -98,21 +95,25 @@ function exportPDF () {
const frameBase = 'https://raw.githack.com/SVG-Edit/svgedit/master';
// const frameBase = 'http://localhost:8001';
const framePath = '/editor/xdomain-svg-editor-es.html?extensions=ext-xdomain-messaging.js';
const iframe = $('<iframe width="900px" height="600px" id="svgedit" src="javascript:0"></iframe>');
iframe[0].src = frameBase + framePath +
const iframe = document.createElement('iframe');
iframe.id = "svgedit";
iframe.style.width = "900px";
iframe.style.width = "600px";
iframe.src = frameBase + framePath +
(location.href.includes('?')
// ? location.href.replace(/\?(?<search>.*)$/, '&$<search>')
? location.href.replace(/\?(.*)$/, '&$1')
: ''); // Append arguments to this file onto the iframe
iframe[0].addEventListener('load', function () {
svgCanvas = new EmbeddedSVGEdit(frame, [new URL(frameBase).origin]);
iframe.addEventListener('load', function () {
svgCanvas = new EmbeddedSVGEdit(frame, [ new URL(frameBase).origin ]);
const { $id } = svgCanvas;
// Hide main button, as we will be controlling new, load, save, etc. from the host document
let doc;
try {
doc = frame.contentDocument || frame.contentWindow.document;
} catch (err) {
console.log('Blocked from accessing document', err); // eslint-disable-line no-console
console.error('Blocked from accessing document', err);
}
if (doc) {
// Todo: Provide a way to get this to occur by `postMessage`
@@ -121,10 +122,10 @@ iframe[0].addEventListener('load', function () {
}
// Add event handlers now that `svgCanvas` is ready
$('#load').click(loadSvg);
$('#save').click(saveSvg);
$('#exportPNG').click(exportPNG);
$('#exportPDF').click(exportPDF);
$id('load').addEventListener('click', loadSvg);
$id('save').addEventListener('click', saveSvg);
$id('exportPNG').addEventListener('click', exportPNG);
$id('exportPDF').addEventListener('click', exportPDF);
});
$('body').append(iframe);
document.body.appendChild(iframe);
const frame = document.getElementById('svgedit');

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>Embed API</title>
<link rel="icon" type="image/png" href="images/logo.png"/>
<link rel="icon" type="image/png" href="images/logo.svg"/>
<script src="jquery.min.js"></script>
<script type="module" src="embedapi-dom.js"></script>
</head>

View File

@@ -24,7 +24,7 @@ let cbid = 0;
function getCallbackSetter (funcName) {
return function (...args) {
const that = this, // New callback
callbackID = this.send(funcName, args, function () { /* empty */ }); // The callback (currently it's nothing, but will be set later)
callbackID = this.send(funcName, args, function () { /* empty fn */ }); // The callback (currently it's nothing, but will be set later)
return function (newCallback) {
that.callbacks[callbackID] = newCallback; // Set callback
@@ -43,7 +43,7 @@ function getCallbackSetter (funcName) {
* @param {Integer} data.id
* @returns {void}
*/
function addCallback (t, {result, error, id: callbackID}) {
function addCallback (t, { result, error, id: callbackID }) {
if (typeof callbackID === 'number' && t.callbacks[callbackID]) {
// These should be safe both because we check `cbid` is numeric and
// because the calls are from trusted origins
@@ -62,16 +62,15 @@ function addCallback (t, {result, error, id: callbackID}) {
function messageListener (e) {
// We accept and post strings as opposed to objects for the sake of IE9 support; this
// will most likely be changed in the future
if (!e.data || !['string', 'object'].includes(typeof e.data)) {
if (!e.data || ![ 'string', 'object' ].includes(typeof e.data)) {
return;
}
const {allowedOrigins} = this,
const { allowedOrigins } = this,
data = typeof e.data === 'object' ? e.data : JSON.parse(e.data);
if (!data || typeof data !== 'object' || data.namespace !== 'svg-edit' ||
e.source !== this.frame.contentWindow ||
(!allowedOrigins.includes('*') && !allowedOrigins.includes(e.origin))
) {
// eslint-disable-next-line no-console -- Info for developers
console.error(
`The origin ${e.origin} was not whitelisted as an origin from ` +
`which responses may be received by this ${window.origin} script.`
@@ -331,10 +330,10 @@ class EmbeddedSVGEdit {
// Older IE may need a polyfill for addEventListener, but so it would for SVG
window.addEventListener('message', getMessageListener(this));
window.addEventListener('keydown', (e) => {
const {type, key} = e;
const { type, key } = e;
if (key === 'Backspace') {
e.preventDefault();
const keyboardEvent = new KeyboardEvent(type, {key});
const keyboardEvent = new KeyboardEvent(type, { key });
that.frame.contentDocument.dispatchEvent(keyboardEvent);
}
});
@@ -346,7 +345,7 @@ class EmbeddedSVGEdit {
* @param {GenericCallback} callback (This may be better than a promise in case adding an event.)
* @returns {Integer}
*/
send (name, args, callback) { // eslint-disable-line promise/prefer-await-to-callbacks
send (name, args, callback) {
const that = this;
cbid++;
@@ -364,8 +363,8 @@ class EmbeddedSVGEdit {
let sameOriginWithGlobal = false;
try {
sameOriginWithGlobal = window.location.origin === that.frame.contentWindow.location.origin &&
that.frame.contentWindow.svgEditor.canvas;
} catch (err) {}
that.frame.contentWindow.svgEditor.svgCanvas;
}catch (err) {/* empty */}
if (sameOriginWithGlobal) {
// Although we do not really need this API if we are working same
@@ -374,8 +373,8 @@ class EmbeddedSVGEdit {
// of the current JSON-based communication API (e.g., not passing
// callbacks). We might be able to address these shortcomings; see
// the todo elsewhere in this file.
const message = {id: callbackID},
{svgEditor: {canvas: svgCanvas}} = that.frame.contentWindow;
const message = { id: callbackID },
{ svgEditor: { canvas: svgCanvas } } = that.frame.contentWindow;
try {
message.result = svgCanvas[name](...args);
} catch (err) {

View File

@@ -7,9 +7,11 @@
*
*/
const loadExtensionTranslation = async function (lang) {
const loadExtensionTranslation = async function (svgEditor) {
let translationModule;
const lang = svgEditor.configObj.pref('lang');
try {
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
@@ -23,11 +25,12 @@ export default {
name: 'arrows',
async init (S) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const svgCanvas = svgEditor.canvas;
const strings = await loadExtensionTranslation(svgEditor);
const { svgCanvas } = svgEditor;
const { $id } = svgCanvas;
const
addElem = svgCanvas.addSVGElementFromJson,
{nonce, $} = S,
{ nonce } = S,
prefix = 'se_arrow_';
let selElems, arrowprefix, randomizeIds = S.randomize_ids;
@@ -48,7 +51,7 @@ export default {
* @param {Window} win
* @returns {void}
*/
function unsetArrowNonce (win) {
function unsetArrowNonce (_win) {
randomizeIds = false;
arrowprefix = prefix;
pathdata.fw.id = arrowprefix + 'fw';
@@ -58,11 +61,11 @@ export default {
svgCanvas.bind('setnonce', setArrowNonce);
svgCanvas.bind('unsetnonce', unsetArrowNonce);
arrowprefix = randomizeIds ? prefix + nonce + '_' : prefix;
arrowprefix = (randomizeIds) ? `${prefix}${nonce}_` : prefix;
const pathdata = {
fw: {d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', refx: 8, id: arrowprefix + 'fw'},
bk: {d: 'm10,0l-10,5l10,5l-5,-5l5,-5z', refx: 2, id: arrowprefix + 'bk'}
fw: { d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', refx: 8, id: arrowprefix + 'fw' },
bk: { d: 'm10,0l-10,5l10,5l-5,-5l5,-5z', refx: 2, id: arrowprefix + 'bk' }
};
/**
@@ -88,7 +91,7 @@ export default {
* @returns {void}
*/
function showPanel (on) {
$('#arrow_panel').toggle(on);
$id('arrow_panel').style.display = (on) ? 'block' : 'none';
if (on) {
const el = selElems[0];
const end = el.getAttribute('marker-end');
@@ -112,7 +115,7 @@ export default {
val = 'none';
}
$('#arrow_list').val(val);
$id('arrow_list').value = val;
}
}
@@ -211,25 +214,26 @@ export default {
*/
function colorChanged (elem) {
const color = elem.getAttribute('stroke');
const mtypes = ['start', 'mid', 'end'];
const mtypes = [ 'start', 'mid', 'end' ];
const defs = svgCanvas.findDefs();
$.each(mtypes, function (i, type) {
mtypes.forEach(function(type){
const marker = getLinked(elem, 'marker-' + type);
if (!marker) { return; }
const curColor = $(marker).children().attr('fill');
const curD = $(marker).children().attr('d');
const curColor = marker.children.getAttribute('fill');
const curD = marker.children.getAttribute('d');
if (curColor === color) { return; }
const allMarkers = $(defs).find('marker');
const allMarkers = defs.querySelectorAll('marker');
let newMarker = null;
// Different color, check if already made
allMarkers.each(function () {
const attrs = $(this).children().attr(['fill', 'd']);
if (attrs.fill === color && attrs.d === curD) {
Array.from(allMarkers).forEach(function(marker) {
const attrsFill = marker.children.getAttribute('fill');
const attrsD = marker.children.getAttribute('d');
if (attrsFill === color && attrsD === curD) {
// Found another marker with this color and this path
newMarker = this;
newMarker = marker;
}
});
@@ -240,17 +244,17 @@ export default {
newMarker = addMarker(dir, type, arrowprefix + dir + allMarkers.length);
$(newMarker).children().attr('fill', color);
newMarker.children.setAttribute('fill', color);
}
$(elem).attr('marker-' + type, 'url(#' + newMarker.id + ')');
elem.setAttribute('marker-' + type, 'url(#' + newMarker.id + ')');
// Check if last marker can be removed
let remove = true;
$(S.svgcontent).find('line, polyline, path, polygon').each(function () {
const element = this;
$.each(mtypes, function (j, mtype) {
if ($(element).attr('marker-' + mtype) === 'url(#' + marker.id + ')') {
const sElements = S.svgcontent.querySelectorAll('line, polyline, path, polygon');
Array.prototype.forEach.call(sElements, function(element){
mtypes.forEach(function(mtype){
if (element.getAttribute('marker-' + mtype) === 'url(#' + marker.id + ')') {
remove = false;
return remove;
}
@@ -262,7 +266,7 @@ export default {
// Not found, so can safely remove
if (remove) {
$(marker).remove();
marker.remove();
}
});
}
@@ -285,12 +289,13 @@ export default {
return Object.assign(contextTools[i], contextTool);
}),
callback () {
$('#arrow_panel').hide();
$id("arrow_panel").style.display = 'none';
// Set ID so it can be translated in locale file
$('#arrow_list option')[0].id = 'connector_no_arrow';
$id('arrow_list option').setAttribute('id', 'connector_no_arrow');
},
async addLangData ({lang, importLocale}) {
const {langList} = await importLocale();
async addLangData ({ _lang, importLocale }) {
const { langList } = await importLocale();
return {
data: langList
};
@@ -299,7 +304,7 @@ export default {
// Use this to update the current selected elements
selElems = opts.elems;
const markerElems = ['line', 'path', 'polyline', 'polygon'];
const markerElems = [ 'line', 'path', 'polyline', 'polygon' ];
let i = selElems.length;
while (i--) {
const elem = selElems[i];

View File

@@ -1,7 +1,7 @@
export default {
name: 'Arrows',
langList: [
{id: 'arrow_none', textContent: 'No arrow'}
{ id: 'arrow_none', textContent: 'No arrow' }
],
contextTools: [
{

View File

@@ -1,7 +1,7 @@
export default {
name: 'Arrows',
langList: [
{id: 'arrow_none', textContent: 'Sans flèche'}
{ id: 'arrow_none', textContent: 'Sans flèche' }
],
contextTools: [
{

View File

@@ -1,7 +1,7 @@
export default {
name: '箭头',
langList: [
{id: 'arrow_none', textContent: '无箭头'}
{ id: 'arrow_none', textContent: '无箭头' }
],
contextTools: [
{

View File

@@ -10,6 +10,7 @@
const loadExtensionTranslation = async function (lang) {
let translationModule;
try {
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
@@ -23,20 +24,22 @@ const loadExtensionTranslation = async function (lang) {
// The button toggles whether the path is open or closed
export default {
name: 'closepath',
async init ({importLocale, $}) {
async init ({ _importLocale }) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const { svgCanvas } = svgEditor;
const { $id } = svgCanvas;
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
let selElems;
const updateButton = function (path) {
const seglist = path.pathSegList,
closed = seglist.getItem(seglist.numberOfItems - 1).pathSegType === 1,
showbutton = closed ? '#tool_openpath' : '#tool_closepath',
hidebutton = closed ? '#tool_closepath' : '#tool_openpath';
$(hidebutton).hide();
$(showbutton).show();
showbutton = closed ? 'tool_openpath' : 'tool_closepath',
hidebutton = closed ? 'tool_closepath' : 'tool_openpath';
$id(hidebutton).style.display = 'none';
$id(showbutton).style.display = 'block';
};
const showPanel = function (on) {
$('#closepath_panel').toggle(on);
$id('closepath_panel').style.display = (on) ? 'block' : 'none';
if (on) {
const path = selElems[0];
if (path) { updateButton(path); }
@@ -89,7 +92,7 @@ export default {
return Object.assign(buttons[i], button);
}),
callback () {
$('#closepath_panel').hide();
$id("closepath_panel").style.display = 'none';
},
selectedChanged (opts) {
selElems = opts.elems;

View File

@@ -7,41 +7,44 @@
*
*/
const loadExtensionTranslation = async function (lang) {
const name = "connector";
const loadExtensionTranslation = async function (svgEditor) {
let translationModule;
const lang = svgEditor.configObj.pref('lang');
try {
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${lang}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
console.error(`Missing translation (${lang}) - using 'en'`);
console.warn(`Missing translation (${lang}) for ${name} - using 'en'`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/en.js`);
}
return translationModule.default;
svgEditor.i18next.addResourceBundle(lang, name, translationModule.default);
};
export default {
name: 'connector',
async init (S) {
name,
async init(S) {
const svgEditor = this;
const svgCanvas = svgEditor.canvas;
const {getElem} = svgCanvas;
const {$, svgroot} = S,
const { svgCanvas } = svgEditor;
const { getElem, $id, mergeDeep } = svgCanvas;
const { $, svgroot } = S,
addElem = svgCanvas.addSVGElementFromJson,
selManager = S.selectorManager,
connSel = '.se_connector',
// connect_str = '-SE_CONNECT-',
elData = $.data;
selManager = S.selectorManager;
await loadExtensionTranslation(svgEditor);
let startX,
startY,
curLine,
startElem,
endElem,
seNs,
{svgcontent} = S,
started = false,
connections = [],
selElems = [];
let startX;
let startY;
let curLine;
let startElem;
let endElem;
let seNs;
let { svgcontent } = S;
let started = false;
let connections = [];
let selElems = [];
/**
*
@@ -51,10 +54,10 @@ export default {
* @param {Float} offset
* @returns {module:math.XYObject}
*/
function getBBintersect (x, y, bb, offset) {
const getBBintersect = (x, y, bb, offset) => {
if (offset) {
offset -= 0;
bb = $.extend({}, bb);
bb = mergeDeep({}, bb);
bb.width += offset;
bb.height += offset;
bb.x -= offset / 2;
@@ -81,34 +84,36 @@ export default {
x: midX + lenX * ratio,
y: midY + lenY * ratio
};
}
};
/**
* @param {"start"|"end"} side
* @param {Element} line
* @returns {Float}
*/
function getOffset (side, line) {
const getOffset = (side, line) => {
const giveOffset = line.getAttribute('marker-' + side);
// const giveOffset = $(line).data(side+'_off');
// TODO: Make this number (5) be based on marker width/height
const size = line.getAttribute('stroke-width') * 5;
return giveOffset ? size : 0;
}
};
/**
* @param {boolean} on
* @returns {void}
*/
function showPanel (on) {
let connRules = $('#connector_rules');
if (!connRules.length) {
connRules = $('<style id="connector_rules"></style>').appendTo('head');
const showPanel = (on) => {
let connRules = $id('connector_rules');
if (!connRules) {
connRules = document.createElement('style');
connRules.setAttribute('id', 'connector_rules');
document.getElementsByTagName("head")[0].appendChild(connRules);
}
connRules.text(!on ? '' : '#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }');
$('#connector_panel').toggle(on);
}
connRules.textContent = (!on ? '' : '#tool_clone, #tool_topath, #tool_angle, #xy_panel { display: none !important; }');
if ($id('connector_panel'))
$id('connector_panel').style.display = (on) ? 'block' : 'none';
};
/**
* @param {Element} elem
@@ -118,7 +123,7 @@ export default {
* @param {boolean} [setMid]
* @returns {void}
*/
function setPoint (elem, pos, x, y, setMid) {
const setPoint = (elem, pos, x, y, setMid) => {
const pts = elem.points;
const pt = svgroot.createSVGPoint();
pt.x = x;
@@ -144,14 +149,15 @@ export default {
const ptEnd = pts.getItem(pts.numberOfItems - 1);
setPoint(elem, 1, (ptEnd.x + ptStart.x) / 2, (ptEnd.y + ptStart.y) / 2);
}
}
};
/**
* @param {Float} diffX
* @param {Float} diffY
* @returns {void}
*/
function updateLine (diffX, diffY) {
const updateLine = (diffX, diffY) => {
const dataStorage = svgCanvas.getDataStorage();
// Update line with element
let i = connections.length;
while (i--) {
@@ -163,82 +169,79 @@ export default {
// const sw = line.getAttribute('stroke-width') * 5;
// Update bbox for this element
const bb = elData(line, pre + '_bb');
const bb = dataStorage.get(line, pre + '_bb');
bb.x = conn.start_x + diffX;
bb.y = conn.start_y + diffY;
elData(line, pre + '_bb', bb);
dataStorage.put(line, pre + '_bb', bb);
const altPre = conn.is_start ? 'end' : 'start';
// Get center pt of connected element
const bb2 = elData(line, altPre + '_bb');
const bb2 = dataStorage.get(line, altPre + '_bb');
const srcX = bb2.x + bb2.width / 2;
const srcY = bb2.y + bb2.height / 2;
// Set point of element being moved
const pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0
const pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line));
setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true);
// Set point of connected element
const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line));
const pt2 = getBBintersect(pt.x, pt.y, dataStorage.get(line, altPre + '_bb'), getOffset(altPre, line));
setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true);
}
}
};
/**
*
* @param {Element[]} [elems=selElems] Array of elements
* @returns {void}
*/
function findConnectors (elems = selElems) {
const connectors = $(svgcontent).find(connSel);
const findConnectors = (elems = selElems) => {
const dataStorage = svgCanvas.getDataStorage();
// const connectors = svgcontent.querySelectorAll('.se_connector');
const connectors = svgcontent.querySelectorAll('.se_connector');
connections = [];
// Loop through connectors to see if one is connected to the element
connectors.each(function () {
Array.prototype.forEach.call(connectors, function (ethis) {
let addThis;
/**
*
* @returns {void}
*/
function add () {
if (elems.includes(this)) {
// Pretend this element is selected
addThis = true;
}
}
// Grab the ends
const parts = [];
['start', 'end'].forEach(function (pos, i) {
[ 'start', 'end' ].forEach(function (pos, i) {
const key = 'c_' + pos;
let part = elData(this, key);
let part = dataStorage.get(ethis, key);
if (part === null || part === undefined) { // Does this ever return nullish values?
part = document.getElementById(
this.attributes['se:connector'].value.split(' ')[i]
ethis.attributes['se:connector'].value.split(' ')[i]
);
elData(this, 'c_' + pos, part.id);
elData(this, pos + '_bb', svgCanvas.getStrokedBBox([part]));
dataStorage.put(ethis, 'c_' + pos, part.id);
dataStorage.put(ethis, pos + '_bb', svgCanvas.getStrokedBBox([ part ]));
} else part = document.getElementById(part);
parts.push(part);
}, this);
}, ethis);
for (let i = 0; i < 2; i++) {
const cElem = parts[i];
addThis = false;
// The connected element might be part of a selected group
$(cElem).parents().each(add);
const parents = svgCanvas.getParents(cElem.parentNode);
Array.prototype.forEach.call(parents, function (el) {
if (elems.includes(el)) {
// Pretend this element is selected
addThis = true;
}
});
if (!cElem || !cElem.parentNode) {
$(this).remove();
ethis.remove();
continue;
}
if (elems.includes(cElem) || addThis) {
const bb = svgCanvas.getStrokedBBox([cElem]);
const bb = svgCanvas.getStrokedBBox([ cElem ]);
connections.push({
elem: cElem,
connector: this,
connector: ethis,
is_start: (i === 0),
start_x: bb.x,
start_y: bb.y
@@ -246,13 +249,14 @@ export default {
}
}
});
}
};
/**
* @param {Element[]} [elems=selElems]
* @returns {void}
*/
function updateConnectors (elems) {
const updateConnectors = (elems) => {
const dataStorage = svgCanvas.getDataStorage();
// Updates connector lines based on selected elements
// Is not used on mousemove, as it runs getStrokedBBox every time,
// which isn't necessary there.
@@ -263,22 +267,22 @@ export default {
while (i--) {
const conn = connections[i];
const line = conn.connector;
const {elem} = conn;
const { elem } = conn;
// const sw = line.getAttribute('stroke-width') * 5;
const pre = conn.is_start ? 'start' : 'end';
// Update bbox for this element
const bb = svgCanvas.getStrokedBBox([elem]);
const bb = svgCanvas.getStrokedBBox([ elem ]);
bb.x = conn.start_x;
bb.y = conn.start_y;
elData(line, pre + '_bb', bb);
/* const addOffset = */ elData(line, pre + '_off');
dataStorage.put(line, pre + '_bb', bb);
/* const addOffset = */ dataStorage.get(line, pre + '_off');
const altPre = conn.is_start ? 'end' : 'start';
// Get center pt of connected element
const bb2 = elData(line, altPre + '_bb');
const bb2 = dataStorage.get(line, altPre + '_bb');
const srcX = bb2.x + bb2.width / 2;
const srcY = bb2.y + bb2.height / 2;
@@ -287,7 +291,7 @@ export default {
setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true);
// Set point of connected element
const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line));
const pt2 = getBBintersect(pt.x, pt.y, dataStorage.get(line, altPre + '_bb'), getOffset(altPre, line));
setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true);
// Update points attribute manually for webkit
@@ -303,14 +307,15 @@ export default {
}
}
}
}
};
// Do once
(function () {
const gse = svgCanvas.groupSelectedElements;
svgCanvas.groupSelectedElements = function (...args) {
svgCanvas.removeFromSelection($(connSel).toArray());
svgCanvas.removeFromSelection(document.querySelectorAll('.se_connector'));
return gse.apply(this, args);
};
@@ -329,74 +334,74 @@ export default {
* Do on reset.
* @returns {void}
*/
function init () {
const init = () => {
const dataStorage = svgCanvas.getDataStorage();
// Make sure all connectors have data set
$(svgcontent).find('*').each(function () {
const conn = this.getAttributeNS(seNs, 'connector');
const elements = svgcontent.querySelectorAll('*');
elements.forEach(function (curthis) {
const conn = curthis.getAttributeNS(seNs, 'connector');
if (conn) {
this.setAttribute('class', connSel.substr(1));
curthis.setAttribute('class', 'se_connector');
const connData = conn.split(' ');
const sbb = svgCanvas.getStrokedBBox([getElem(connData[0])]);
const ebb = svgCanvas.getStrokedBBox([getElem(connData[1])]);
$(this).data('c_start', connData[0])
.data('c_end', connData[1])
.data('start_bb', sbb)
.data('end_bb', ebb);
const sbb = svgCanvas.getStrokedBBox([ getElem(connData[0]) ]);
const ebb = svgCanvas.getStrokedBBox([ getElem(connData[1]) ]);
dataStorage.put(curthis, 'c_start', connData[0]);
dataStorage.put(curthis, 'c_end', connData[1]);
dataStorage.put(curthis, 'start_bb', sbb);
dataStorage.put(curthis, 'end_bb', ebb);
svgCanvas.getEditorNS(true);
}
});
}
};
const buttons = [{
id: 'mode_connect',
type: 'mode',
icon: svgEditor.curConfig.imgPath + 'cut.png',
includeWith: {
button: '#tool_line',
isDefault: false,
position: 1
},
events: {
click () {
svgCanvas.setMode('connector');
}
}
}];
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
return {
name: strings.name,
svgicons: 'conn.svg',
buttons: strings.buttons.map((button, i) => {
return Object.assign(buttons[i], button);
}),
/* async */ addLangData ({lang}) { // , importLocale: importLoc
/** @todo JFH special flag */
newUI: true,
name: svgEditor.i18next.t(`${name}:name`),
callback() {
const btitle = svgEditor.i18next.t(`${name}:langListTitle`);
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
// eslint-disable-next-line no-unsanitized/property
buttonTemplate.innerHTML = `
<se-button id="mode_connect" title="${btitle}" src="./images/conn.svg"></se-button>
`;
$id('tools_left').append(buttonTemplate.content.cloneNode(true));
$id('mode_connect').addEventListener("click", () => {
svgCanvas.setMode('connector');
});
},
/* async */ addLangData({ _lang }) { // , importLocale: importLoc
return {
data: strings.langList
data: [
{ id: 'mode_connect', title: svgEditor.i18next.t(`${name}:langListTitle`) }
]
};
},
mouseDown (opts) {
mouseDown(opts) {
const dataStorage = svgCanvas.getDataStorage();
const e = opts.event;
startX = opts.start_x;
startY = opts.start_y;
const mode = svgCanvas.getMode();
const {curConfig: {initStroke}} = svgEditor;
const { curConfig: { initStroke } } = svgEditor.configObj;
if (mode === 'connector') {
if (started) { return undefined; }
const mouseTarget = e.target;
const parents = $(mouseTarget).parents();
const parents = svgCanvas.getParents(mouseTarget.parentNode);
if ($.inArray(svgcontent, parents) !== -1) {
// Connectable element
// If child of foreignObject, use parent
const fo = $(mouseTarget).closest('foreignObject');
startElem = fo.length ? fo[0] : mouseTarget;
const fo = svgCanvas.getClosest(mouseTarget.parentNode, 'foreignObject');
startElem = fo ? fo : mouseTarget;
// Get center of source element
const bb = svgCanvas.getStrokedBBox([startElem]);
const bb = svgCanvas.getStrokedBBox([ startElem ]);
const x = bb.x + bb.width / 2;
const y = bb.y + bb.height / 2;
@@ -415,7 +420,7 @@ export default {
style: 'pointer-events:none'
}
});
elData(curLine, 'start_bb', bb);
dataStorage.put(curLine, 'start_bb', bb);
}
return {
started: true
@@ -426,7 +431,8 @@ export default {
}
return undefined;
},
mouseMove (opts) {
mouseMove(opts) {
const dataStorage = svgCanvas.getDataStorage();
const zoom = svgCanvas.getZoom();
// const e = opts.event;
const x = opts.mouse_x / zoom;
@@ -440,7 +446,7 @@ export default {
if (mode === 'connector' && started) {
// const sw = curLine.getAttribute('stroke-width') * 3;
// Set start point (adjusts based on bb)
const pt = getBBintersect(x, y, elData(curLine, 'start_bb'), getOffset('start', curLine));
const pt = getBBintersect(x, y, dataStorage.get(curLine, 'start_bb'), getOffset('start', curLine));
startX = pt.x;
startY = pt.y;
@@ -453,9 +459,9 @@ export default {
while (slen--) {
const elem = selElems[slen];
// Look for selected connector elements
if (elem && elData(elem, 'c_start')) {
if (elem && dataStorage.has(elem, 'c_start')) {
// Remove the "translate" transform given to move
svgCanvas.removeFromSelection([elem]);
svgCanvas.removeFromSelection([ elem ]);
svgCanvas.getTransformList(elem).clear();
}
}
@@ -464,7 +470,8 @@ export default {
}
}
},
mouseUp (opts) {
mouseUp(opts) {
const dataStorage = svgCanvas.getDataStorage();
// const zoom = svgCanvas.getZoom();
const e = opts.event;
// , x = opts.mouse_x / zoom,
@@ -474,10 +481,10 @@ export default {
if (svgCanvas.getMode() !== 'connector') {
return undefined;
}
const fo = $(mouseTarget).closest('foreignObject');
if (fo.length) { mouseTarget = fo[0]; }
const fo = svgCanvas.getClosest(mouseTarget.parentNode, 'foreignObject');
if (fo) { mouseTarget = fo; }
const parents = $(mouseTarget).parents();
const parents = svgCanvas.getParents(mouseTarget.parentNode);
if (mouseTarget === startElem) {
// Start line through click
@@ -488,9 +495,10 @@ export default {
started
};
}
if ($.inArray(svgcontent, parents) === -1) {
if (parents.indexOf(svgcontent) === -1) {
// Not a valid target element, so remove line
$(curLine).remove();
if (curLine)
curLine.remove();
started = false;
return {
keep: false,
@@ -501,17 +509,18 @@ export default {
// Valid end element
endElem = mouseTarget;
const startId = startElem.id, endId = endElem.id;
const startId = (startElem) ? startElem.id : '';
const endId = (endElem) ? endElem.id : '';
const connStr = startId + ' ' + endId;
const altStr = endId + ' ' + startId;
// Don't create connector if one already exists
const dupe = $(svgcontent).find(connSel).filter(function () {
const conn = this.getAttributeNS(seNs, 'connector');
const dupe = Array.prototype.filter.call(svgcontent.querySelectorAll('.se_connector'), function (aThis) {
const conn = aThis.getAttributeNS(seNs, 'connector');
if (conn === connStr || conn === altStr) { return true; }
return false;
});
if (dupe.length) {
$(curLine).remove();
curLine.remove();
return {
keep: false,
element: null,
@@ -519,19 +528,18 @@ export default {
};
}
const bb = svgCanvas.getStrokedBBox([endElem]);
const bb = svgCanvas.getStrokedBBox([ endElem ]);
const pt = getBBintersect(startX, startY, bb, getOffset('start', curLine));
setPoint(curLine, 'end', pt.x, pt.y, true);
$(curLine)
.data('c_start', startId)
.data('c_end', endId)
.data('end_bb', bb);
dataStorage.put(curLine, 'c_start', startId);
dataStorage.put(curLine, 'c_end', endId);
dataStorage.put(curLine, 'end_bb', bb);
seNs = svgCanvas.getEditorNS(true);
curLine.setAttributeNS(seNs, 'se:connector', connStr);
curLine.setAttribute('class', connSel.substr(1));
curLine.setAttribute('class', 'se_connector');
curLine.setAttribute('opacity', 1);
svgCanvas.addToSelection([curLine]);
svgCanvas.addToSelection([ curLine ]);
svgCanvas.moveToBottomSelectedElement();
selManager.requestSelector(curLine).showGrips(false);
started = false;
@@ -541,9 +549,10 @@ export default {
started
};
},
selectedChanged (opts) {
selectedChanged(opts) {
const dataStorage = svgCanvas.getDataStorage();
// TODO: Find better way to skip operations if no connectors are in use
if (!$(svgcontent).find(connSel).length) { return; }
if (!svgcontent.querySelectorAll('.se_connector').length) { return; }
if (svgCanvas.getMode() === 'connector') {
svgCanvas.setMode('select');
@@ -555,7 +564,7 @@ export default {
let i = selElems.length;
while (i--) {
const elem = selElems[i];
if (elem && elData(elem, 'c_start')) {
if (elem && dataStorage.has(elem, 'c_start')) {
selManager.requestSelector(elem).showGrips(false);
if (opts.selectedElement && !opts.multiselected) {
// TODO: Set up context tools and hide most regular line tools
@@ -569,7 +578,8 @@ export default {
}
updateConnectors();
},
elementChanged (opts) {
elementChanged(opts) {
const dataStorage = svgCanvas.getDataStorage();
let elem = opts.elems[0];
if (!elem) return;
if (elem.tagName === 'svg' && elem.id === 'svgcontent') {
@@ -588,9 +598,8 @@ export default {
const mid = elem.getAttribute('marker-mid');
const end = elem.getAttribute('marker-end');
curLine = elem;
$(elem)
.data('start_off', Boolean(start))
.data('end_off', Boolean(end));
dataStorage.put(elem, 'start_off', Boolean(start));
dataStorage.put(elem, 'end_off', Boolean(end));
if (elem.tagName === 'line' && mid) {
// Convert to polyline to accept mid-arrow
@@ -599,7 +608,7 @@ export default {
const x2 = Number(elem.getAttribute('x2'));
const y1 = Number(elem.getAttribute('y1'));
const y2 = Number(elem.getAttribute('y2'));
const {id} = elem;
const { id } = elem;
const midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' ');
const pline = addElem({
@@ -613,22 +622,23 @@ export default {
opacity: elem.getAttribute('opacity') || 1
}
});
$(elem).after(pline).remove();
elem.insertAdjacentElement('afterend', pline);
elem.remove();
svgCanvas.clearSelection();
pline.id = id;
svgCanvas.addToSelection([pline]);
svgCanvas.addToSelection([ pline ]);
elem = pline;
}
}
// Update line if it's a connector
if (elem.getAttribute('class') === connSel.substr(1)) {
const start = getElem(elData(elem, 'c_start'));
updateConnectors([start]);
if (elem.getAttribute('class') === 'se_connector') {
const start = getElem(dataStorage.get(elem, 'c_start'));
updateConnectors([ start ]);
} else {
updateConnectors();
}
},
IDsUpdated (input) {
IDsUpdated(input) {
const remove = [];
input.elems.forEach(function (elem) {
if ('se:connector' in elem.attr) {
@@ -642,16 +652,14 @@ export default {
}
}
});
return {remove};
return { remove };
},
toolButtonStateUpdate (opts) {
if (opts.nostroke) {
if ($('#mode_connect').hasClass('tool_button_current')) {
svgEditor.clickSelect();
}
toolButtonStateUpdate(opts) {
const button = document.getElementById('mode_connect');
if (opts.nostroke && button.pressed === true) {
svgEditor.clickSelect();
}
$('#mode_connect')
.toggleClass('disabled', opts.nostroke);
button.disabled = opts.nostroke;
}
};
}

View File

@@ -1,7 +1,8 @@
export default {
name: 'Connector',
langListTitle: 'Connect two objects',
langList: [
{id: 'mode_connect', title: 'Connect two objects'}
{ id: 'mode_connect', title: 'Connect two objects' }
],
buttons: [
{

View File

@@ -1,7 +1,8 @@
export default {
name: 'Connector',
langListTitle: 'Connecter deux objets',
langList: [
{id: 'mode_connect', title: 'Connecter deux objets'}
{ id: 'mode_connect', title: 'Connecter deux objets' }
],
buttons: [
{

View File

@@ -1,7 +1,8 @@
export default {
name: '连接器',
langListTitle: '连接两个对象',
langList: [
{id: 'mode_connect', title: '连接两个对象'}
{ id: 'mode_connect', title: '连接两个对象' }
],
buttons: [
{

View File

@@ -4,29 +4,35 @@
* @license MIT
*
* @copyright 2010 Jeff Schiller
* @copyright 2021 OptimistikSAS
*
*/
const loadExtensionTranslation = async function (lang) {
const name = "eyedropper";
const loadExtensionTranslation = async function (svgEditor) {
let translationModule;
const lang = svgEditor.configObj.pref('lang');
try {
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${lang}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
console.error(`Missing translation (${lang}) - using 'en'`);
console.warn(`Missing translation (${lang}) for ${name} - using 'en'`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/en.js`);
}
return translationModule.default;
svgEditor.i18next.addResourceBundle(lang, name, translationModule.default);
};
export default {
name: 'eyedropper',
async init (S) {
name,
async init(S) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const {$, ChangeElementCommand} = S, // , svgcontent,
await loadExtensionTranslation(svgEditor);
const { ChangeElementCommand } = S, // , svgcontent,
// svgdoc = S.svgroot.parentNode.ownerDocument,
svgCanvas = svgEditor.canvas,
{ svgCanvas } = svgEditor,
addToHistory = function (cmd) { svgCanvas.undoMgr.addCommandToHistory(cmd); },
currentStyle = {
fillPaint: 'red', fillOpacity: 1.0,
@@ -36,25 +42,26 @@ export default {
strokeLinecap: 'butt',
strokeLinejoin: 'miter'
};
const { $id } = svgCanvas;
/**
*
* @param {module:svgcanvas.SvgCanvas#event:ext_selectedChanged|module:svgcanvas.SvgCanvas#event:ext_elementChanged} opts
* @returns {void}
*/
function getStyle (opts) {
const getStyle = (opts) => {
// if we are in eyedropper mode, we don't want to disable the eye-dropper tool
const mode = svgCanvas.getMode();
if (mode === 'eyedropper') { return; }
const tool = $('#tool_eyedropper');
const tool = $id('tool_eyedropper');
// enable-eye-dropper if one element is selected
let elem = null;
if (!opts.multiselected && opts.elems[0] &&
!['svg', 'g', 'use'].includes(opts.elems[0].nodeName)
![ 'svg', 'g', 'use' ].includes(opts.elems[0].nodeName)
) {
elem = opts.elems[0];
tool.removeClass('disabled');
tool.classList.remove('disabled');
// grab the current style
currentStyle.fillPaint = elem.getAttribute('fill') || 'black';
currentStyle.fillOpacity = elem.getAttribute('fill-opacity') || 1.0;
@@ -65,42 +72,37 @@ export default {
currentStyle.strokeLinecap = elem.getAttribute('stroke-linecap');
currentStyle.strokeLinejoin = elem.getAttribute('stroke-linejoin');
currentStyle.opacity = elem.getAttribute('opacity') || 1.0;
// disable eye-dropper tool
// disable eye-dropper tool
} else {
tool.addClass('disabled');
tool.classList.add('disabled');
}
}
const buttons = [
{
id: 'tool_eyedropper',
icon: 'eyedropper.png',
type: 'mode',
events: {
click () {
svgCanvas.setMode('eyedropper');
}
}
}
];
};
return {
name: strings.name,
svgicons: 'eyedropper-icon.xml',
buttons: strings.buttons.map((button, i) => {
return Object.assign(buttons[i], button);
}),
name: svgEditor.i18next.t(`${name}:name`),
callback() {
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
const title = svgEditor.i18next.t(`${name}:buttons.0.title`);
const key = svgEditor.i18next.t(`${name}:buttons.0.key`);
// eslint-disable-next-line no-unsanitized/property
buttonTemplate.innerHTML = `
<se-button id="tool_eyedropper" title="${title}" src="./images/eye_dropper.svg" shortcut=${key}></se-button>
`;
$id('tools_left').append(buttonTemplate.content.cloneNode(true));
$id('tool_eyedropper').addEventListener("click", () => {
svgCanvas.setMode('eyedropper');
});
},
// if we have selected an element, grab its paint and enable the eye dropper button
selectedChanged: getStyle,
elementChanged: getStyle,
mouseDown (opts) {
mouseDown(opts) {
const mode = svgCanvas.getMode();
if (mode === 'eyedropper') {
const e = opts.event;
const {target} = e;
if (!['svg', 'g', 'use'].includes(target.nodeName)) {
const { target } = e;
if (![ 'svg', 'g', 'use' ].includes(target.nodeName)) {
const changes = {};
const change = function (elem, attrname, newvalue) {

View File

@@ -0,0 +1,9 @@
export default {
name: 'pipette',
buttons: [
{
title: 'Outil pipette',
key: 'I'
}
]
};

View File

@@ -1,3 +1,4 @@
/* globals seConfirm */
/**
* @file ext-foreignobject.js
*
@@ -6,10 +7,10 @@
* @copyright 2010 Jacques Distler, 2010 Alexis Deveria
*
*/
const loadExtensionTranslation = async function (lang) {
let translationModule;
try {
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
@@ -23,19 +24,20 @@ export default {
name: 'foreignobject',
async init (S) {
const svgEditor = this;
const {$, text2xml, NS} = S;
const svgCanvas = svgEditor.canvas;
const { text2xml, NS } = S;
const { svgCanvas } = svgEditor;
const { $id } = svgCanvas;
const
// {svgcontent} = S,
// addElem = svgCanvas.addSVGElementFromJson,
svgdoc = S.svgroot.parentNode.ownerDocument;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const properlySourceSizeTextArea = function () {
// TODO: remove magic numbers here and get values from CSS
const height = $('#svg_source_container').height() - 80;
$('#svg_source_textarea').css('height', height);
const height = parseFloat(getComputedStyle($id('svg_source_container'), null).height.replace("px", "")) - 80;
$id('svg_source_textarea').style.height = height + "px";
};
/**
@@ -43,12 +45,14 @@ export default {
* @returns {void}
*/
function showPanel (on) {
let fcRules = $('#fc_rules');
if (!fcRules.length) {
fcRules = $('<style id="fc_rules"></style>').appendTo('head');
let fcRules = $id('fc_rules');
if (!fcRules) {
fcRules = document.createElement('style');
fcRules.setAttribute('id', 'fc_rules');
document.getElementsByTagName("head")[0].appendChild(fcRules);
}
fcRules.text(!on ? '' : ' #tool_topath { display: none !important; }');
$('#foreignObject_panel').toggle(on);
fcRules.textContent = !on ? '' : ' #tool_topath { display: none !important; }';
$id('foreignObject_panel').style.display = (on) ? 'block' : 'none';
}
/**
@@ -56,8 +60,10 @@ export default {
* @returns {void}
*/
function toggleSourceButtons (on) {
$('#tool_source_save, #tool_source_cancel').toggle(!on);
$('#foreign_save, #foreign_cancel').toggle(on);
$id('tool_source_save').style.display = (!on) ? 'block' : 'none';
$id('tool_source_cancel').style.display = (!on) ? 'block' : 'none';
$id('foreign_save').style.display = (on) ? 'block' : 'none';
$id('foreign_cancel').style.display = (on) ? 'block' : 'none';
}
let selElems,
@@ -74,15 +80,16 @@ export default {
const elt = selElems[0]; // The parent `Element` to append to
try {
// convert string into XML document
const newDoc = text2xml('<svg xmlns="' + NS.SVG + '" xmlns:xlink="' + NS.XLINK + '">' + xmlString + '</svg>');
const oi = (xmlString.indexOf('xmlns:oi') !== -1) ? ' xmlns:oi="' + NS.OI + '"' : '';
const newDoc = text2xml('<svg xmlns="' + NS.SVG + '" xmlns:xlink="' + NS.XLINK + '" '+ oi +'>' + xmlString + '</svg>');
// run it through our sanitizer to remove anything we do not support
svgCanvas.sanitizeSvg(newDoc.documentElement);
elt.replaceWith(svgdoc.importNode(newDoc.documentElement.firstChild, true));
svgCanvas.call('changed', [elt]);
svgCanvas.call('changed', [ elt ]);
svgCanvas.clearSelection();
} catch (e) {
// Todo: Surface error to user
console.log(e); // eslint-disable-line no-console
console.error(e);
return false;
}
@@ -101,10 +108,10 @@ export default {
elt.removeAttribute('fill');
const str = svgCanvas.svgToString(elt, 0);
$('#svg_source_textarea').val(str);
$('#svg_source_editor').fadeIn();
$id('svg_source_textarea').value = str;
$id('#svg_source_editor').style.display = 'block';
properlySourceSizeTextArea();
$('#svg_source_textarea').focus();
$id('svg_source_textarea').focus();
}
/**
@@ -117,7 +124,7 @@ export default {
svgCanvas.call('changed', selElems);
}
const buttons = [{
const buttons = [ {
id: 'tool_foreign',
icon: 'foreignobject-tool.png',
type: 'mode',
@@ -136,7 +143,7 @@ export default {
showForeignEditor();
}
}
}];
} ];
const contextTools = [
{
@@ -182,38 +189,50 @@ export default {
return Object.assign(contextTools[i], contextTool);
}),
callback () {
$('#foreignObject_panel').hide();
$id("foreignObject_panel").style.display = 'none';
const endChanges = function () {
$('#svg_source_editor').hide();
$id("svg_source_editor").style.display = 'none';
editingforeign = false;
$('#svg_source_textarea').blur();
$id('svg_source_textarea').blur();
toggleSourceButtons(false);
};
// TODO: Needs to be done after orig icon loads
setTimeout(function () {
// Create source save/cancel buttons
/* const save = */ $('#tool_source_save').clone()
.hide().attr('id', 'foreign_save').unbind()
.appendTo('#tool_source_back').click(async function () {
if (!editingforeign) { return; }
const toolSourceSave = $id('tool_source_save').cloneNode(true);
toolSourceSave.style.display = 'none';
toolSourceSave.id = 'foreign_save';
// unbind()
// const oldElement = $id('tool_source_save');
// oldElement.parentNode.replaceChild(toolSourceSave, oldElement);
$id('tool_source_back').append(toolSourceSave);
toolSourceSave.addEventListener('click', () => function () {
if (!editingforeign) { return; }
if (!setForeignString($('#svg_source_textarea').val())) {
const ok = await $.confirm('Errors found. Revert to original?');
if (!ok) { return; }
endChanges();
} else {
endChanges();
}
// setSelectMode();
});
/* const cancel = */ $('#tool_source_cancel').clone()
.hide().attr('id', 'foreign_cancel').unbind()
.appendTo('#tool_source_back').click(function () {
if (!setForeignString($id('svg_source_textarea').value)) {
const ok = seConfirm('Errors found. Revert to original?');
if (!ok) { return; }
endChanges();
});
} else {
endChanges();
}
// setSelectMode();
});
var oldToolSourceCancel = $id('tool_source_cancel');
const toolSourceCancel = oldToolSourceCancel.cloneNode(true);
toolSourceCancel.style.display = 'none';
toolSourceCancel.id = 'foreign_cancel';
$id('tool_source_back').append(toolSourceCancel);
toolSourceCancel.addEventListener('click', () => function () {
endChanges();
});
// unbind()
// var oldToolSourceCancel = $id('tool_source_cancel');
// oldToolSourceCancel.parentNode.replaceChild(toolSourceCancel, oldToolSourceCancel);
}, 3000);
},
mouseDown (opts) {
@@ -250,14 +269,17 @@ export default {
started: true
};
},
mouseUp (opts) {
mouseUp (_opts) {
// const e = opts.event;
if (svgCanvas.getMode() !== 'foreign' || !started) {
return undefined;
}
const attrs = $(newFO).attr(['width', 'height']);
const attrs = {
width: newFO.getAttribute('width'),
height: newFO.getAttribute('height'),
};
const keep = (attrs.width !== '0' || attrs.height !== '0');
svgCanvas.addToSelection([newFO], true);
svgCanvas.addToSelection([ newFO ], true);
return {
keep,
@@ -273,9 +295,9 @@ export default {
const elem = selElems[i];
if (elem && elem.tagName === 'foreignObject') {
if (opts.selectedElement && !opts.multiselected) {
$('#foreign_font_size').val(elem.getAttribute('font-size'));
$('#foreign_width').val(elem.getAttribute('width'));
$('#foreign_height').val(elem.getAttribute('height'));
$id('foreign_font_size').value = elem.getAttribute('font-size');
$id('foreign_width').value = elem.getAttribute('width');
$id('foreign_height').value = elem.getAttribute('height');
showPanel(true);
} else {
showPanel(false);
@@ -285,7 +307,7 @@ export default {
}
}
},
elementChanged (opts) {
elementChanged (_opts) {
// const elem = opts.elems[0];
}
};

View File

@@ -7,33 +7,40 @@
*
*/
const loadExtensionTranslation = async function (lang) {
const name = "grid";
const loadExtensionTranslation = async function (svgEditor) {
let translationModule;
const lang = svgEditor.configObj.pref('lang');
try {
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${lang}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
console.error(`Missing translation (${lang}) - using 'en'`);
console.warn(`Missing translation (${lang}) for ${name} - using 'en'`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/en.js`);
}
return translationModule.default;
svgEditor.i18next.addResourceBundle(lang, name, translationModule.default);
};
export default {
name: 'grid',
async init ({$, NS, getTypeMap}) {
name,
async init ({ NS, getTypeMap }) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const svgCanvas = svgEditor.canvas;
const svgdoc = document.getElementById('svgcanvas').ownerDocument,
{assignAttributes} = svgCanvas,
hcanvas = document.createElement('canvas'),
canvBG = $('#canvasBackground'),
units = getTypeMap(), // Assumes prior `init()` call on `units.js` module
intervals = [0.01, 0.1, 1, 10, 100, 1000];
let showGrid = svgEditor.curConfig.showGrid || false;
await loadExtensionTranslation(svgEditor);
const { svgCanvas } = svgEditor;
const { $id } = svgCanvas;
const svgdoc = document.getElementById('svgcanvas').ownerDocument;
const { assignAttributes } = svgCanvas;
const hcanvas = document.createElement('canvas');
const canvBG = $id('canvasBackground');
const units = getTypeMap(); // Assumes prior `init()` call on `units.js` module
const intervals = [ 0.01, 0.1, 1, 10, 100, 1000 ];
let showGrid = svgEditor.configObj.curConfig.showGrid || false;
$(hcanvas).hide().appendTo('body');
hcanvas.style.display = 'none';
document.body.appendChild(hcanvas);
const canvasGrid = svgdoc.createElementNS(NS.SVG, 'svg');
assignAttributes(canvasGrid, {
@@ -45,7 +52,7 @@ export default {
overflow: 'visible',
display: 'none'
});
canvBG.append(canvasGrid);
canvBG.appendChild(canvasGrid);
const gridDefs = svgdoc.createElementNS(NS.SVG, 'defs');
// grid-pattern
const gridPattern = svgdoc.createElementNS(NS.SVG, 'pattern');
@@ -67,7 +74,7 @@ export default {
});
gridPattern.append(gridimg);
gridDefs.append(gridPattern);
$('#canvasGrid').append(gridDefs);
$id('canvasGrid').appendChild(gridDefs);
// grid-box
const gridBox = svgdoc.createElementNS(NS.SVG, 'rect');
@@ -81,16 +88,16 @@ export default {
fill: 'url(#gridpattern)',
style: 'pointer-events: none; display:visible;'
});
$('#canvasGrid').append(gridBox);
$id('canvasGrid').appendChild(gridBox);
/**
*
* @param {Float} zoom
* @returns {void}
*/
function updateGrid (zoom) {
const updateGrid = (zoom) => {
// TODO: Try this with <line> elements, then compare performance difference
const unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px
const unit = units[svgEditor.configObj.curConfig.baseUnit]; // 1 = 1px
const uMulti = unit * zoom;
// Calculate the main number interval
const rawM = 100 / uMulti;
@@ -109,7 +116,7 @@ export default {
const part = bigInt / 10;
ctx.globalAlpha = 0.2;
ctx.strokeStyle = svgEditor.curConfig.gridColor;
ctx.strokeStyle = svgEditor.configObj.curConfig.gridColor;
for (let i = 1; i < 10; i++) {
const subD = Math.round(part * i) + 0.5;
// const lineNum = (i % 2)?12:10;
@@ -135,46 +142,43 @@ export default {
gridimg.parentNode.setAttribute('width', bigInt);
gridimg.parentNode.setAttribute('height', bigInt);
svgCanvas.setHref(gridimg, datauri);
}
};
/**
*
* @returns {void}
*/
function gridUpdate () {
const gridUpdate = () => {
if (showGrid) {
updateGrid(svgCanvas.getZoom());
}
$('#canvasGrid').toggle(showGrid);
$('#view_grid').toggleClass('push_button_pressed tool_button');
}
const buttons = [{
id: 'view_grid',
icon: 'grid.png',
type: 'context',
panel: 'editor_panel',
events: {
click () {
svgEditor.curConfig.showGrid = showGrid = !showGrid;
gridUpdate();
}
}
}];
$id('canvasGrid').style.display = (showGrid) ? 'block' : 'none';
document.getElementById('view_grid').pressed = showGrid;
};
return {
name: strings.name,
svgicons: 'grid-icon.xml',
name: svgEditor.i18next.t(`${name}:name`),
zoomChanged (zoom) {
if (showGrid) { updateGrid(zoom); }
},
callback () {
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
const title = svgEditor.i18next.t(`${name}:buttons.0.title`);
// eslint-disable-next-line no-unsanitized/property
buttonTemplate.innerHTML = `
<se-button id="view_grid" title="${title}" src="./images/grid.svg"></se-button>
`;
$id('editor_panel').append(buttonTemplate.content.cloneNode(true));
$id('view_grid').addEventListener("click", () => {
svgEditor.configObj.curConfig.showGrid = showGrid = !showGrid;
gridUpdate();
});
if (showGrid) {
gridUpdate();
}
},
buttons: strings.buttons.map((button, i) => {
return Object.assign(buttons[i], button);
})
}
};
}
};

View File

@@ -0,0 +1,8 @@
export default {
name: 'Grille',
buttons: [
{
title: 'Afficher/Cacher Grille'
}
]
};

View File

@@ -13,55 +13,45 @@
* will show the user the point on the canvas that was clicked on.
*/
const loadExtensionTranslation = async function (lang) {
const name = "helloworld";
const loadExtensionTranslation = async function (svgEditor) {
let translationModule;
const lang = svgEditor.configObj.pref('lang');
try {
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${lang}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
console.error(`Missing translation (${lang}) - using 'en'`);
console.warn(`Missing translation (${lang}) for ${name} - using 'en'`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/en.js`);
}
return translationModule.default;
svgEditor.i18next.addResourceBundle(lang, name, translationModule.default);
};
export default {
name: 'helloworld',
async init ({$, importLocale}) {
name,
async init ({ _importLocale }) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const svgCanvas = svgEditor.canvas;
await loadExtensionTranslation(svgEditor);
const { svgCanvas } = svgEditor;
const { $id } = svgCanvas;
return {
name: strings.name,
// For more notes on how to make an icon file, see the source of
// the helloworld-icon.xml
svgicons: 'helloworld-icon.xml',
// Multiple buttons can be added in this array
buttons: [{
// Must match the icon ID in helloworld-icon.xml
id: 'hello_world',
// Fallback, e.g., for `file:///` access
icon: 'helloworld.png',
// This indicates that the button will be added to the "mode"
// button panel on the left side
type: 'mode',
// Tooltip text
title: strings.buttons[0].title,
// Events
events: {
click () {
// The action taken when the button is clicked on.
// For "mode" buttons, any other button will
// automatically be de-pressed.
svgCanvas.setMode('hello_world');
}
}
}],
name: svgEditor.i18next.t(`${name}:name`),
callback() {
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
const title = svgEditor.i18next.t(`${name}:buttons.0.title`);
// eslint-disable-next-line no-unsanitized/property
buttonTemplate.innerHTML = `
<se-button id="hello_world" title="${title}" src="./images/hello_world.svg"></se-button>
`;
$id('tools_left').append(buttonTemplate.content.cloneNode(true));
$id('hello_world').addEventListener("click", () => {
svgCanvas.setMode('hello_world');
});
},
// This is triggered when the main mouse button is pressed down
// on the editor canvas (not the tool panels)
mouseDown () {
@@ -69,7 +59,7 @@ export default {
if (svgCanvas.getMode() === 'hello_world') {
// The returned object must include "started" with
// a value of true in order for mouseUp to be triggered
return {started: true};
return { started: true };
}
return undefined;
},
@@ -86,16 +76,9 @@ export default {
const y = opts.mouse_y / zoom;
// We do our own formatting
let {text} = strings;
[
['x', x],
['y', y]
].forEach(([prop, val]) => {
text = text.replace('{' + prop + '}', val);
});
let text = svgEditor.i18next.t(`${name}:text`, { x, y });
// Show the text using the custom alert function
$.alert(text);
alert(text);
}
}
};

View File

@@ -1,6 +1,6 @@
export default {
name: 'Hello World',
text: 'Hello World!\n\nYou clicked here: {x}, {y}',
text: 'Hello World!\n\nYou clicked here: {{x}}, {{y}}',
buttons: [
{
title: "Say 'Hello World'"

View File

@@ -0,0 +1,9 @@
export default {
name: 'Bonjour le Monde',
text: 'Bonjour le Monde!\n\nVous avez cliqué ici: {{x}}, {{y}}',
buttons: [
{
title: "Dire 'Bonjour le Monde'"
}
]
};

View File

@@ -1,6 +1,6 @@
export default {
name: 'Hello World',
text: 'Hello World!\n\n 请点击: {x}, {y}',
text: 'Hello World!\n\n 请点击: {{x}}, {{y}}',
buttons: [
{
title: "输出 'Hello World'"

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-unsanitized/property */
/* globals seConfirm */
/**
* @file ext-imagelib.js
*
@@ -7,27 +9,46 @@
*
*/
const loadExtensionTranslation = async function (lang) {
let translationModule;
try {
translationModule = await import(`./locale/${encodeURIComponent(lang)}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
console.error(`Missing translation (${lang}) - using 'en'`);
translationModule = await import(`./locale/en.js`);
}
return translationModule.default;
};
const name = "imagelib";
const loadExtensionTranslation = async function (svgEditor) {
let translationModule;
const lang = svgEditor.configObj.pref('lang');
try {
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/${lang}.js`);
} catch (_error) {
// eslint-disable-next-line no-console
console.warn(`Missing translation (${lang}) for ${name} - using 'en'`);
// eslint-disable-next-line no-unsanitized/method
translationModule = await import(`./locale/en.js`);
}
svgEditor.i18next.addResourceBundle(lang, name, translationModule.default);
};
export default {
name: 'imagelib',
async init ({$, decode64, dropXMLInternalSubset}) {
name,
async init({ decode64, dropXMLInternalSubset }) {
const svgEditor = this;
const imagelibStrings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const { $id } = svgEditor.svgCanvas;
await loadExtensionTranslation(svgEditor);
const {uiStrings, canvas: svgCanvas} = svgEditor;
const { svgCanvas } = svgEditor;
const allowedImageLibOrigins = imagelibStrings.imgLibs.map(({url}) => {
const imgLibs = [
{
name: svgEditor.i18next.t(`${name}:imgLibs_0_name`),
url: 'extensions/ext-imagelib/index.html',
description: svgEditor.i18next.t(`${name}:imgLibs_0_description`)
},
{
name: svgEditor.i18next.t(`${name}:imgLibs_1_name`),
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: svgEditor.i18next.t(`${name}:imgLibs_1_description`)
}
];
const allowedImageLibOrigins = imgLibs.map(({ url }) => {
try {
return new URL(url).origin;
} catch (err) {
@@ -39,16 +60,16 @@ export default {
*
* @returns {void}
*/
function closeBrowser () {
$('#imgbrowse_holder').hide();
const closeBrowser = () => {
$id("imgbrowse_holder").style.display = 'none';
document.activeElement.blur(); // make sure focus is the body to correct issue #417
}
};
/**
* @param {string} url
* @returns {void}
*/
function importImage (url) {
const importImage = (url) => {
const newImage = svgCanvas.addSVGElementFromJson({
element: 'image',
attr: {
@@ -61,9 +82,9 @@ export default {
}
});
svgCanvas.clearSelection();
svgCanvas.addToSelection([newImage]);
svgCanvas.addToSelection([ newImage ]);
svgCanvas.setImageURL(url);
}
};
const pending = {};
@@ -108,8 +129,9 @@ export default {
* @param {ImageLibMetaMessage|ImageLibMessage|string} cfg.data String is deprecated when parsed to JSON `ImageLibMessage`
* @returns {void}
*/
async function onMessage ({origin, data: response}) { // eslint-disable-line no-shadow
if (!response || !['string', 'object'].includes(typeof response)) {
async function onMessage({ origin, data }) {
let response = data;
if (!response || ![ 'string', 'object' ].includes(typeof response)) {
// Do nothing
return;
}
@@ -125,7 +147,7 @@ export default {
}
if (!allowedImageLibOrigins.includes('*') && !allowedImageLibOrigins.includes(origin)) {
// Todo: Surface this error to user?
console.log(`Origin ${origin} not whitelisted for posting to ${window.origin}`); // eslint-disable-line no-console
console.error(`Origin ${origin} not whitelisted for posting to ${window.origin}`);
return;
}
const hasName = 'name' in response;
@@ -142,7 +164,9 @@ export default {
}
// Hide possible transfer dialog box
$('#dialog_box').hide();
if (document.querySelector('se-elix-alert-dialog')) {
document.querySelector('se-elix-alert-dialog').remove();
}
type = hasName
? 'meta'
: response.charAt(0);
@@ -168,381 +192,418 @@ export default {
let entry, curMeta, svgStr, imgStr;
switch (type) {
case 'meta': {
// Metadata
transferStopped = false;
curMeta = response;
case 'meta': {
// Metadata
transferStopped = false;
curMeta = response;
// Should be safe to add dynamic property as passed metadata
pending[curMeta.id] = curMeta; // lgtm [js/remote-property-injection]
// Should be safe to add dynamic property as passed metadata
pending[curMeta.id] = curMeta; // lgtm [js/remote-property-injection]
const name = (curMeta.name || 'file');
const name = (curMeta.name || 'file');
const message = uiStrings.notification.retrieving.replace('%s', name);
const message = svgEditor.i18next.t('notification.retrieving').replace('%s', name);
if (mode !== 'm') {
await $.process_cancel(message);
transferStopped = true;
// Should a message be sent back to the frame?
if (mode !== 'm') {
await seConfirm(message);
transferStopped = true;
} else {
entry = document.createElement('div');
entry.textContent = message;
entry.dataset.id = curMeta.id;
preview.appendChild(entry);
curMeta.entry = entry;
}
$('#dialog_box').hide();
} else {
entry = $('<div>').text(message).data('id', curMeta.id);
preview.append(entry);
curMeta.entry = entry;
return;
}
return;
}
case '<':
svgStr = true;
break;
case 'd': {
if (response.startsWith('data:image/svg+xml')) {
const pre = 'data:image/svg+xml;base64,';
const src = response.substring(pre.length);
response = decode64(src);
case '<':
svgStr = true;
break;
} else if (response.startsWith('data:image/')) {
imgStr = true;
break;
case 'd': {
if (response.startsWith('data:image/svg+xml')) {
const pre = 'data:image/svg+xml;base64,';
const src = response.substring(pre.length);
response = decode64(src);
svgStr = true;
break;
} else if (response.startsWith('data:image/')) {
imgStr = true;
break;
}
}
}
// Else fall through
default:
// TODO: See if there's a way to base64 encode the binary data stream
// const str = 'data:;base64,' + svgedit.utilities.encode64(response, true);
// Else fall through
default:
// TODO: See if there's a way to base64 encode the binary data stream
// const str = 'data:;base64,' + svgedit.utilities.encode64(response, true);
// Assume it's raw image data
// importImage(str);
// Assume it's raw image data
// importImage(str);
// Don't give warning as postMessage may have been used by something else
if (mode !== 'm') {
closeBrowser();
} else {
pending[id].entry.remove();
}
// await $.alert('Unexpected data was returned: ' + response, function() {
// if (mode !== 'm') {
// closeBrowser();
// } else {
// pending[id].entry.remove();
// }
// });
return;
// Don't give warning as postMessage may have been used by something else
if (mode !== 'm') {
closeBrowser();
} else {
pending[id].entry.remove();
}
// await alert('Unexpected data was returned: ' + response, function() {
// if (mode !== 'm') {
// closeBrowser();
// } else {
// pending[id].entry.remove();
// }
// });
return;
}
switch (mode) {
case 's':
// Import one
if (svgStr) {
svgCanvas.importSvgString(response);
} else if (imgStr) {
importImage(response);
}
closeBrowser();
break;
case 'm': {
// Import multiple
multiArr.push([(svgStr ? 'svg' : 'img'), response]);
curMeta = pending[id];
let title;
if (svgStr) {
if (curMeta && curMeta.name) {
title = curMeta.name;
} else {
// Try to find a title
// `dropXMLInternalSubset` is to help prevent the billion laughs attack
const xml = new DOMParser().parseFromString(dropXMLInternalSubset(response), 'text/xml').documentElement; // lgtm [js/xml-bomb]
title = $(xml).children('title').first().text() || '(SVG #' + response.length + ')';
case 's':
// Import one
if (svgStr) {
svgEditor.svgCanvas.importSvgString(response);
} else if (imgStr) {
importImage(response);
}
if (curMeta) {
preview.children().each(function () {
if ($(this).data('id') === id) {
if (curMeta.preview_url) {
$(this).html(
$('<span>').append(
$('<img>').attr('src', curMeta.preview_url),
title
)
);
} else {
$(this).text(title);
closeBrowser();
break;
case 'm': {
// Import multiple
multiArr.push([ (svgStr ? 'svg' : 'img'), response ]);
curMeta = pending[id];
let title;
if (svgStr) {
if (curMeta && curMeta.name) {
title = curMeta.name;
} else {
// Try to find a title
// `dropXMLInternalSubset` is to help prevent the billion laughs attack
const xml = new DOMParser().parseFromString(dropXMLInternalSubset(response), 'text/xml').documentElement; // lgtm [js/xml-bomb]
title = xml.querySelector('title').textContent || '(SVG #' + response.length + ')';
}
if (curMeta) {
Array.from(preview.children).forEach(function (element) {
if (element.dataset.id === id) {
if (curMeta.preview_url) {
const img = document.createElement("img");
img.src = curMeta.preview_url;
const span = document.createElement("span");
span.appendChild(img);
element.append(span);
} else {
element.textContent = title;
}
submit.removeAttribute('disabled');
}
submit.removeAttr('disabled');
}
});
});
} else {
const div = document.createElement("div");
div.textContent = title;
preview.appendChild(div);
submit.removeAttribute('disabled');
}
} else {
preview.append(
$('<div>').text(title)
);
submit.removeAttr('disabled');
}
} else {
if (curMeta && curMeta.preview_url) {
title = curMeta.name || '';
entry = $('<span>').append(
$('<img>').attr('src', curMeta.preview_url),
title
);
} else {
entry = $('<img>').attr('src', response);
}
if (curMeta && curMeta.preview_url) {
title = curMeta.name || '';
entry = document.createElement('span');
const img = document.createElement("img");
img.src = curMeta.preview_url;
entry.appendChild(img);
entry.appendChild(document.createTextNode(title));
} else {
entry = document.createElement("img");
entry.src = response;
}
if (curMeta) {
preview.children().each(function () {
if ($(this).data('id') === id) {
$(this).html(entry);
submit.removeAttr('disabled');
}
});
} else {
preview.append($('<div>').append(entry));
submit.removeAttr('disabled');
if (curMeta) {
Array.from(preview.children).forEach(function (element) {
if (element.dataset.id === id) {
element.appendChild(entry);
submit.removeAttribute('disabled');
}
});
} else {
const div = document.createElement("div");
div.appendChild(entry);
preview.appendChild(div);
submit.removeAttribute('disabled');
}
}
break;
} case 'o': {
// Open
if (!svgStr) { break; }
closeBrowser();
const ok = await svgEditor.openPrep();
if (!ok) { return; }
svgCanvas.clear();
svgCanvas.setSvgString(response);
// updateCanvas();
break;
}
break;
} case 'o': {
// Open
if (!svgStr) { break; }
closeBrowser();
const ok = await svgEditor.openPrep();
if (!ok) { return; }
svgCanvas.clear();
svgCanvas.setSvgString(response);
// updateCanvas();
break;
}
}
}
// Receive `postMessage` data
window.addEventListener('message', onMessage, true);
const insertAfter = (referenceNode, newNode) => {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
};
const toggleMultiLoop = () => {
multiArr.forEach(function(item, i){
const type = item[0];
const data = item[1];
if (type === 'svg') {
svgCanvas.importSvgString(data);
} else {
importImage(data);
}
svgCanvas.moveSelectedElements(i * 20, i * 20, false);
});
while (preview.firstChild)
preview.removeChild(preview.firstChild);
multiArr = [];
$id("imgbrowse_holder").style.display = 'none';
};
/**
* @param {boolean} show
* @returns {void}
*/
function toggleMulti (show) {
$('#lib_framewrap, #imglib_opts').css({right: (show ? 200 : 10)});
const toggleMulti = (show) => {
$id('lib_framewrap').style.right = (show ? 200 : 10);
$id('imglib_opts').style.right = (show ? 200 : 10);
if (!preview) {
preview = $('<div id=imglib_preview>').css({
position: 'absolute',
top: 45,
right: 10,
width: 180,
bottom: 45,
background: '#fff',
overflow: 'auto'
}).insertAfter('#lib_framewrap');
preview = document.createElement('div');
preview.setAttribute('id', 'imglib_preview');
// eslint-disable-next-line max-len
preview.setAttribute('style', `position: absolute;top: 45px;right: 10px;width: 180px;bottom: 45px;background: #fff;overflow: auto;`);
insertAfter($id('lib_framewrap'), preview);
submit = $('<button disabled>Import selected</button>')
.appendTo('#imgbrowse')
.on('click touchend', function () {
$.each(multiArr, function (i) {
const type = this[0];
const data = this[1];
if (type === 'svg') {
svgCanvas.importSvgString(data);
} else {
importImage(data);
}
svgCanvas.moveSelectedElements(i * 20, i * 20, false);
});
preview.empty();
multiArr = [];
$('#imgbrowse_holder').hide();
}).css({
position: 'absolute',
bottom: 10,
right: -10
});
submit = document.createElement('button');
submit.setAttribute('content', 'Import selected');
submit.setAttribute('disabled', true);
submit.textContent = 'Import selected';
submit.setAttribute('style', `position: absolute;bottom: 10px;right: -10px;`);
$id('imgbrowse').appendChild(submit);
submit.addEventListener('click', toggleMultiLoop);
submit.addEventListener('touchend', toggleMultiLoop);
}
submit.style.display = (show) ? 'block' : 'none';
preview.style.display = (show) ? 'block' : 'none';
preview.toggle(show);
submit.toggle(show);
}
};
/**
*
* @returns {void}
*/
function showBrowser () {
let browser = $('#imgbrowse');
if (!browser.length) {
$('<div id=imgbrowse_holder><div id=imgbrowse class=toolbar_button>' +
'</div></div>').insertAfter('#svg_docprops');
browser = $('#imgbrowse');
const showBrowser = () => {
let browser = $id('imgbrowse');
if (!browser) {
const div = document.createElement('div');
div.id = 'imgbrowse_holder';
div.innerHTML = '<div id=imgbrowse class=toolbar_button></div>';
insertAfter($id('svg_editor'), div);
browser = $id('imgbrowse');
const allLibs = imagelibStrings.select_lib;
const allLibs = svgEditor.i18next.t(`${name}:select_lib`);
const libOpts = $('<ul id=imglib_opts>').appendTo(browser);
const frame = $('<iframe src="javascript:0"/>').prependTo(browser).hide().wrap('<div id=lib_framewrap>');
const divFrameWrap = document.createElement('div');
divFrameWrap.id = 'lib_framewrap';
const header = $('<h1>').prependTo(browser).text(allLibs).css({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
const libOpts = document.createElement('ul');
libOpts.id = 'imglib_opts';
browser.append(libOpts);
const frame = document.createElement('iframe');
frame.src = "javascript:0";
frame.style.display = 'none';
divFrameWrap.append(frame);
browser.prepend(divFrameWrap);
const header = document.createElement('h1');
browser.prepend(header);
header.textContent = allLibs;
header.setAttribute('style', `position: absolute;top: 0;left: 0;width: 100%;`);
const button = document.createElement('button');
// eslint-disable-next-line max-len
button.innerHTML = '<img class="svg_icon" src="./images/cancel.svg" alt="icon" width="16" height="16" />' + svgEditor.i18next.t('common.cancel');
browser.appendChild(button);
button.addEventListener('click', function () {
$id("imgbrowse_holder").style.display = 'none';
});
button.addEventListener('touchend', function () {
$id("imgbrowse_holder").style.display = 'none';
});
button.setAttribute('style', `position: absolute;top: 5;right: -10;`);
const cancel = $('<button>' + uiStrings.common.cancel + '</button>')
.appendTo(browser)
.on('click touchend', function () {
$('#imgbrowse_holder').hide();
}).css({
position: 'absolute',
top: 5,
right: -10
});
const leftBlock = document.createElement('span');
leftBlock.setAttribute('style', `position: absolute;top: 5;left: 10;`);
browser.appendChild(leftBlock);
const leftBlock = $('<span>').css({position: 'absolute', top: 5, left: 10}).appendTo(browser);
const back = document.createElement('button');
back.style.visibility = "hidden";
// eslint-disable-next-line max-len
back.innerHTML = '<img class="svg_icon" src="./images/library.svg" alt="icon" width="16" height="16" />' + svgEditor.i18next.t(`${name}:show_list`);
leftBlock.appendChild(back);
back.addEventListener('click', function () {
frame.setAttribute('src', 'about:blank');
frame.style.display = 'none';
libOpts.style.display = 'block';
header.textContent = allLibs;
back.style.display = 'none';
});
// eslint-disable-next-line sonarjs/no-identical-functions
back.addEventListener('touchend', function () {
frame.setAttribute('src', 'about:blank');
frame.style.display = 'none';
libOpts.style.display = 'block';
header.textContent = allLibs;
back.style.display = 'none';
});
back.setAttribute('style', `margin-right: 5px;`);
back.style.display = 'none';
const back = $('<button hidden>' + imagelibStrings.show_list + '</button>')
.appendTo(leftBlock)
.on('click touchend', function () {
frame.attr('src', 'about:blank').hide();
libOpts.show();
header.text(allLibs);
back.hide();
}).css({
'margin-right': 5
}).hide();
/* const type = */ $('<select><option value=s>' +
imagelibStrings.import_single + '</option><option value=m>' +
imagelibStrings.import_multi + '</option><option value=o>' +
imagelibStrings.open + '</option></select>').appendTo(leftBlock).change(function () {
mode = $(this).val();
const select = document.createElement('select');
select.innerHTML = '<select><option value=s>' +
svgEditor.i18next.t(`${name}:import_single`) + '</option><option value=m>' +
svgEditor.i18next.t(`${name}:import_multi`) + '</option><option value=o>' +
svgEditor.i18next.t(`${name}:open`) + '</option>';
leftBlock.appendChild(select);
select.addEventListener('change', function () {
mode = this.value;
switch (mode) {
case 's':
case 'o':
toggleMulti(false);
break;
case 's':
case 'o':
toggleMulti(false);
break;
case 'm':
// Import multiple
toggleMulti(true);
break;
case 'm':
// Import multiple
toggleMulti(true);
break;
}
}).css({
'margin-top': 10
});
select.setAttribute('style', `margin-top: 10px;`);
cancel.prepend($.getSvgIcon('cancel', true));
back.prepend($.getSvgIcon('tool_imagelib', true));
imagelibStrings.imgLibs.forEach(function ({name, url, description}) {
$('<li>')
.appendTo(libOpts)
.text(name)
.on('click touchend', function () {
frame.attr(
'src',
url
).show();
header.text(name);
libOpts.hide();
back.show();
}).append(`<span>${description}</span>`);
imgLibs.forEach(function ({ name, url, description }) {
const li = document.createElement('li');
libOpts.appendChild(li);
li.textContent = name;
li.addEventListener('click', function () {
frame.setAttribute('src', url);
frame.style.display = 'block';
header.textContent = name;
libOpts.style.display = 'none';
back.style.display = 'block';
});
// eslint-disable-next-line sonarjs/no-identical-functions
li.addEventListener('touchend', function () {
frame.setAttribute('src', url);
frame.style.display = 'block';
header.textContent = name;
libOpts.style.display = 'none';
back.style.display = 'block';
});
var span = document.createElement("span");
span.textContent = description;
li.appendChild(span);
});
} else {
$('#imgbrowse_holder').show();
$id("imgbrowse_holder").style.display = 'block';
}
}
const buttons = [{
id: 'tool_imagelib',
type: 'app_menu', // _flyout
icon: 'imagelib.png',
position: 4,
events: {
mouseup: showBrowser
}
}];
};
return {
svgicons: 'ext-imagelib.xml',
buttons: imagelibStrings.buttons.map((button, i) => {
return Object.assign(buttons[i], button);
}),
callback () {
$('<style>').text(
'#imgbrowse_holder {' +
'position: absolute;' +
'top: 0;' +
'left: 0;' +
'width: 100%;' +
'height: 100%;' +
'background-color: rgba(0, 0, 0, .5);' +
'z-index: 5;' +
callback() {
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
buttonTemplate.innerHTML = `
<se-menu-item id="tool_imagelib" label="Image library" src="./images/library.svg"></se-menu-item>
`;
insertAfter($id('tool_export'), buttonTemplate.content.cloneNode(true));
$id('tool_imagelib').addEventListener("click", () => {
showBrowser();
});
const style = document.createElement('style');
style.textContent = '#imgbrowse_holder {' +
'position: absolute;' +
'top: 0;' +
'left: 0;' +
'width: 100%;' +
'height: 100%;' +
'background-color: rgba(0, 0, 0, .5);' +
'z-index: 5;' +
'}' +
'#imgbrowse {' +
'position: absolute;' +
'top: 25px;' +
'left: 25px;' +
'right: 25px;' +
'bottom: 25px;' +
'min-width: 300px;' +
'min-height: 200px;' +
'background: #B0B0B0;' +
'border: 1px outset #777;' +
'position: absolute;' +
'top: 25px;' +
'left: 25px;' +
'right: 25px;' +
'bottom: 25px;' +
'min-width: 300px;' +
'min-height: 200px;' +
'background: #5a6162;' +
'border: 1px outset #777;' +
'}' +
'#imgbrowse h1 {' +
'font-size: 20px;' +
'margin: .4em;' +
'text-align: center;' +
'font-size: 20px;' +
'margin: .4em;' +
'text-align: center;' +
'}' +
'#lib_framewrap,' +
'#imgbrowse > ul {' +
'position: absolute;' +
'top: 45px;' +
'left: 10px;' +
'right: 10px;' +
'bottom: 10px;' +
'background: white;' +
'margin: 0;' +
'padding: 0;' +
'position: absolute;' +
'top: 45px;' +
'left: 10px;' +
'right: 10px;' +
'bottom: 10px;' +
'background: white;' +
'margin: 0;' +
'padding: 0;' +
'}' +
'#imgbrowse > ul {' +
'overflow: auto;' +
'overflow: auto;' +
'}' +
'#imgbrowse > div {' +
'border: 1px solid #666;' +
'border: 1px solid #666;' +
'}' +
'#imglib_preview > div {' +
'padding: 5px;' +
'font-size: 12px;' +
'padding: 5px;' +
'font-size: 12px;' +
'}' +
'#imglib_preview img {' +
'display: block;' +
'margin: 0 auto;' +
'max-height: 100px;' +
'display: block;' +
'margin: 0 auto;' +
'max-height: 100px;' +
'}' +
'#imgbrowse li {' +
'list-style: none;' +
'padding: .5em;' +
'background: #E8E8E8;' +
'border-bottom: 1px solid #B0B0B0;' +
'line-height: 1.2em;' +
'font-style: sans-serif;' +
'}' +
'list-style: none;' +
'padding: .5em;' +
'background: #E8E8E8;' +
'border-bottom: 1px solid #5a6162;' +
'line-height: 1.2em;' +
'font-style: sans-serif;' +
'}' +
'#imgbrowse li > span {' +
'color: #666;' +
'font-size: 15px;' +
'display: block;' +
'}' +
'color: #666;' +
'font-size: 15px;' +
'display: block;' +
'}' +
'#imgbrowse li:hover {' +
'background: #FFC;' +
'cursor: pointer;' +
'}' +
'background: #FFC;' +
'cursor: pointer;' +
'}' +
'#imgbrowse iframe {' +
'width: 100%;' +
'height: 100%;' +
'border: 0;' +
'}'
).appendTo('head');
'width: 100%;' +
'height: 100%;' +
'border: 0;' +
'}';
document.head.appendChild(style);
}
};
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>-</title>
<link rel="icon" type="image/png" href="../../images/logo.png" />
<link rel="icon" type="image/png" href="../../images/logo.svg" />
<!-- Lacking browser support -->
<script nomodule="" src="../../redirect-on-no-module-support.js"></script>
@@ -19,7 +19,7 @@
<h1>Select an image:</h1>
<a href="smiley.svg">smiley.svg</a>
<br/>
<a href="../../images/logo.png">logo.png</a>
<a href="../../images/logo.svg">logo.svg</a>
</body>
</html>

View File

@@ -1,49 +1,52 @@
/* globals jQuery */
const $ = jQuery;
$('a').click(function () {
const {href} = this;
const target = window.parent;
const post = (message) => {
// Todo: Make origin customizable as set by opening window
// Todo: If dropping IE9, avoid stringifying
target.postMessage(JSON.stringify({
namespace: 'imagelib',
...message
}), '*');
};
// Convert Non-SVG images to data URL first
// (this could also have been done server-side by the library)
// Send metadata (also indicates file is about to be sent)
post({
name: $(this).text(),
id: href
});
if (!href.includes('.svg')) {
const img = new Image();
img.addEventListener('load', function () {
const canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
// load the raster image into the canvas
canvas.getContext('2d').drawImage(this, 0, 0);
// retrieve the data: URL
let data;
try {
data = canvas.toDataURL();
} catch (err) {
// This fails in Firefox with `file:///` URLs :(
// Todo: This could use a generic alert library instead
alert('Data URL conversion failed: ' + err); // eslint-disable-line no-alert
data = '';
}
post({href, data});
const atags = document.querySelectorAll('a');
Array.prototype.forEach.call(atags, function (aEle) {
aEle.addEventListener('click', function (event) {
const { href } = event.currentTarget;
const target = window.parent;
const post = (message) => {
// Todo: Make origin customizable as set by opening window
// Todo: If dropping IE9, avoid stringifying
target.postMessage(JSON.stringify({
namespace: 'imagelib',
...message
}), '*');
};
// Convert Non-SVG images to data URL first
// (this could also have been done server-side by the library)
// Send metadata (also indicates file is about to be sent)
post({
name: event.currentTarget.textContent,
id: href
});
img.src = href;
} else {
// Do ajax request for image's href value
$.get(href, function (data) {
post({href, data});
}, 'html'); // 'html' is necessary to keep returned data as a string
}
return false;
if (!href.includes('.svg')) {
const img = new Image();
img.addEventListener('load', function () {
const canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
// load the raster image into the canvas
canvas.getContext('2d').drawImage(this, 0, 0);
// retrieve the data: URL
let data;
try {
data = canvas.toDataURL();
} catch (err) {
// This fails in Firefox with `file:///` URLs :(
// Todo: This could use a generic alert library instead
alert('Data URL conversion failed: ' + err);
data = '';
}
post({ href, data });
});
img.src = href;
} else {
// Do ajax request for image's href value
$.get(href, function (data) {
post({ href, data });
}, 'html'); // 'html' is necessary to keep returned data as a string
}
return false;
});
});

View File

@@ -9,25 +9,8 @@ export default {
title: 'Bilder-Bibliothek'
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details
,
{
name: 'Openclipart',
url: 'https://openclipart.org/svgedit',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -9,30 +9,8 @@ export default {
title: 'Image library'
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
// The site is no longer using our API, and they have added an
// `X-Frame-Options` header which prevents our usage cross-origin:
// Getting messages like this in console:
// Refused to display 'https://openclipart.org/detail/307176/sign-bike' in a frame
// because it set 'X-Frame-Options' to 'sameorigin'.
// url: 'https://openclipart.org/svgedit',
// However, they do have a custom API which we are using here:
/*
{
name: 'Openclipart',
url: '{path}imagelib/openclipart.html',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -9,25 +9,8 @@ export default {
title: "Bibliothèque d'images"
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details
,
{
name: 'Openclipart',
url: 'https://openclipart.org/svgedit',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -9,25 +9,8 @@ export default {
title: 'Biblioteka obrazów'
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details
,
{
name: 'Openclipart',
url: 'https://openclipart.org/svgedit',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -9,25 +9,8 @@ export default {
title: 'Biblioteca de Imagens'
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details
,
{
name: 'Openclipart',
url: 'https://openclipart.org/svgedit',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -9,25 +9,8 @@ export default {
title: 'Bibliotecă de Imagini'
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details
,
{
name: 'Openclipart',
url: 'https://openclipart.org/svgedit',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -9,25 +9,8 @@ export default {
title: 'Knižnica obrázkov'
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details
,
{
name: 'Openclipart',
url: 'https://openclipart.org/svgedit',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -9,25 +9,8 @@ export default {
title: 'Knjižnica slik'
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details
,
{
name: 'Openclipart',
url: 'https://openclipart.org/svgedit',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -9,25 +9,8 @@ export default {
title: '图像库'
}
],
imgLibs: [
{
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details
,
{
name: 'Openclipart',
url: 'https://openclipart.org/svgedit',
description: 'Share and Use Images. Over 100,000 Public Domain SVG Images and Growing.'
}
*/
]
imgLibs_0_name: 'Demo library (local)',
imgLibs_0_description: 'Demonstration library for SVG-edit on this server',
imgLibs_1_name: 'IAN Symbol Libraries',
imgLibs_1_description: 'Free library of illustrations',
};

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>-</title>
<link rel="icon" type="image/png" href="../../images/logo.png" />
<link rel="icon" type="image/svg" href="../../images/logo.svg" />
<!-- Lacking browser support -->
<script nomodule="" src="../../redirect-on-no-module-support.js"></script>

View File

@@ -1,9 +1,7 @@
// eslint-disable-next-line node/no-unpublished-import
import {jml, body, nbsp} from 'jamilih';
// eslint-disable-next-line node/no-unpublished-import
/* eslint-disable node/no-unpublished-import */
import { jml, body, nbsp } from 'jamilih';
import $ from 'query-result';
// eslint-disable-next-line node/no-unpublished-import
import {manipulation} from 'qr-manipulation';
import { manipulation } from 'qr-manipulation';
manipulation($, jml);
@@ -22,32 +20,31 @@ async function processResults (url) {
* @returns {external:JamilihArray}
*/
function queryLink (query) {
return ['a', {
return [ 'a', {
href: jsVoid,
dataset: {value: query},
$on: {click (e) {
dataset: { value: query },
$on: { click (e) {
e.preventDefault();
const {value} = this.dataset;
const { value } = this.dataset;
$('#query')[0].$set(value);
$('#openclipart')[0].$submit();
}}
}, [query]];
} }
}, [ query ] ];
}
const r = await fetch(url);
const json = await r.json();
// console.log('json', json);
if (!json || json.msg !== 'success') {
// Todo: This could use a generic alert library instead
alert('There was a problem downloading the results'); // eslint-disable-line no-alert
alert('There was a problem downloading the results');
return;
}
const {payload, info: {
const { payload, info: {
results: numResults,
pages,
current_page: currentPage
}} = json;
} } = json;
// $('#page')[0].value = currentPage;
// $('#page')[0].max = pages;
@@ -62,21 +59,21 @@ async function processResults (url) {
// - `svg`'s: `png_thumb`, `png_full_lossy`, `png_2400px`
const semiColonSep = '; ' + nbsp;
$('#results').jml('div', [
['span', [
[ 'span', [
'Number of results: ',
numResults
]],
] ],
semiColonSep,
['span', [
[ 'span', [
'page ',
currentPage,
' out of ',
pages
]],
] ],
...payload.map(({
title, description, id,
uploader, created,
svg: {url: svgURL},
svg: { url: svgURL },
detail_link: detailLink,
tags_array: tagsArray,
downloaded_by: downloadedBy,
@@ -84,12 +81,11 @@ async function processResults (url) {
}) => {
const imgHW = '100px';
const colonSep = ': ' + nbsp;
return ['div', [
['button', {style: 'margin-right: 8px; border: 2px solid black;', dataset: {id, value: svgURL}, $on: {
return [ 'div', [
[ 'button', { style: 'margin-right: 8px; border: 2px solid black;', dataset: { id, value: svgURL }, $on: {
async click (e) {
e.preventDefault();
const {value: svgurl} = this.dataset;
// console.log('this', id, svgurl);
const { value: svgurl } = this.dataset;
const post = (message) => {
// Todo: Make origin customizable as set by opening window
// Todo: If dropping IE9, avoid stringifying
@@ -105,79 +101,78 @@ async function processResults (url) {
});
const result = await fetch(svgurl);
const svg = await result.text();
// console.log('url and svg', svgurl, svg);
post({
href: svgurl,
data: svg
});
}
}}, [
} }, [
// If we wanted interactive versions despite security risk:
// ['object', {data: svgURL, type: 'image/svg+xml'}]
['img', {src: svgURL, style: `width: ${imgHW}; height: ${imgHW};`}]
]],
['b', [title]],
[ 'img', { src: svgURL, style: `width: ${imgHW}; height: ${imgHW};` } ]
] ],
[ 'b', [ title ] ],
' ',
['i', [description]],
[ 'i', [ description ] ],
' ',
['span', [
[ 'span', [
'(ID: ',
['a', {
[ 'a', {
href: jsVoid,
dataset: {value: id},
dataset: { value: id },
$on: {
click (e) {
e.preventDefault();
const {value} = this.dataset;
const { value } = this.dataset;
$('#byids')[0].$set(value);
$('#openclipart')[0].$submit();
}
}
}, [id]],
}, [ id ] ],
')'
]],
] ],
' ',
['i', [
['a', {
[ 'i', [
[ 'a', {
href: detailLink,
target: '_blank'
}, ['Details']]
]],
['br'],
['span', [
['u', ['Uploaded by']], colonSep,
}, [ 'Details' ] ]
] ],
[ 'br' ],
[ 'span', [
[ 'u', [ 'Uploaded by' ] ], colonSep,
queryLink(uploader),
semiColonSep
]],
['span', [
['u', ['Download count']], colonSep,
] ],
[ 'span', [
[ 'u', [ 'Download count' ] ], colonSep,
downloadedBy,
semiColonSep
]],
['span', [
['u', ['Times used as favorite']], colonSep,
] ],
[ 'span', [
[ 'u', [ 'Times used as favorite' ] ], colonSep,
totalFavorites,
semiColonSep
]],
['span', [
['u', ['Created date']], colonSep,
] ],
[ 'span', [
[ 'u', [ 'Created date' ] ], colonSep,
created
]],
['br'],
['u', ['Tags']], colonSep,
] ],
[ 'br' ],
[ 'u', [ 'Tags' ] ], colonSep,
...tagsArray.map((tag) => {
return ['span', [
return [ 'span', [
' ',
queryLink(tag)
]];
] ];
})
]];
] ];
}),
['br'], ['br'],
[ 'br' ], [ 'br' ],
(currentPage === 1 || pages <= 2
? ''
: ['span', [
['a', {
: [ 'span', [
[ 'a', {
href: jsVoid,
$on: {
click (e) {
@@ -186,14 +181,14 @@ async function processResults (url) {
$('#openclipart')[0].$submit();
}
}
}, ['First']],
}, [ 'First' ] ],
' '
]]
] ]
),
(currentPage === 1
? ''
: ['span', [
['a', {
: [ 'span', [
[ 'a', {
href: jsVoid,
$on: {
click (e) {
@@ -202,14 +197,14 @@ async function processResults (url) {
$('#openclipart')[0].$submit();
}
}
}, ['Prev']],
}, [ 'Prev' ] ],
' '
]]
] ]
),
(currentPage === pages
? ''
: ['span', [
['a', {
: [ 'span', [
[ 'a', {
href: jsVoid,
$on: {
click (e) {
@@ -218,14 +213,14 @@ async function processResults (url) {
$('#openclipart')[0].$submit();
}
}
}, ['Next']],
}, [ 'Next' ] ],
' '
]]
] ]
),
(currentPage === pages || pages <= 2
? ''
: ['span', [
['a', {
: [ 'span', [
[ 'a', {
href: jsVoid,
$on: {
click (e) {
@@ -234,20 +229,20 @@ async function processResults (url) {
$('#openclipart')[0].$submit();
}
}
}, ['Last']],
}, [ 'Last' ] ],
' '
]]
] ]
)
]);
}
jml('div', [
['style', [
[ 'style', [
`.control {
padding-top: 10px;
}`
]],
['form', {
] ],
[ 'form', {
id: 'openclipart',
$custom: {
async $submit () {
@@ -255,7 +250,7 @@ jml('div', [
[
'query', 'sort', 'amount', 'page', 'byids'
].forEach((prop) => {
const {value} = $('#' + prop)[0];
const { value } = $('#' + prop)[0];
if (value) {
url.searchParams.set(prop, value);
}
@@ -271,12 +266,12 @@ jml('div', [
}
}, [
// Todo: i18nize
['fieldset', [
['legend', ['Search terms']],
['div', {class: 'control'}, [
['label', [
[ 'fieldset', [
[ 'legend', [ 'Search terms' ] ],
[ 'div', { class: 'control' }, [
[ 'label', [
'Query (Title, description, uploader, or tag): ',
['input', {id: 'query', name: 'query', placeholder: 'cat', $custom: {
[ 'input', { id: 'query', name: 'query', placeholder: 'cat', $custom: {
$set (value) {
$('#byids')[0].value = '';
this.value = value;
@@ -285,16 +280,16 @@ jml('div', [
change () {
$('#byids')[0].value = '';
}
}}]
]]
]],
['br'],
} } ]
] ]
] ],
[ 'br' ],
' OR ',
['br'],
['div', {class: 'control'}, [
['label', [
[ 'br' ],
[ 'div', { class: 'control' }, [
[ 'label', [
'IDs (single or comma-separated): ',
['input', {id: 'byids', name: 'ids', placeholder: '271380, 265741', $custom: {
[ 'input', { id: 'byids', name: 'ids', placeholder: '271380, 265741', $custom: {
$set (value) {
$('#query')[0].value = '';
this.value = value;
@@ -303,47 +298,47 @@ jml('div', [
change () {
$('#query')[0].value = '';
}
}}]
]]
]]
]],
['fieldset', [
['legend', ['Configuring results']],
['div', {class: 'control'}, [
['label', [
} } ]
] ]
] ]
] ],
[ 'fieldset', [
[ 'legend', [ 'Configuring results' ] ],
[ 'div', { class: 'control' }, [
[ 'label', [
'Sort by: ',
['select', {id: 'sort'}, [
[ 'select', { id: 'sort' }, [
// Todo: i18nize first values
['Date', 'date'],
['Downloads', 'downloads'],
['Favorited', 'favorites']
].map(([text, value = text]) => {
return ['option', {value}, [text]];
})]
]]
]],
['div', {class: 'control'}, [
['label', [
[ 'Date', 'date' ],
[ 'Downloads', 'downloads' ],
[ 'Favorited', 'favorites' ]
].map(([ text, value = text ]) => {
return [ 'option', { value }, [ text ] ];
}) ]
] ]
] ],
[ 'div', { class: 'control' }, [
[ 'label', [
'Results per page: ',
['input', {
[ 'input', {
id: 'amount', name: 'amount', value: 10,
type: 'number', min: 1, max: 200, step: 1, pattern: '\\d+'}]
]]
]],
['div', {class: 'control'}, [
['label', [
type: 'number', min: 1, max: 200, step: 1, pattern: '\\d+' } ]
] ]
] ],
[ 'div', { class: 'control' }, [
[ 'label', [
'Page number: ',
['input', {
[ 'input', {
// max: 1, // We'll change this based on available results
id: 'page', name: 'page', value: 1, style: 'width: 40px;',
type: 'number', min: 1, step: 1, pattern: '\\d+'
}]
]]
]]
]],
['div', {class: 'control'}, [
['input', {type: 'submit'}]
]]
]],
['div', {id: 'results'}]
} ]
] ]
] ]
] ],
[ 'div', { class: 'control' }, [
[ 'input', { type: 'submit' } ]
] ]
] ],
[ 'div', { id: 'results' } ]
], body);

Some files were not shown because too many files have changed in this diff Show More