update master to V7

This commit is contained in:
JFH
2021-05-09 19:29:45 +02:00
parent 41fc05672d
commit 593c415664
1000 changed files with 47537 additions and 54304 deletions

View File

@@ -6,9 +6,6 @@
* @copyright 2010 Jeff Schiller, 2010 Alexis Deveria
*/
// Dependencies:
// 1) jQuery (for $.alert())
import 'pathseg';
import {NS} from './namespaces.js';
@@ -30,7 +27,6 @@ const svg = document.createElementNS(NS.SVG, 'svg');
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
};

View File

@@ -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.
@@ -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;

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

@@ -0,0 +1,481 @@
// 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-polygon',
'ext-shapes',
'ext-star',
'ext-storage',
'ext-opensave'
];
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);
}
}

1221
src/editor/Editor.js Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,806 @@
/* 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 () {
const self = this;
const { i18next } = await putLocale(this.configObj.pref('lang'), this.goodLangs);
this.i18next = i18next;
// 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);
// editor prefences dialoag added to DOM
const newSeEditPrefsDialog = document.createElement('se-edit-prefs-dialog');
newSeEditPrefsDialog.setAttribute('id', 'se-edit-prefs');
document.body.append(newSeEditPrefsDialog);
// canvas menu added to DOM
const dialogBox = document.createElement('se-cmenu_canvas-dialog');
dialogBox.setAttribute('id', 'se-cmenu_canvas');
document.body.append(dialogBox);
// 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);
} catch (err) {
console.error(err);
}
if ('localStorage' in window) { // && onWeb removed so Webkit works locally
this.storage = window.localStorage;
}
this.configObj.load();
/**
* @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.uiStrings.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.uiStrings.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.uiStrings.notification.unsavedChanges; // Firefox needs this when beforeunload set by addEventListener (even though message is not used)
return this.uiStrings.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.uiStrings.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.log(err);
}
}
/**
* @param {PlainObject} info
* @param {any} info.data
* @param {string} info.origin
* @fires module:svgcanvas.SvgCanvas#event:message
* @returns {void}
*/
messageListener ({data, origin}) {
// console.log('data, origin, extensionsAdded', data, origin, extensionsAdded);
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;

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

@@ -0,0 +1,403 @@
/* 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.uiStrings.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.uiStrings.notification.invalidAttrValGiven);
return false;
}
if (h !== "fit" && !isValidUnit("height", h)) {
seAlert(this.editor.uiStrings.notification.invalidAttrValGiven);
return false;
}
if (!this.editor.svgCanvas.setResolution(w, h)) {
seAlert(this.editor.uiStrings.notification.noContentToFitTo);
return false;
}
// Set image save option
this.editor.configObj.pref("img_save", save);
this.editor.updateCanvas();
this.editor.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")) {
const { langParam, langData } = await this.editor.putLocale(
lang,
this.editor.goodLangs
);
await this.editor.svgCanvassetLang(langParam, langData);
}
// 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.editor.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.uiStrings.notification;
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,805 @@
/* eslint-disable max-len */
import {jGraduate, jGraduateMethod} from './jgraduate/jQuery.jGraduate.js';
import PaintBox from './PaintBox.js';
const template = document.createElement('template');
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(../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="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', `Pick a ${newValue} Paint and Opacity`);
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 #B0B0B0;
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 #B0B0B0;
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,136 @@
/* eslint-disable max-len */
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');
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="Click to change fill color, shift-click to change stroke color">
<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 !important;
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,7 @@ 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
console.log('Registered contextmenu item: {id:' + menuItem.id + ', label:' + menuItem.label + '}');
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 #B0B0B0;
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,276 @@
const template = document.createElement('template');
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">
Cut<span class="shortcut">META+X</span>
</a>
</li>
<li>
<a href="#copy" id="se-copy">
Copy<span class="shortcut">META+C</span>
</a>
</li>
<li>
<a href="#paste" id="se-paste">Paste</a>
</li>
<li>
<a href="#paste_in_place" id="se-paste-in-place">Paste in Place</a>
</li>
<li class="separator">
<a href="#delete" id="se-delete">
Delete<span class="shortcut">BACKSPACE</span>
</a>
</li>
<li class="separator">
<a href="#group" id="se-group">
Group<span class="shortcut">G</span>
</a>
</li>
<li>
<a href="#ungroup" id="se-ungroup">
Ungroup<span class="shortcut">G</span>
</a>
</li>
<li class="separator">
<a href="#move_front" id="se-move-front">
Bring to Front<span class="shortcut">CTRL+SHFT+]</span>
</a>
</li>
<li>
<a href="#move_up" id="se-move-up">
Bring Forward<span class="shortcut">CTRL+]</span>
</a>
</li>
<li>
<a href="#move_down" id="se-move-down">
Send Backward<span class="shortcut">CTRL+[</span>
</a>
</li>
<li>
<a href="#move_back" id="se-move-back">
Send to 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 observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return ['disableallmenu', 'enablemenuitems', 'disablemenuitems'];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
let eles = [];
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;
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,194 @@
const template = document.createElement('template');
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">Duplicate Layer...</a></li>
<li><a href="#delete" id="se-layer-delete">Delete Layer</a></li>
<li><a href="#merge_down" id="se-merge-down">Merge Down</a></li>
<li><a href="#merge_all" id="se-merge-all">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 observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return ['value', 'leftclick'];
}
/**
* @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;
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,538 @@
const template = document.createElement('template');
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: #B0B0B0;
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;
}
#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">
<img class="svg_icon" src="./images/ok.svg" alt="icon" width="16" height="16" />
OK
</button>
<button id="tool_prefs_cancel">
<img class="svg_icon" src="./images/cancel.svg" alt="icon" width="16" height="16" />
Cancel
</button>
</div>
<fieldset>
<legend id="svginfo_editor_prefs">Editor Preferences</legend>
<label>
<span id="svginfo_lang">Language:</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">Icon size:</span>
<select id="iconsize">
<option id="icon_small" value="s">Small</option>
<option id="icon_medium" value="m" selected="selected">Medium</option>
<option id="icon_large" value="l">Large</option>
<option id="icon_xlarge" value="xl">Extra Large</option>
</select>
</label>
<fieldset id="change_background">
<legend id="svginfo_change_background">Editor Background</legend>
<div id="bg_blocks"></div>
<label>
<span id="svginfo_bg_url">URL:</span>
<input type="text" id="canvas_bg_url" />
</label>
<p id="svginfo_bg_note">Note: Background will not be saved with image.</p>
</fieldset>
<fieldset id="change_grid">
<legend id="svginfo_grid_settings">Grid</legend>
<label for="svginfo_snap_onoff">
<span id="svginfo_snap_onoff">Snapping on/off</span>
<input type="checkbox" value="snapping_on" id="grid_snapping_on" />
</label>
<label for="grid_snapping_step">
<span id="svginfo_snap_step">Snapping Step-Size:</span>
<input type="text" id="grid_snapping_step" size="3" value="10" />
</label>
<label>
<span id="svginfo_grid_color">Grid color:</span>
<input type="text" id="grid_color" size="3" value="#000" />
</label>
</fieldset>
<fieldset id="units_rulers">
<legend id="svginfo_units_rulers">Units &amp; Rulers</legend>
<label>
<span id="svginfo_rulers_onoff">Show rulers</span>
<input id="show_rulers" type="checkbox" value="show_rulers" checked="checked" />
</label>
<label>
<span id="svginfo_unit">Base 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 observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
// eslint-disable-next-line max-len
return ['dialog', 'lang', 'iconsize', 'canvasbg', 'bgurl', 'gridsnappingon', 'gridsnappingstep', 'gridcolor', 'showrulers', 'baseunit'];
}
/**
* @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';
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;
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,188 @@
import './se-elix/define/NumberSpinBox.js';
const template = document.createElement('template');
template.innerHTML = `
<style>
#dialog_content {
margin: 10px 10px 5px 10px;
background: #DDD;
overflow: auto;
text-align: left;
border: 1px solid #B0B0B0;
}
#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: #CCC;
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">
Select an image type for export:
</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">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">
<img class="svg_icon" src="./images/ok.svg" alt="icon" width="16" height="16" />
Ok
</button>
<button id="export_cancel">
<img class="svg_icon" src="./images/cancel.svg" alt="icon" width="16" height="16" />
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 observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return ['dialog'];
}
/**
* @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
switch (name) {
case 'dialog':
if (newValue === 'open') {
this.$dialog.open();
} else {
this.$dialog.close();
}
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,375 @@
import {isValidUnit} from '../../common/units.js';
const template = document.createElement('template');
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: #B0B0B0;
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">OK</button>
<button id="tool_docprops_cancel">Cancel</button>
</div>
<fieldset id="svg_docprops_docprops">
<legend id="svginfo_image_props">Image Properties</legend>
<label>
<span id="svginfo_title">Title:</span>
<input type="text" id="canvas_title" />
</label>
<fieldset id="change_resolution">
<legend id="svginfo_dim">Canvas Dimensions</legend>
<label>
<span id="svginfo_width">width:</span>
<input type="text" id="canvas_width" size="6" />
</label>
<label>
<span id="svginfo_height">height:</span>
<input type="text" id="canvas_height" size="6" />
</label>
<label>
<select id="resolution">
<option id="selectedPredefined" selected="selected">Select predefined:</option>
<option>640x480</option>
<option>800x600</option>
<option>1024x768</option>
<option>1280x960</option>
<option>1600x1200</option>
<option id="fitToContent" value="content">Fit to Content</option>
</select>
</label>
</fieldset>
<fieldset id="image_save_opts">
<legend id="includedImages">Included Images</legend>
<label>
<input type="radio" id="image_embed" name="image_opt" value="embed" checked="checked" />
<span id="image_opt_embed">Embed data (local files)</span>
</label>
<label>
<input type="radio" id="image_ref" name="image_opt" value="ref" />
<span id="image_opt_ref">Use file reference</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 observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return ['title', 'width', 'height', 'save', 'dialog', 'embed'];
}
/**
* @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.$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;
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,235 @@
const template = document.createElement('template');
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: #B0B0B0;
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;
margin: 5px 10px;
}
</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">
<img class="svg_icon" src="./images/ok.svg" alt="icon" width="16" height="16" />
Apply Changes
</button>
<button id="tool_source_cancel">
<img class="svg_icon" src="./images/cancel.svg" alt="icon" width="16" height="16" />
Cancel
</button>
</div>
<div id="save_output_btns">
<p id="copy_save_note">
Copy the contents of this box into a text editor,
then save the file with a .svg extension.</p>
<button id="copy_save_done">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 observedAttributes
* @returns {any} observed
*/
static get observedAttributes () {
return ['dialog', 'value', 'applysec', 'copysec'];
}
/**
* @function attributeChangedCallback
* @param {string} name
* @param {string} oldValue
* @param {string} newValue
* @returns {void}
*/
attributeChangedCallback (name, oldValue, newValue) {
if (oldValue === newValue) return;
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;
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

@@ -0,0 +1,97 @@
// https://github.com/knadh/dragmove.js
// Kailash Nadh (c) 2020.
// MIT License.
// can't use npm version as the dragmove is different.
let _loaded = false;
let _callbacks = [];
const _isTouch = window.ontouchstart !== undefined;
export const dragmove = function(target, handler, parent, onStart, onEnd, onDrag) {
// Register a global event to capture mouse moves (once).
if (!_loaded) {
document.addEventListener(_isTouch ? "touchmove" : "mousemove", function(e) {
let c = e;
if (e.touches) {
c = e.touches[0];
}
// On mouse move, dispatch the coords to all registered callbacks.
for (let i = 0; i < _callbacks.length; i++) {
_callbacks[i](c.clientX, c.clientY);
}
});
}
_loaded = true;
let isMoving = false, hasStarted = false;
let startX = 0, startY = 0, lastX = 0, lastY = 0;
// On the first click and hold, record the offset of the pointer in relation
// to the point of click inside the element.
handler.addEventListener(_isTouch ? "touchstart" : "mousedown", function(e) {
e.stopPropagation();
e.preventDefault();
if (target.dataset.dragEnabled === "false") {
return;
}
let c = e;
if (e.touches) {
c = e.touches[0];
}
isMoving = true;
startX = target.offsetLeft - c.clientX;
startY = target.offsetTop - c.clientY;
});
// On leaving click, stop moving.
document.addEventListener(_isTouch ? "touchend" : "mouseup", function() {
if (onEnd && hasStarted) {
onEnd(target, parent, parseInt(target.style.left), parseInt(target.style.top));
}
isMoving = false;
hasStarted = false;
});
// On leaving click, stop moving.
document.addEventListener(_isTouch ? "touchmove" : "mousemove", function() {
if (onDrag && hasStarted) {
onDrag(target, parseInt(target.style.left), parseInt(target.style.top));
}
});
// Register mouse-move callback to move the element.
_callbacks.push(function move(x, y) {
if (!isMoving) {
return;
}
if (!hasStarted) {
hasStarted = true;
if (onStart) {
onStart(target, lastX, lastY);
}
}
lastX = x + startX;
lastY = y + startY;
// If boundary checking is on, don't let the element cross the viewport.
if (target.dataset.dragBoundary === "true") {
if (lastX < 1 || lastX >= window.innerWidth - target.offsetWidth) {
return;
}
if (lastY < 1 || lastY >= window.innerHeight - target.offsetHeight) {
return;
}
}
target.style.left = lastX + "px";
target.style.top = lastY + "px";
});
}
export { dragmove as default };

View File

@@ -1,4 +1,3 @@
/* globals jQuery */
/**
* Attaches items to DOM for Embedded SVG support.
* @module EmbeddedSVGEditDOM
@@ -6,8 +5,6 @@
import EmbeddedSVGEdit from './embedapi.js';
import {isChrome} from '../common/browser.js';
const $ = jQuery;
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 () {
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.log('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
@@ -71,7 +71,6 @@ function messageListener (e) {
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.`
@@ -111,6 +110,7 @@ svgCanvas.setSvgString('string');
svgCanvas.setSvgString('string')(function (data, error) {
if (error) {
// There was an error
throw error
} else {
// Handle data
}
@@ -344,7 +344,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++;
@@ -362,8 +362,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

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,7 +61,7 @@ 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'},
@@ -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;
}
}
@@ -214,25 +217,26 @@ export default {
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;
}
});
if (!newMarker) {
// Create a new marker with this color
const lastId = marker.id;
@@ -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,11 +289,12 @@ 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}) {
async addLangData ({_lang, importLocale}) {
const {langList} = await importLocale();
return {
data: langList

View File

@@ -1,19 +0,0 @@
export default {
name: 'Arrows',
langList: [
{id: 'arrow_none', textContent: 'Kein Pfeil'}
],
contextTools: [
{
title: 'Pfeiltyp auswählen',
options: {
none: 'Kein Pfeil',
end: '----&gt;',
start: '&lt;----',
both: '&lt;---&gt;',
mid: '--&gt;--',
mid_bk: '--&lt;--'
}
}
]
};

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

@@ -1,11 +0,0 @@
export default {
name: 'ClosePath',
buttons: [
{
title: 'Pfad öffnen'
},
{
title: 'Pfad schließen'
}
]
};

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
@@ -21,27 +22,24 @@ const loadExtensionTranslation = async function (lang) {
export default {
name: 'connector',
async init (S) {
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;
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 +49,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;
@@ -88,9 +86,8 @@ export default {
* @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;
@@ -101,13 +98,16 @@ export default {
* @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';
}
/**
@@ -118,7 +118,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;
@@ -151,7 +151,8 @@ export default {
* @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,24 +164,24 @@ 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);
}
}
@@ -190,55 +191,52 @@ export default {
* @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) {
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]);
connections.push({
elem: cElem,
connector: this,
connector: ethis,
is_start: (i === 0),
start_x: bb.x,
start_y: bb.y
@@ -252,7 +250,8 @@ 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,7 +262,7 @@ 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';
@@ -272,13 +271,13 @@ export default {
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 +286,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
@@ -310,7 +309,8 @@ export default {
const gse = svgCanvas.groupSelectedElements;
svgCanvas.groupSelectedElements = function (...args) {
svgCanvas.removeFromSelection($(connSel).toArray());
svgCanvas.removeFromSelection(document.querySelectorAll('.se_connector'));
return gse.apply(this, args);
};
@@ -329,71 +329,68 @@ 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);
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);
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
return {
/** @todo JFH special flag */
newUI: true,
name: strings.name,
svgicons: 'conn.svg',
buttons: strings.buttons.map((button, i) => {
return Object.assign(buttons[i], button);
}),
/* async */ addLangData ({lang}) { // , importLocale: importLoc
callback() {
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
buttonTemplate.innerHTML = `
<se-button id="mode_connect" title="Connect two objects" 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
};
},
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]);
@@ -415,7 +412,7 @@ export default {
style: 'pointer-events:none'
}
});
elData(curLine, 'start_bb', bb);
dataStorage.put(curLine, 'start_bb', bb);
}
return {
started: true
@@ -426,7 +423,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 +438,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,7 +451,7 @@ 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.getTransformList(elem).clear();
@@ -464,7 +462,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 +473,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 +487,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 +501,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,
@@ -523,13 +524,12 @@ export default {
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.moveToBottomSelectedElement();
@@ -541,9 +541,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 +556,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 +570,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 +590,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 +600,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,7 +614,8 @@ 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]);
@@ -621,14 +623,14 @@ export default {
}
}
// Update line if it's a connector
if (elem.getAttribute('class') === connSel.substr(1)) {
const start = getElem(elData(elem, 'c_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 +644,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,11 +0,0 @@
export default {
name: 'Connector',
langList: [
{id: 'mode_connect', title: 'Zwei Objekte verbinden'}
],
buttons: [
{
title: 'Zwei Objekte verbinden'
}
]
};

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
@@ -21,12 +22,12 @@ const loadExtensionTranslation = async function (lang) {
export default {
name: 'eyedropper',
async init (S) {
async init(S) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const {$, ChangeElementCommand} = S, // , svgcontent,
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
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 +37,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)
) {
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,41 +67,33 @@ 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);
}),
callback() {
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
buttonTemplate.innerHTML = `
<se-button id="tool_eyedropper" title="Eye Dropper Tool" src="./images/eye_dropper.svg" shortcut="I"></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;
const { target } = e;
if (!['svg', 'g', 'use'].includes(target.nodeName)) {
const changes = {};

View File

@@ -1,9 +0,0 @@
export default {
name: 'eyedropper',
buttons: [
{
title: 'Pipetten Werkzeug',
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,7 +80,8 @@ 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));
@@ -82,7 +89,7 @@ export default {
svgCanvas.clearSelection();
} catch (e) {
// Todo: Surface error to user
console.log(e); // eslint-disable-line no-console
console.log(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();
}
/**
@@ -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,12 +269,15 @@ 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);
@@ -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

@@ -1,25 +0,0 @@
export default {
name: 'foreignObject',
buttons: [
{
title: 'Foreign Object Werkzeug'
},
{
title: 'Inhalt des ForeignObject bearbeiten'
}
],
contextTools: [
{
title: 'Breite des ForeignObject ändern',
label: 'w'
},
{
title: 'Höhe des ForeignObject ändern',
label: 'h'
},
{
title: 'Schriftgröße des ForeignObject ändern',
label: 'Schriftgröße'
}
]
};

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
@@ -21,19 +22,21 @@ const loadExtensionTranslation = async function (lang) {
export default {
name: 'grid',
async init ({$, NS, getTypeMap}) {
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;
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
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 +48,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 +70,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 +84,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 +112,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;
@@ -141,40 +144,34 @@ export default {
*
* @returns {void}
*/
function gridUpdate () {
const gridUpdate = () => {
if (showGrid) {
updateGrid(svgCanvas.getZoom());
}
$('#canvasGrid').toggle(showGrid);
$('#view_grid').toggleClass('push_button_pressed tool_button');
$id('canvasGrid').style.display = (showGrid) ? 'block' : 'none';
document.getElementById('view_grid').pressed = showGrid;
}
const buttons = [{
id: 'view_grid',
icon: 'grid.png',
type: 'context',
panel: 'editor_panel',
events: {
click () {
svgEditor.curConfig.showGrid = showGrid = !showGrid;
gridUpdate();
}
}
}];
return {
name: strings.name,
svgicons: 'grid-icon.xml',
zoomChanged (zoom) {
if (showGrid) { updateGrid(zoom); }
},
callback () {
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
buttonTemplate.innerHTML = `
<se-button id="view_grid" title="Show grid" 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

@@ -1,8 +0,0 @@
export default {
name: 'Raster anzeigen',
buttons: [
{
title: 'Raster anzeigen/verbergen'
}
]
};

View File

@@ -16,6 +16,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
@@ -27,39 +28,22 @@ const loadExtensionTranslation = async function (lang) {
export default {
name: 'helloworld',
async init ({$, importLocale}) {
async init ({_importLocale}) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const svgCanvas = svgEditor.canvas;
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const {svgCanvas} = svgEditor;
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: [{
events: [{
// 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');
}
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');
}
}],
// This is triggered when the main mouse button is pressed down
@@ -95,7 +79,7 @@ export default {
});
// Show the text using the custom alert function
$.alert(text);
alert(text);
}
}
};

View File

@@ -1,9 +0,0 @@
export default {
name: 'Hello World',
text: 'Hello World!\n\nSie haben hier geklickt: {x}, {y}',
buttons: [
{
title: "Sage 'Hello World'"
}
]
};

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-unsanitized/property */
/* globals seConfirm */
/**
* @file ext-imagelib.js
*
@@ -10,6 +12,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
@@ -21,13 +24,14 @@ const loadExtensionTranslation = async function (lang) {
export default {
name: 'imagelib',
async init ({$, decode64, dropXMLInternalSubset}) {
async init({ decode64, dropXMLInternalSubset }) {
const svgEditor = this;
const imagelibStrings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const { $id } = svgEditor.svgCanvas;
const imagelibStrings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const {uiStrings, canvas: svgCanvas} = svgEditor;
const { uiStrings, svgCanvas } = svgEditor;
const allowedImageLibOrigins = imagelibStrings.imgLibs.map(({url}) => {
const allowedImageLibOrigins = imagelibStrings.imgLibs.map(({ url }) => {
try {
return new URL(url).origin;
} catch (err) {
@@ -39,8 +43,8 @@ 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
}
@@ -48,7 +52,7 @@ export default {
* @param {string} url
* @returns {void}
*/
function importImage (url) {
const importImage = (url) => {
const newImage = svgCanvas.addSVGElementFromJson({
element: 'image',
attr: {
@@ -108,7 +112,7 @@ 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
async function onMessage({ origin, data: response }) {
if (!response || !['string', 'object'].includes(typeof response)) {
// Do nothing
return;
@@ -125,7 +129,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.log(`Origin ${origin} not whitelisted for posting to ${window.origin}`);
return;
}
const hasName = 'name' in response;
@@ -142,7 +146,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 +174,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 = uiStrings.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 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" />' + uiStrings.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" />' + imagelibStrings.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>' +
const select = document.createElement('select');
select.innerHTML = '<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();
imagelibStrings.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>`);
imagelibStrings.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: #B0B0B0;' +
'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 #B0B0B0;' +
'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

@@ -1,9 +1,9 @@
export default {
select_lib: 'Bilder Bibliothek auswählen',
show_list: 'Liste aller Bibliotheken anzeigen',
import_single: 'Einzelne importieren',
import_multi: 'Mehrere importieren',
open: 'Öffnen als neues Dokument',
select_lib: 'Select an image library',
show_list: 'Show library list',
import_single: 'Import single',
import_multi: 'Import multiple',
open: 'Open as new document',
buttons: [
{
title: 'Bilder-Bibliothek'
@@ -11,14 +11,14 @@ export default {
],
imgLibs: [
{
name: 'Demo Bibliothek (lokal)',
name: 'Demo library (local)',
url: 'extensions/ext-imagelib/index.html',
description: 'Demo Bibltiothek für svg-edit auf diesem Server'
description: 'Demonstration library for SVG-edit on this server'
},
{
name: 'IAN Symbol Bibliothek',
name: 'IAN Symbol Libraries',
url: 'https://ian.umces.edu/symbols/catalog/svgedit/album_chooser.php?svgedit=3',
description: 'Kostenlose Bibliothek mit Illustrationen'
description: 'Free library of illustrations'
}
/*
// See message in "en" locale for further details

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,8 +1,6 @@
// eslint-disable-next-line node/no-unpublished-import
/* eslint-disable node/no-unpublished-import */
import {jml, body, nbsp} from 'jamilih';
// eslint-disable-next-line node/no-unpublished-import
import $ from 'query-result';
// eslint-disable-next-line node/no-unpublished-import
import {manipulation} from 'qr-manipulation';
manipulation($, jml);
@@ -40,7 +38,7 @@ async function processResults (url) {
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: {

View File

@@ -32,6 +32,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
@@ -45,9 +46,10 @@ export default {
name: 'markers',
async init (S) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const {$} = S;
const svgCanvas = svgEditor.canvas;
const {svgCanvas} = svgEditor;
const {$id} = svgCanvas;
const // {svgcontent} = S,
addElem = svgCanvas.addSVGElementFromJson;
const mtypes = ['start', 'mid', 'end'];
@@ -120,9 +122,17 @@ export default {
*/
function setIcon (pos, id) {
if (id.substr(0, 1) !== '\\') { id = '\\textmarker'; }
const ci = '#' + idPrefix + pos + '_' + id.substr(1);
svgEditor.setIcon('#cur_' + pos + '_marker_list', $(ci).children());
$(ci).addClass('current').siblings().removeClass('current');
const ci = idPrefix + pos + '_' + id.substr(1);
console.log(ci)
console.log('cur_' + pos + '_marker_list')
svgEditor.setIcon('cur_' + pos + '_marker_list', $id(ci).children);
$id(ci).classList.add('current');
const siblings = Array.prototype.filter.call($id(ci).parentNode.children, function(child){
return child !== $id(ci);
});
Array.from(siblings).forEach(function(sibling) {
sibling.classList.remove('current');
});
}
let selElems;
@@ -133,7 +143,7 @@ export default {
* @returns {void}
*/
function showPanel (on) {
$('#marker_panel').toggle(on);
$id('marker_panel').style.display = (on) ? 'block' : 'none';
if (on) {
const el = selElems[0];
@@ -141,11 +151,11 @@ export default {
let val, ci;
$.each(mtypes, function (i, pos) {
const m = getLinked(el, 'marker-' + pos);
const txtbox = $('#' + pos + '_marker');
const txtbox = $id(pos + '_marker');
if (!m) {
val = '\\nomarker';
ci = val;
txtbox.hide(); // hide text box
txtbox.style.display = 'none';
} else {
if (!m.attributes.se_type) { return; } // not created by this extension
val = '\\' + m.attributes.se_type.textContent;
@@ -154,10 +164,10 @@ export default {
val = m.lastChild.textContent;
// txtbox.show(); // show text box
} else {
txtbox.hide(); // hide text box
txtbox.style.display = 'none';
}
}
txtbox.val(val);
txtbox.value = val;
setIcon(pos, ci);
});
}
@@ -189,7 +199,7 @@ export default {
let viewBox = '0 0 100 100';
let markerWidth = 5;
let markerHeight = 5;
const seType = val.substr(0, 1) === '\\' ? val.substr(1) : 'textmarker';
const seType = (val.substr(0, 1) === '\\') ? val.substr(1) : 'textmarker';
if (!markerTypes[seType]) { return undefined; } // an unknown type!
@@ -301,7 +311,8 @@ export default {
batchCmd.addSubCommand(new S.RemoveElementCommand(elem, elem.parentNode));
batchCmd.addSubCommand(new S.InsertElementCommand(pline));
$(elem).after(pline).remove();
elem.insertAdjacentElement('afterend', pline);
elem.remove();
svgCanvas.clearSelection();
pline.id = id;
svgCanvas.addToSelection([pline]);
@@ -319,7 +330,7 @@ export default {
const markerName = 'marker-' + pos;
const el = selElems[0];
const marker = getLinked(el, markerName);
if (marker) { $(marker).remove(); }
if (marker) { marker.remove(); }
el.removeAttribute(markerName);
let val = this.value;
if (val === '') { val = '\\nomarker'; }
@@ -378,7 +389,7 @@ export default {
const len = el.id.length;
const linkid = url.substr(-len - 1, len);
if (el.id !== linkid) {
const val = $('#' + pos + '_marker').attr('value');
const val = $id(pos + '_marker').getAttribute('value');
addMarker(id, val);
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
if (el.tagName === 'line' && pos === 'mid') { el = convertline(el); }
@@ -395,21 +406,19 @@ export default {
* @returns {void}
*/
function triggerTextEntry (pos, val) {
$('#' + pos + '_marker').val(val);
$('#' + pos + '_marker').change();
// const txtbox = $('#'+pos+'_marker');
// if (val.substr(0,1)=='\\') {txtbox.hide();}
// else {txtbox.show();}
$id(pos + '_marker').value = val;
$id(pos + '_marker').change();
}
/**
* @param {"start"|"mid"|"end"} pos
* @returns {Promise<void>} Resolves to `undefined`
* @returns {void} Resolves to `undefined`
*/
async function showTextPrompt (pos) {
let def = $('#' + pos + '_marker').val();
function showTextPrompt (pos) {
let def = $id(pos + '_marker').value;
if (def.substr(0, 1) === '\\') { def = ''; }
const txt = await $.prompt('Enter text for ' + pos + ' marker', def);
// eslint-disable-next-line no-alert
const txt = prompt('Enter text for ' + pos + ' marker', def);
if (txt) {
triggerTextEntry(pos, txt);
}
@@ -444,7 +453,7 @@ export default {
* @param {Event} ev
* @returns {Promise<void>} Resolves to `undefined`
*/
async function setArrowFromButton (ev) {
async function setArrowFromButton () {
const parts = this.id.split('_');
const pos = parts[1];
let val = parts[2];
@@ -507,7 +516,7 @@ export default {
buttons.push({
id: idPrefix + pos + '_' + id,
svgicon: id,
icon: 'markers-' + id + '.png',
icon: id + '.svg',
title,
type: 'context',
events: {click: setArrowFromButton},
@@ -564,11 +573,14 @@ export default {
return {
name: strings.name,
svgicons: 'markers-icons.xml',
svgicons: '',
callback () {
$('#marker_panel').addClass('toolset').hide();
if($id("marker_panel") !== null) {
$id("marker_panel").classList.add('toolset');
$id("marker_panel").style.display = 'none';
}
},
/* async */ addLangData ({importLocale, lang}) {
/* async */ addLangData ({_importLocale, _lang}) {
return {data: strings.langList};
},
selectedChanged (opts) {
@@ -594,7 +606,6 @@ export default {
},
elementChanged (opts) {
// console.log('elementChanged',opts);
const elem = opts.elems[0];
if (elem && (
elem.getAttribute('marker-start') ||

View File

@@ -1,46 +0,0 @@
export default {
name: 'Markers',
langList: [
{id: 'nomarker', title: 'Keine Markierung'},
{id: 'leftarrow', title: 'Pfeil links'},
{id: 'rightarrow', title: 'Pfeil rechts'},
{id: 'textmarker', title: 'Text Marker'},
{id: 'forwardslash', title: 'Schrägstrich'},
{id: 'reverseslash', title: 'Umgekehrter Schrägstrich'},
{id: 'verticalslash', title: 'Vertikaler Strich'},
{id: 'box', title: 'Box'},
{id: 'star', title: 'Stern'},
{id: 'xmark', title: 'X'},
{id: 'triangle', title: 'Dreieck'},
{id: 'mcircle', title: 'Kreis'},
{id: 'leftarrow_o', title: 'Offener Pfeil links'},
{id: 'rightarrow_o', title: 'Offener Pfeil rechts'},
{id: 'box_o', title: 'Offene Box'},
{id: 'star_o', title: 'Offener Stern'},
{id: 'triangle_o', title: 'Offenes Dreieck'},
{id: 'mcircle_o', title: 'Offener Kreis'}
],
contextTools: [
{
title: 'Start-Markierung',
label: 's'
},
{
title: 'Start-Markierung auswählen'
},
{
title: 'Mitte-Markierung',
label: 'm'
},
{
title: 'Mitte-Markierung auswählen'
},
{
title: 'End-Markierung',
label: 'e'
},
{
title: 'End-Markierung auswählen'
}
]
};

View File

@@ -11,6 +11,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
@@ -24,8 +25,9 @@ export default {
name: 'mathjax',
async init ({$}) {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const svgCanvas = svgEditor.canvas;
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const {svgCanvas} = svgEditor;
const {$id} = svgCanvas;
// Configuration of the MathJax extention.
@@ -81,7 +83,7 @@ export default {
* @returns {void}
*/
function saveMath () {
const code = $('#mathjax_code_textarea').val();
const code = $id('mathjax_code_textarea').value;
// displaystyle to force MathJax NOT to use the inline style. Because it is
// less fancy!
MathJax.Hub.queue.Push(['Text', math, '\\displaystyle{' + code + '}']);
@@ -144,65 +146,59 @@ export default {
// Only load Mathjax when needed, we don't want to strain Svg-Edit any more.
// From this point on it is very probable that it will be needed, so load it.
if (mathjaxLoaded === false) {
$(
'<div id="mathjax">' +
'<!-- Here is where MathJax creates the math -->' +
'<div id="mathjax_creator" class="tex2jax_process" style="display:none">' +
'$${}$$' +
'</div>' +
'<div id="mathjax_overlay"></div>' +
'<div id="mathjax_container">' +
'<div id="tool_mathjax_back" class="toolbar_button">' +
'<button id="tool_mathjax_save">OK</button>' +
'<button id="tool_mathjax_cancel">Cancel</button>' +
'</div>' +
'<fieldset>' +
'<legend id="mathjax_legend">Mathematics Editor</legend>' +
'<label>' +
'<span id="mathjax_explication">Please type your mathematics in ' +
'<a href="https://en.wikipedia.org/wiki/Help:' +
'Displaying_a_formula" target="_blank">TeX</a> code.' +
'</span></label>' +
'<textarea id="mathjax_code_textarea" spellcheck="false"></textarea>' +
'</fieldset>' +
'</div>' +
'</div>'
).insertAfter('#svg_prefs').hide();
// Make the MathEditor draggable.
$('#mathjax_container').draggable({
cancel: 'button,fieldset',
containment: 'window'
});
const div = document.createElement('div');
div.id = 'mathjax';
div.innerHTML = '<!-- Here is where MathJax creates the math -->' +
'<div id="mathjax_creator" class="tex2jax_process" style="display:none">' +
'$${}$$' +
'</div>' +
'<div id="mathjax_overlay"></div>' +
'<div id="mathjax_container">' +
'<div id="tool_mathjax_back" class="toolbar_button">' +
'<button id="tool_mathjax_save">OK</button>' +
'<button id="tool_mathjax_cancel">Cancel</button>' +
'</div>' +
'<fieldset>' +
'<legend id="mathjax_legend">Mathematics Editor</legend>' +
'<label>' +
'<span id="mathjax_explication">Please type your mathematics in ' +
'<a href="https://en.wikipedia.org/wiki/Help:' +
'Displaying_a_formula" target="_blank">TeX</a> code.' +
'</span></label>' +
'<textarea id="mathjax_code_textarea" spellcheck="false"></textarea>' +
'</fieldset>' +
'</div>';
$id('svg_prefs').parentNode.insertBefore(div, $id('svg_prefs').nextSibling);
div.style.display = 'none';
// Add functionality and picture to cancel button.
$('#tool_mathjax_cancel').prepend($.getSvgIcon('cancel', true))
.on('click touched', function () {
$('#mathjax').hide();
$id("mathjax").style.display = 'none';
});
// Add functionality and picture to the save button.
$('#tool_mathjax_save').prepend($.getSvgIcon('ok', true))
.on('click touched', function () {
saveMath();
$('#mathjax').hide();
$id("mathjax").style.display = 'none';
});
// MathJax preprocessing has to ignore most of the page.
$('body').addClass('tex2jax_ignore');
document.body.classList.add("tex2jax_ignore");
try {
await import('./mathjax/MathJax.min.js'); // ?config=TeX-AMS-MML_SVG.js');
// When MathJax is loaded get the div where the math will be rendered.
MathJax.Hub.queue.Push(function () {
math = MathJax.Hub.getAllJax('#mathjax_creator')[0];
console.log(math); // eslint-disable-line no-console
console.log(math);
mathjaxLoaded = true;
console.log('MathJax Loaded'); // eslint-disable-line no-console
console.log('MathJax Loaded');
});
} catch (e) {
console.log('Failed loading MathJax.'); // eslint-disable-line no-console
$.alert('Failed loading MathJax. You will not be able to change the mathematics.');
console.log('Failed loading MathJax.');
// eslint-disable-next-line no-alert
alert('Failed loading MathJax. You will not be able to change the mathematics.');
}
}
}
@@ -230,65 +226,63 @@ export default {
locationX = opts.mouse_x / zoom;
locationY = opts.mouse_y / zoom;
$('#mathjax').show();
$id("mathjax").style.display = 'block';
return {started: false}; // Otherwise the last selected object dissapears.
}
return undefined;
},
callback () {
$('<style>').text(
'#mathjax fieldset{' +
'padding: 5px;' +
'margin: 5px;' +
'border: 1px solid #DDD;' +
'}' +
'#mathjax label{' +
'display: block;' +
'margin: .5em;' +
'}' +
'#mathjax legend {' +
'max-width:195px;' +
'}' +
'#mathjax_overlay {' +
'position: absolute;' +
'top: 0;' +
'left: 0;' +
'right: 0;' +
'bottom: 0;' +
'background-color: black;' +
'opacity: 0.6;' +
'z-index: 20000;' +
'}' +
'#mathjax_container {' +
'position: absolute;' +
'top: 50px;' +
'padding: 10px;' +
'background-color: #B0B0B0;' +
'border: 1px outset #777;' +
'opacity: 1.0;' +
'font-family: Verdana, Helvetica, sans-serif;' +
'font-size: .8em;' +
'z-index: 20001;' +
'}' +
'#tool_mathjax_back {' +
'margin-left: 1em;' +
'overflow: auto;' +
'}' +
'#mathjax_legend{' +
'font-weight: bold;' +
'font-size:1.1em;' +
'}' +
'#mathjax_code_textarea {\\n' +
'margin: 5px .7em;' +
'overflow: hidden;' +
'width: 416px;' +
'display: block;' +
'height: 100px;' +
'}'
).appendTo('head');
// Add the MathJax configuration.
// $(mathjaxConfiguration).appendTo('head');
const head = document.head || document.getElementsByTagName('head')[0],
style = document.createElement('style');
style.textContent = '#mathjax fieldset{' +
'padding: 5px;' +
'margin: 5px;' +
'border: 1px solid #DDD;' +
'}' +
'#mathjax label{' +
'display: block;' +
'margin: .5em;' +
'}' +
'#mathjax legend {' +
'max-width:195px;' +
'}' +
'#mathjax_overlay {' +
'position: absolute;' +
'top: 0;' +
'left: 0;' +
'right: 0;' +
'bottom: 0;' +
'background-color: black;' +
'opacity: 0.6;' +
'z-index: 20000;' +
'}' +
'#mathjax_container {' +
'position: absolute;' +
'top: 50px;' +
'padding: 10px;' +
'background-color: #B0B0B0;' +
'border: 1px outset #777;' +
'opacity: 1.0;' +
'font-family: Verdana, Helvetica, sans-serif;' +
'font-size: .8em;' +
'z-index: 20001;' +
'}' +
'#tool_mathjax_back {' +
'margin-left: 1em;' +
'overflow: auto;' +
'}' +
'#mathjax_legend{' +
'font-weight: bold;' +
'font-size:1.1em;' +
'}' +
'#mathjax_code_textarea {\\n' +
'margin: 5px .7em;' +
'overflow: hidden;' +
'width: 416px;' +
'display: block;' +
'height: 100px;' +
'}';
head.appendChild(style);
}
};
}

View File

@@ -1,8 +0,0 @@
export default {
name: 'MathJax',
buttons: [
{
title: 'Mathematik hinzufügen'
}
]
};

View File

@@ -0,0 +1,78 @@
/* globals seAlert */
/**
* @file ext-opensave.js
*
* @license MIT
*
* @copyright 2020 OptimistikSAS
*
*/
/**
* @type {module:svgcanvas.EventHandler}
* @param {external:Window} wind
* @param {module:svgcanvas.SvgCanvas#event:saved} svg The SVG source
* @listens module:svgcanvas.SvgCanvas#event:saved
* @returns {void}
*/
export default {
name: 'opensave',
init ({encode64}) {
const svgEditor = this;
svgEditor.setCustomHandlers({
save (win, svg) {
this.showSaveWarning = false;
// by default, we add the XML prolog back, systems integrating SVG-edit (wikis, CMSs)
// can just provide their own custom save handler and might not want the XML prolog
svg = '<?xml version="1.0"?>\n' + svg;
// Since saving SVGs by opening a new window was removed in Chrome use artificial link-click
// https://stackoverflow.com/questions/45603201/window-is-not-allowed-to-navigate-top-frame-navigations-to-data-urls
const a = document.createElement('a');
a.href = 'data:image/svg+xml;base64,' + encode64(svg);
a.download = 'icon.svg';
a.style.display = 'none';
document.body.append(a); // Need to append for Firefox
a.click();
// Alert will only appear the first time saved OR the
// first time the bug is encountered
const done = this.configObj.pref('save_notice_done');
if (done !== 'all') {
const note = svgEditor.i18next.t('notification.saveFromBrowser', { type: 'SVG'});
this.configObj.pref('save_notice_done', 'all');
if (done !== 'part') {
seAlert(note);
}
}
},
async open () {
const ok = await this.openPrep();
if (ok === 'Cancel') { return; }
this.svgCanvas.clear();
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', (e) => {
// getting a hold of the file reference
const file = e.target.files[0];
// setting up the reader
const reader = new FileReader();
reader.readAsText(file, 'UTF-8');
// here we tell the reader what to do when it's done reading...
reader.addEventListener('load', async (readerEvent) => {
const content = readerEvent.target.result;
await this.loadSvgString(content);
this.updateCanvas();
});
});
input.click();
}
});
}
};

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-len */
/**
* @file ext-overview_window.js
*
@@ -6,9 +7,13 @@
* @copyright 2013 James Sacksteder
*
*/
import { dragmove } from '../../../editor/dragmove/dragmove.js';
export default {
name: 'overview_window',
init ({$, isChrome, isIE}) {
init ({_$, isChrome}) {
const svgEditor = this;
const {$id} = svgEditor.svgCanvas;
const overviewWindowGlobals = {};
// Disabled in Chrome 48-, see https://github.com/SVG-Edit/svgedit/issues/26 and
// https://code.google.com/p/chromium/issues/detail?id=565120.
@@ -41,59 +46,51 @@ export default {
'</div>' +
'</div>' +
'</div>';
$('#sidepanels').append(propsWindowHtml);
// eslint-disable-next-line no-unsanitized/method
$id("sidepanels").insertAdjacentHTML( 'beforeend', propsWindowHtml );
// Define dynamic animation of the view box.
const updateViewBox = function () {
const portHeight = Number.parseFloat($('#workarea').css('height'));
const portWidth = Number.parseFloat($('#workarea').css('width'));
const portX = $('#workarea').scrollLeft();
const portY = $('#workarea').scrollTop();
const windowWidth = Number.parseFloat($('#svgcanvas').css('width'));
const windowHeight = Number.parseFloat($('#svgcanvas').css('height'));
const overviewWidth = $('#overviewMiniView').attr('width');
const overviewHeight = $('#overviewMiniView').attr('height');
const warea = document.getElementById('workarea');
const portHeight = parseFloat(getComputedStyle(warea, null).height.replace("px", ""));
const portWidth = parseFloat(getComputedStyle(warea, null).width.replace("px", ""));
const portX = warea.scrollLeft;
const portY = warea.scrollTop;
const windowWidth = parseFloat(getComputedStyle($id("svgcanvas"), null).width.replace("px", ""));
const windowHeight = parseFloat(getComputedStyle($id("svgcanvas"), null).height.replace("px", ""));
const overviewWidth = parseFloat(getComputedStyle($id("overviewMiniView"), null).width.replace("px", ""));
const overviewHeight = parseFloat(getComputedStyle($id("overviewMiniView"), null).height.replace("px", ""));
const viewBoxX = portX / windowWidth * overviewWidth;
const viewBoxY = portY / windowHeight * overviewHeight;
const viewBoxWidth = portWidth / windowWidth * overviewWidth;
const viewBoxHeight = portHeight / windowHeight * overviewHeight;
$('#overview_window_view_box').css('min-width', viewBoxWidth + 'px');
$('#overview_window_view_box').css('min-height', viewBoxHeight + 'px');
$('#overview_window_view_box').css('top', viewBoxY + 'px');
$('#overview_window_view_box').css('left', viewBoxX + 'px');
$id("overview_window_view_box").style.minWidth = viewBoxWidth + 'px';
$id("overview_window_view_box").style.minHeight = viewBoxHeight + 'px';
$id("overview_window_view_box").style.top = viewBoxY + 'px';
$id("overview_window_view_box").style.left = viewBoxX + 'px';
};
$('#workarea').scroll(function () {
document.getElementById('workarea').addEventListener('scroll', () => {
if (!(overviewWindowGlobals.viewBoxDragging)) {
updateViewBox();
}
});
$('#workarea').resize(updateViewBox);
document.getElementById('workarea').addEventListener('resize', updateViewBox);
updateViewBox();
// Compensate for changes in zoom and canvas size.
const updateViewDimensions = function () {
const viewWidth = $('#svgroot').attr('width');
const viewHeight = $('#svgroot').attr('height');
const viewWidth = parseFloat(getComputedStyle($id("svgroot"), null).width.replace("px", ""));
const viewHeight = parseFloat(getComputedStyle($id("svgroot"), null).height.replace("px", ""));
let viewX = 640;
let viewY = 480;
if (isIE()) {
// This has only been tested with Firefox 10 and IE 9 (without chrome frame).
// I am not sure if if is Firefox or IE that is being non compliant here.
// Either way the one that is noncompliant may become more compliant later.
// TAG:HACK
// TAG:VERSION_DEPENDENT
// TAG:BROWSER_SNIFFING
viewX = 0;
viewY = 0;
}
const viewX = 640;
const viewY = 480;
const svgWidthOld = $('#overviewMiniView').attr('width');
const svgWidthOld = parseFloat(getComputedStyle($id("overviewMiniView"), null).width.replace("px", ""));
const svgHeightNew = viewHeight / viewWidth * svgWidthOld;
$('#overviewMiniView').attr('viewBox', viewX + ' ' + viewY + ' ' + viewWidth + ' ' + viewHeight);
$('#overviewMiniView').attr('height', svgHeightNew);
$id('overviewMiniView').setAttribute('viewBox', viewX + ' ' + viewY + ' ' + viewWidth + ' ' + viewHeight);
$id('overviewMiniView').setAttribute('height', svgHeightNew);
updateViewBox();
};
updateViewDimensions();
@@ -101,33 +98,52 @@ export default {
// Set up the overview window as a controller for the view port.
overviewWindowGlobals.viewBoxDragging = false;
const updateViewPortFromViewBox = function () {
const windowWidth = Number.parseFloat($('#svgcanvas').css('width'));
const windowHeight = Number.parseFloat($('#svgcanvas').css('height'));
const overviewWidth = $('#overviewMiniView').attr('width');
const overviewHeight = $('#overviewMiniView').attr('height');
const viewBoxX = Number.parseFloat($('#overview_window_view_box').css('left'));
const viewBoxY = Number.parseFloat($('#overview_window_view_box').css('top'));
const windowWidth = parseFloat(getComputedStyle($id("svgcanvas"), null).width.replace("px", ""));
const windowHeight = parseFloat(getComputedStyle($id("svgcanvas"), null).height.replace("px", ""));
const overviewWidth = parseFloat(getComputedStyle($id("overviewMiniView"), null).width.replace("px", ""));
const overviewHeight = parseFloat(getComputedStyle($id("overviewMiniView"), null).height.replace("px", ""));
const viewBoxX = parseFloat(getComputedStyle($id("overview_window_view_box"), null).getPropertyValue('left').replace("px", ""));
const viewBoxY = parseFloat(getComputedStyle($id("overview_window_view_box"), null).getPropertyValue('top').replace("px", ""));
const portX = viewBoxX / overviewWidth * windowWidth;
const portY = viewBoxY / overviewHeight * windowHeight;
$('#workarea').scrollLeft(portX);
$('#workarea').scrollTop(portY);
$id('workarea').scrollLeft = portX;
$id('workarea').scrollTop = portY;
};
$('#overview_window_view_box').draggable({
containment: 'parent',
drag: updateViewPortFromViewBox,
start () { overviewWindowGlobals.viewBoxDragging = true; },
stop () { overviewWindowGlobals.viewBoxDragging = false; }
});
$('#overviewMiniView').click(function (evt) {
const onStart = () => {
overviewWindowGlobals.viewBoxDragging = true;
updateViewPortFromViewBox();
};
const onEnd = (el, parent, _x, _y) => {
if((el.offsetLeft + el.offsetWidth) > parseFloat(getComputedStyle(parent, null).width.replace("px", ""))){
el.style.left = (parseFloat(getComputedStyle(parent, null).width.replace("px", "")) - el.offsetWidth) + 'px';
} else if(el.offsetLeft < 0){
el.style.left = "0px"
}
if((el.offsetTop + el.offsetHeight) > parseFloat(getComputedStyle(parent, null).height.replace("px", ""))){
el.style.top = (parseFloat(getComputedStyle(parent, null).height.replace("px", "")) - el.offsetHeight) + 'px';
} else if(el.offsetTop < 0){
el.style.top = "0px"
}
overviewWindowGlobals.viewBoxDragging = false;
updateViewPortFromViewBox();
};
const onDrag = function () {
updateViewPortFromViewBox();
};
const dragElem = document.querySelector("#overview_window_view_box");
const parentElem = document.querySelector("#overviewMiniView");
dragmove(dragElem, dragElem, parentElem, onStart, onEnd, onDrag);
$id("overviewMiniView").addEventListener("click", evt => {
// Firefox doesn't support evt.offsetX and evt.offsetY.
const mouseX = (evt.offsetX || evt.originalEvent.layerX);
const mouseY = (evt.offsetY || evt.originalEvent.layerY);
const overviewWidth = $('#overviewMiniView').attr('width');
const overviewHeight = $('#overviewMiniView').attr('height');
const viewBoxWidth = Number.parseFloat($('#overview_window_view_box').css('min-width'));
const viewBoxHeight = Number.parseFloat($('#overview_window_view_box').css('min-height'));
const overviewWidth = parseFloat(getComputedStyle($id("overviewMiniView"), null).width.replace("px", ""));
const overviewHeight = parseFloat(getComputedStyle($id("overviewMiniView"), null).height.replace("px", ""));
const viewBoxWidth = parseFloat(getComputedStyle($id("overview_window_view_box"), null).getPropertyValue('min-width').replace("px", ""));
const viewBoxHeight = parseFloat(getComputedStyle($id("overview_window_view_box"), null).getPropertyValue('min-height').replace("px", ""));
let viewBoxX = mouseX - 0.5 * viewBoxWidth;
let viewBoxY = mouseY - 0.5 * viewBoxHeight;
@@ -144,9 +160,8 @@ export default {
if (viewBoxY + viewBoxHeight > overviewHeight) {
viewBoxY = overviewHeight - viewBoxHeight;
}
$('#overview_window_view_box').css('top', viewBoxY + 'px');
$('#overview_window_view_box').css('left', viewBoxX + 'px');
$id("overview_window_view_box").style.top = viewBoxY + 'px';
$id("overview_window_view_box").style.left = viewBoxX + 'px';
updateViewPortFromViewBox();
});

View File

@@ -13,6 +13,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
@@ -24,34 +25,42 @@ const loadExtensionTranslation = async function (lang) {
export default {
name: 'panning',
async init ({importLocale}) {
async init() {
const svgEditor = this;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const svgCanvas = svgEditor.canvas;
const buttons = [{
id: 'ext-panning',
icon: 'panning.png',
type: 'mode',
events: {
click () {
svgCanvas.setMode('ext-panning');
}
}
}];
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const {
svgCanvas
} = svgEditor;
const {
$id
} = svgCanvas;
const insertAfter = (referenceNode, newNode) => {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
return {
newUI: true,
name: strings.name,
svgicons: 'ext-panning.xml',
buttons: strings.buttons.map((button, i) => {
return Object.assign(buttons[i], button);
}),
mouseDown () {
callback() {
// Add the button and its handler(s)
const buttonTemplate = document.createElement("template");
buttonTemplate.innerHTML = `
<se-button id="ext-panning" title="Panning" src="./images/panning.svg"></se-button>
`;
insertAfter($id('tool_zoom'), buttonTemplate.content.cloneNode(true));
$id('ext-panning').addEventListener("click", () => {
svgCanvas.setMode('ext-panning');
});
},
mouseDown() {
if (svgCanvas.getMode() === 'ext-panning') {
svgEditor.setPanning(true);
return {started: true};
return {
started: true
};
}
return undefined;
},
mouseUp () {
mouseUp() {
if (svgCanvas.getMode() === 'ext-panning') {
svgEditor.setPanning(false);
return {

View File

@@ -1,8 +0,0 @@
export default {
name: 'Extension Panning',
buttons: [
{
title: 'Fenster verschieben'
}
]
};

View File

@@ -9,6 +9,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
@@ -20,11 +21,11 @@ const loadExtensionTranslation = async function (lang) {
export default {
name: 'placemark',
async init (S) {
async init (_S) {
const svgEditor = this;
const svgCanvas = svgEditor.canvas;
const {svgCanvas} = svgEditor;
const {$id} = svgCanvas;
const addElem = svgCanvas.addSVGElementFromJson;
const {$} = S; // {svgcontent},
let
selElems,
// editingitex = false,
@@ -35,7 +36,7 @@ export default {
// newFOG, newFOGParent, newDef, newImageName, newMaskID,
// undoCommand = 'Not image',
// modeChangeG, ccZoom, wEl, hEl, wOffset, hOffset, ccRgbEl, brushW, brushH;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
const markerTypes = {
nomarker: {},
forwardslash:
@@ -71,7 +72,7 @@ export default {
* @returns {void}
*/
function showPanel (on) {
$('#placemark_panel').toggle(on);
$id('placemark_panel').style.display = (on) ? 'block' : 'none';
}
/**
@@ -103,10 +104,11 @@ export default {
const items = txt.split(';');
selElems.forEach((elem) => {
if (elem && elem.getAttribute('class').includes('placemark')) {
$(elem).children().each((_, i) => {
var elements = elem.children;
Array.prototype.forEach.call(elements, function(i, _){
const [, , type, n] = i.id.split('_');
if (type === 'txt') {
$(i).text(items[n]);
txt.textContent = items[n];
}
});
}
@@ -123,10 +125,11 @@ export default {
font = font.join(' ');
selElems.forEach((elem) => {
if (elem && elem.getAttribute('class').includes('placemark')) {
$(elem).children().each((_, i) => {
var elements = elem.children;
Array.prototype.forEach.call(elements, function(i, _){
const [, , type] = i.id.split('_');
if (type === 'txt') {
$(i).attr({'font-family': font, 'font-size': fontSize});
i.style.cssText = 'font-family:' + font + ';font-size:'+fontSize+';';
}
});
}
@@ -140,13 +143,12 @@ export default {
function addMarker (id, val) {
let marker = svgCanvas.getElem(id);
if (marker) { return undefined; }
// console.log(id);
if (val === '' || val === 'nomarker') { return undefined; }
const color = svgCanvas.getColor('stroke');
// NOTE: Safari didn't like a negative value in viewBox
// so we use a standardized 0 0 100 100
// with 50 50 being mapped to the marker position
const scale = 2;// parseFloat($('#marker_size').val());
const scale = 2;
const strokeWidth = 10;
let refX = 50;
const refY = 50;
@@ -199,7 +201,7 @@ export default {
function setMarker (el, val) {
const markerName = 'marker-start';
const marker = getLinked(el, markerName);
if (marker) { $(marker).remove(); }
if (marker) { marker.remove(); }
el.removeAttribute(markerName);
if (val === 'nomarker') {
svgCanvas.call('changed', [el]);
@@ -248,7 +250,7 @@ export default {
const len = el.id.length;
const linkid = url.substr(-len - 1, len);
if (el.id !== linkid) {
const val = $('#placemark_marker').attr('value') || 'leftarrow';
const val = $id('placemark_marker').getAttribute('value') || 'leftarrow';
addMarker(id, val);
svgCanvas.changeSelectedAttribute(markerName, 'url(#' + id + ')');
svgCanvas.call('changed', selElems);
@@ -259,11 +261,11 @@ export default {
* @param {Event} ev
* @returns {void}
*/
function setArrowFromButton (ev) {
function setArrowFromButton (_ev) {
const parts = this.id.split('_');
let val = parts[2];
if (parts[3]) { val += '_' + parts[3]; }
$('#placemark_marker').attr('value', val);
$id('placemark_marker').setAttribute('value', val);
}
/**
@@ -356,7 +358,7 @@ export default {
return Object.assign(contextTools[i], contextTool);
}),
callback () {
$('#placemark_panel').hide();
$id("placemark_panel").style.display = 'none';
// const endChanges = function(){};
},
mouseDown (opts) {
@@ -367,8 +369,8 @@ export default {
if (svgCanvas.getMode() === 'placemark') {
started = true;
const id = svgCanvas.getNextId();
const items = $('#placemarkText').val().split(';');
let font = $('#placemarkFont').val().split(' ');
const items = $id('placemarkText').value.split(';');
let font = $id('placemarkFont').value.split(' ');
const fontSize = Number.parseInt(font.pop());
font = font.join(' ');
const x0 = opts.start_x + 10, y0 = opts.start_y + 10;
@@ -452,7 +454,7 @@ export default {
});
setMarker(
newPM.firstElementChild,
$('#placemark_marker').attr('value') || 'leftarrow'
$id('placemark_marker').getAttribute('value') || 'leftarrow'
);
return {
started: true
@@ -467,11 +469,16 @@ export default {
if (svgCanvas.getMode() === 'placemark') {
const x = opts.mouse_x / svgCanvas.getZoom();
const y = opts.mouse_y / svgCanvas.getZoom();
const {fontSize, maxlen, lines, px, py} = $(newPM).attr(
['fontSize', 'maxlen', 'lines', 'px', 'py']
);
$(newPM).attr({x, y});
$(newPM).children().each((_, i) => {
const fontSize = newPM.getAttribute('fontSize');
const maxlen = newPM.getAttribute('maxlen');
const lines = newPM.getAttribute('lines');
const px = newPM.getAttribute('px');
const py = newPM.getAttribute('py');
newPM.setAttribute('x', x);
newPM.setAttribute('y', y);
const elements = newPM.children;
Array.prototype.forEach.call(elements, function(i, _){
const [, , type, n] = i.id.split('_');
const y0 = y + (fontSize + 6) * n,
x0 = x + maxlen * fontSize * 0.5 + fontSize;
@@ -508,7 +515,10 @@ export default {
},
mouseUp () {
if (svgCanvas.getMode() === 'placemark') {
const {x, y, px, py} = $(newPM).attr(['x', 'y', 'px', 'py']);
const x = newPM.getAttribute('x');
const y = newPM.getAttribute('y');
const px = newPM.getAttribute('px');
const py = newPM.getAttribute('py');
return {
keep: (x != px && y != py), // eslint-disable-line eqeqeq
element: newPM
@@ -522,16 +532,17 @@ export default {
selElems.forEach((elem) => {
if (elem && elem.getAttribute('class').includes('placemark')) {
const txt = [];
$(elem).children().each((n, i) => {
const elements = elem.children;
Array.prototype.forEach.call(elements, function(i){
const [, , type] = i.id.split('_');
if (type === 'txt') {
$('#placemarkFont').val(
$id('placemarkFont').value = (
i.getAttribute('font-family') + ' ' + i.getAttribute('font-size')
);
txt.push($(i).text());
txt.push(i.textContent);
}
});
$('#placemarkText').val(txt.join(';'));
$id('placemarkText').value = txt.join(';');
showPanel(true);
} else {
showPanel(false);

View File

@@ -1,40 +0,0 @@
export default {
name: 'placemark',
langList: [
{id: 'nomarker', title: 'Keine Markierung'},
{id: 'leftarrow', title: 'Pfeil links'},
{id: 'rightarrow', title: 'Pfeil rechts'},
{id: 'forwardslash', title: 'Schrägstrich'},
{id: 'reverseslash', title: 'Umgekehrter Schrägstrich'},
{id: 'verticalslash', title: 'Vertikaler Strich'},
{id: 'box', title: 'Box'},
{id: 'star', title: 'Stern'},
{id: 'xmark', title: 'X'},
{id: 'triangle', title: 'Dreieck'},
{id: 'mcircle', title: 'Kreis'},
{id: 'leftarrow_o', title: 'Offener Pfeil links'},
{id: 'rightarrow_o', title: 'Offener Pfeil rechts'},
{id: 'box_o', title: 'Offene Box'},
{id: 'star_o', title: 'Offener Stern'},
{id: 'triangle_o', title: 'Offenes Dreieck'},
{id: 'mcircle_o', title: 'Offener Kreis'}
],
buttons: [
{
title: 'Placemark Werkzeug'
}
],
contextTools: [
{
title: 'Typ der Placemark auswählen'
},
{
title: 'Text (mehrere Texte mit Semikolon getrennt)',
label: 'Text'
},
{
title: 'Schriftart für den Text',
label: ''
}
]
};

View File

@@ -9,6 +9,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
@@ -20,61 +21,29 @@ const loadExtensionTranslation = async function (lang) {
export default {
name: 'polygon',
async init (S) {
async init (_S) {
const svgEditor = this;
const svgCanvas = svgEditor.canvas;
const {$} = S, // {svgcontent}
// addElem = svgCanvas.addSVGElementFromJson,
editingitex = false;
const strings = await loadExtensionTranslation(svgEditor.curPrefs.lang);
let selElems,
// svgdoc = S.svgroot.parentNode.ownerDocument,
// newFOG, newFOGParent, newDef, newImageName, newMaskID, modeChangeG,
// edg = 0,
// undoCommand = 'Not image';
started, newFO;
// const ccZoom;
// const wEl, hEl;
// const wOffset, hOffset;
// const ccRBG;
// const ccOpacity;
// const brushW, brushH;
// const ccDebug = document.getElementById('debugpanel');
/* 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 {svgCanvas} = svgEditor;
const {$id} = svgCanvas;
// const editingitex = false;
const strings = await loadExtensionTranslation(svgEditor.configObj.pref('lang'));
let selElems;
let started;
let newFO;
/**
* @param {boolean} on
* @returns {void}
*/
function showPanel (on) {
let fcRules = $('#fc_rules');
if (!fcRules.length) {
fcRules = $('<style id="fc_rules"></style>').appendTo('head');
}
fcRules.text(!on ? '' : ' #tool_topath { display: none !important; }');
$('#polygon_panel').toggle(on);
}
/*
function toggleSourceButtons(on){
$('#tool_source_save, #tool_source_cancel').toggle(!on);
$('#polygon_save, #polygon_cancel').toggle(on);
}
*/
const showPanel = (on) => {
$id('polygon_panel').style.display = (on) ? 'block' : 'none';
}
/**
* @param {string} attr
* @param {string|Float} val
* @returns {void}
*/
function setAttr (attr, val) {
const setAttr = (attr, val) => {
svgCanvas.changeSelectedAttribute(attr, val);
svgCanvas.call('changed', selElems);
}
@@ -83,134 +52,50 @@ export default {
* @param {Float} n
* @returns {Float}
*/
function cot (n) {
return 1 / Math.tan(n);
}
const cot = (n) => (1 / Math.tan(n));
/**
* @param {Float} n
* @returns {Float}
*/
function sec (n) {
return 1 / Math.cos(n);
}
const sec = (n) => (1 / Math.cos(n));
/**
* Obtained from http://code.google.com/p/passenger-top/source/browse/instiki/public/svg-edit/editor/extensions/ext-itex.js?r=3
* This function sets the content of of the currently-selected foreignObject element,
* based on the itex contained in string.
* @param {string} tex The itex text.
* @returns {boolean} This function returns false if the set was unsuccessful, true otherwise.
*/
/*
function setItexString(tex) {
const mathns = 'http://www.w3.org/1998/Math/MathML',
xmlnsns = 'http://www.w3.org/2000/xmlns/',
ajaxEndpoint = '../../itex';
const elt = selElems[0];
try {
const math = svgdoc.createElementNS(mathns, 'math');
math.setAttributeNS(xmlnsns, 'xmlns', mathns);
math.setAttribute('display', 'inline');
const semantics = document.createElementNS(mathns, 'semantics');
const annotation = document.createElementNS(mathns, 'annotation');
annotation.setAttribute('encoding', 'application/x-tex');
annotation.textContent = tex;
const mrow = document.createElementNS(mathns, 'mrow');
semantics.append(mrow, annotation);
math.append(semantics);
// make an AJAX request to the server, to get the MathML
$.post(ajaxEndpoint, {tex, display: 'inline'}, function(data){
const children = data.documentElement.childNodes;
while (children.length > 0) {
mrow.append(svgdoc.adoptNode(children[0], true));
}
svgCanvas.sanitizeSvg(math);
svgCanvas.call('changed', [elt]);
});
elt.firstChild.replaceWith(math);
svgCanvas.call('changed', [elt]);
svgCanvas.clearSelection();
} catch(e) {
console.log(e);
return false;
}
return true;
}
*/
const buttons = [{
id: 'tool_polygon',
icon: 'polygon.png',
type: 'mode',
position: 11,
events: {
click () {
svgCanvas.setMode('polygon');
showPanel(true);
}
}
}];
const contextTools = [{
type: 'input',
panel: 'polygon_panel',
id: 'polySides',
size: 3,
defval: 5,
events: {
change () {
setAttr('sides', this.value);
}
}
}];
return {
name: strings.name,
svgicons: 'polygon-icons.svg',
buttons: strings.buttons.map((button, i) => {
return Object.assign(buttons[i], button);
}),
context_tools: strings.contextTools.map((contextTool, i) => {
return Object.assign(contextTools[i], contextTool);
}),
// The callback should be used to load the DOM with the appropriate UI items
callback () {
$('#polygon_panel').hide();
// Add the button and its handler(s)
// Note: the star extension may also add the same flying button so we check first
if ($id('tools_polygon') === null) {
const buttonTemplate = document.createElement("template");
buttonTemplate.innerHTML = `
<se-flyingbutton id="tools_polygon" title="Polygone/Star Tool">
<se-button id="tool_polygon" title="Polygon Tool" src="./images/polygon.svg"></se-button>
<se-button id="tool_star" title="Star Tool" src="./images/star.svg"></se-button>
</se-flyingbutton>
`
$id('tools_left').append(buttonTemplate.content.cloneNode(true));
}
$id('tool_polygon').addEventListener("click", () => {
if (this.leftPanel.updateLeftPanel('tool_polygon')) {
svgCanvas.setMode('polygon');
showPanel(true);
}
});
const endChanges = function () {
// Todo: Missing?
};
// 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', 'polygon_save'
).unbind().appendTo(
'#tool_source_back'
).click(function () {
if (!editingitex) {
return;
}
// Todo: Uncomment the setItexString() function above and handle ajaxEndpoint?
/*
if (!setItexString($('#svg_source_textarea').val())) {
const ok = await $.confirm('Errors found. Revert to original?', function (ok) {
if (!ok) {
return false;
}
endChanges();
} else { */
endChanges();
// }
// setSelectMode();
});
/* const cancel = */ $('#tool_source_cancel').clone().hide().attr(
'id', 'polygon_cancel'
).unbind().appendTo('#tool_source_back').click(function () {
endChanges();
});
}, 3000);
// Add the context panel and its handler(s)
const panelTemplate = document.createElement("template");
panelTemplate.innerHTML = `
<div id="polygon_panel">
<se-spin-input size="3" id="polySides" min=1 step=1 value=5 label="sides">
</se-spin-input>
</div>
`
$id('tools_top').appendChild(panelTemplate.content.cloneNode(true));
$id("polygon_panel").style.display = 'none';
$id("polySides").addEventListener("change", (event) => {
setAttr('sides', event.target.value);
});
},
mouseDown (opts) {
if (svgCanvas.getMode() !== 'polygon') {
@@ -249,12 +134,18 @@ export default {
if (!started || svgCanvas.getMode() !== 'polygon') {
return undefined;
}
// const e = opts.event;
const c = $(newFO).attr(['cx', 'cy', 'sides', 'orient', 'fill', 'strokecolor', 'strokeWidth']);
const cx = Number(newFO.getAttribute('cx'));
const cy = Number(newFO.getAttribute('cy'));
const sides = Number(newFO.getAttribute('sides'));
// const orient = newFO.getAttribute('orient');
const fill = newFO.getAttribute('fill');
const strokecolor = newFO.getAttribute('strokecolor');
const strokeWidth = Number(newFO.getAttribute('strokeWidth'));
let x = opts.mouse_x;
let y = opts.mouse_y;
const {cx, cy, fill, strokecolor, strokeWidth, sides} = c, // {orient} = c,
edg = (Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))) / 1.5;
const edg = (Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))) / 1.5;
newFO.setAttribute('edge', edg);
const inradius = (edg / 2) * cot(Math.PI / sides);
@@ -273,21 +164,17 @@ export default {
newFO.setAttribute('fill', fill);
newFO.setAttribute('stroke', strokecolor);
newFO.setAttribute('stroke-width', strokeWidth);
// newFO.setAttribute('transform', 'rotate(-90)');
// const shape = newFO.getAttribute('shape');
// newFO.append(poly);
// DrawPoly(cx, cy, sides, edg, orient);
return {
started: true
};
},
mouseUp (opts) {
mouseUp () {
if (svgCanvas.getMode() !== 'polygon') {
return undefined;
}
const attrs = $(newFO).attr('edge');
const keep = (attrs.edge !== '0');
const edge = newFO.getAttribute('edge');
const keep = (edge !== '0');
// svgCanvas.addToSelection([newFO], true);
return {
keep,
@@ -303,7 +190,7 @@ export default {
const elem = selElems[i];
if (elem && elem.getAttribute('shape') === 'regularPoly') {
if (opts.selectedElement && !opts.multiselected) {
$('#polySides').val(elem.getAttribute('sides'));
$id('polySides').value = elem.getAttribute('sides');
showPanel(true);
} else {
@@ -314,7 +201,7 @@ export default {
}
}
},
elementChanged (opts) {
elementChanged () {
// const elem = opts.elems[0];
}
};

View File

@@ -1,14 +0,0 @@
export default {
name: 'polygon',
buttons: [
{
title: 'Polygon Werkzeug'
}
],
contextTools: [
{
title: 'Anzahl der Seiten',
label: 'Seiten'
}
]
};

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