diff --git a/src/editor/components/index.js b/src/editor/components/index.js index 38adf0bb..8f16e993 100644 --- a/src/editor/components/index.js +++ b/src/editor/components/index.js @@ -1,10 +1,12 @@ import './seButton.js'; import './seFlyingButton.js'; import './seExplorerButton.js'; -import './seDropdown.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'; diff --git a/src/editor/components/seList.js b/src/editor/components/seList.js new file mode 100644 index 00000000..fbf3e73d --- /dev/null +++ b/src/editor/components/seList.js @@ -0,0 +1,87 @@ +/* eslint-disable node/no-unpublished-import */ +import 'elix/define/DropdownList.js'; + +const template = document.createElement('template'); +template.innerHTML = ` + + + + + + +`; +/** + * @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.appendChild(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']; + } + + /** + * @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; + 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 connectedCallback + * @returns {void} + */ + connectedCallback () { + this.$dropdown.addEventListener('change', (e) => { + e.preventDefault(); + const selectedItem = e?.detail?.closeResult; + if (selectedItem !== undefined && selectedItem?.id !== undefined) { + document.getElementById(selectedItem.id).click(); + } + }); + } +} + +// Register +customElements.define('se-list', SeList); diff --git a/src/editor/components/seListItem.js b/src/editor/components/seListItem.js new file mode 100644 index 00000000..f460a410 --- /dev/null +++ b/src/editor/components/seListItem.js @@ -0,0 +1,71 @@ +/* eslint-disable node/no-unpublished-import */ +import 'elix/define/Option.js'; + +const template = document.createElement('template'); +template.innerHTML = ` + + + + +`; +/** + * @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.appendChild(template.content.cloneNode(true)); + this.$menuitem = this._shadowRoot.querySelector('elix-menu-item'); + } + /** + * @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; + 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); diff --git a/src/editor/components/seZoom.js b/src/editor/components/seZoom.js new file mode 100644 index 00000000..7f5602a5 --- /dev/null +++ b/src/editor/components/seZoom.js @@ -0,0 +1,179 @@ +/* eslint-disable node/no-unpublished-import */ +import ListComboBox from 'elix/define/ListComboBox.js'; +import NumberSpinBox from 'elix/define/NumberSpinBox.js'; +// import Input from 'elix/src/base/Input.js'; +import * as internal from 'elix/src/base/internal.js'; +import {templateFrom, fragmentFrom} from 'elix/src/core/htmlLiterals.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` + icon + `.cloneNode(true)); + // change the style so it fits in our toolbar + result.content.append( + templateFrom.html` + + `.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; + } +*/ diff --git a/src/editor/dialogs/index.js b/src/editor/dialogs/index.js index 5e22a5af..8201da2a 100644 --- a/src/editor/dialogs/index.js +++ b/src/editor/dialogs/index.js @@ -4,3 +4,7 @@ import './svgSourceDialog.js'; import './cmenuDialog.js'; import './cmenuLayersDialog.js'; import './seSelectDialog.js'; +import './seConfirmDialog.js'; +import './sePromptDialog.js'; +import './seAlertDialog.js'; +import './storageDialog.js'; diff --git a/src/editor/dialogs/seAlertDialog.js b/src/editor/dialogs/seAlertDialog.js new file mode 100644 index 00000000..2d178ee8 --- /dev/null +++ b/src/editor/dialogs/seAlertDialog.js @@ -0,0 +1,11 @@ +// eslint-disable-next-line node/no-unpublished-import +import AlertDialog from 'elix/define/AlertDialog.js'; + +const dialog = new AlertDialog(); +const seAlert = (type, text) => { + dialog.textContent = text; + dialog.choices = (type === 'alert') ? ['Ok'] : ['Cancel']; + dialog.open(); +}; + +window.seAlert = seAlert; diff --git a/src/editor/dialogs/seConfirmDialog.js b/src/editor/dialogs/seConfirmDialog.js new file mode 100644 index 00000000..b2502ff1 --- /dev/null +++ b/src/editor/dialogs/seConfirmDialog.js @@ -0,0 +1,13 @@ +// eslint-disable-next-line node/no-unpublished-import +import AlertDialog from 'elix/define/AlertDialog.js'; + +const dialog = new AlertDialog(); +const seConfirm = async (text, choices) => { + dialog.textContent = text; + dialog.choices = (choices === undefined) ? ['Ok', 'Cancel'] : choices; + dialog.open(); + const response = await dialog.whenClosed(); + return response.choice; +}; + +window.seConfirm = seConfirm; diff --git a/src/editor/dialogs/sePromptDialog.js b/src/editor/dialogs/sePromptDialog.js new file mode 100644 index 00000000..10fdf802 --- /dev/null +++ b/src/editor/dialogs/sePromptDialog.js @@ -0,0 +1,83 @@ +// eslint-disable-next-line node/no-unpublished-import +import AlertDialog from 'elix/define/AlertDialog.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 AlertDialog(); + } + /** + * @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: + console.error('unkonw 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); diff --git a/src/editor/dialogs/storageDialog.js b/src/editor/dialogs/storageDialog.js new file mode 100644 index 00000000..32ee919e --- /dev/null +++ b/src/editor/dialogs/storageDialog.js @@ -0,0 +1,166 @@ +/* eslint-disable node/no-unpublished-import */ +import 'elix/define/Dialog.js'; + +const template = document.createElement('template'); +template.innerHTML = ` + + +
+
+
+

+ By default and where supported, SVG-Edit can store your editor preferences and SVG content locally on your machine so you do not need to add these back each time you load SVG-Edit. If, for privacy reasons, you do not wish to store this information on your machine, you can change away from the default option below. +

+ + +
+
+ + +
+
+
+`; +/** + * @class SeStorageDialog + */ +export class SeStorageDialog extends HTMLElement { + /** + * @function constructor + */ + constructor () { + super(); + // create the shadowDom and insert the template + this._shadowRoot = this.attachShadow({mode: 'open'}); + this._shadowRoot.appendChild(template.content.cloneNode(true)); + this.$dialog = this._shadowRoot.querySelector('#dialog_box'); + this.$storage = this._shadowRoot.querySelector('#js-storage'); + this.$okBtn = this._shadowRoot.querySelector('#storage_ok'); + this.$cancelBtn = this._shadowRoot.querySelector('#storage_cancel'); + this.$storageInput = this._shadowRoot.querySelector('#se-storage-pref'); + this.$rememberInput = this._shadowRoot.querySelector('#se-remember'); + } + /** + * @function observedAttributes + * @returns {any} observed + */ + static get observedAttributes () { + return ['dialog', 'storage']; + } + /** + * @function attributeChangedCallback + * @param {string} name + * @param {string} oldValue + * @param {string} newValue + * @returns {void} + */ + attributeChangedCallback (name, oldValue, newValue) { + switch (name) { + case 'dialog': + if (newValue === 'open') { + this.$dialog.open(); + } else { + this.$dialog.close(); + } + break; + case 'storage': + if (newValue === 'true') { + this.$storageInput.options[0].disabled = false; + } else { + this.$storageInput.options[0].disabled = true; + } + 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 () { + const onSubmitHandler = (e, action) => { + const triggerEvent = new CustomEvent('change', {detail: { + trigger: action, + select: this.$storageInput.value, + checkbox: this.$rememberInput.checked + }}); + this.dispatchEvent(triggerEvent); + }; + this.$okBtn.addEventListener('click', (evt) => onSubmitHandler(evt, 'ok')); + this.$cancelBtn.addEventListener('click', (evt) => onSubmitHandler(evt, 'cancel')); + } +} + +// Register +customElements.define('se-storage-dialog', SeStorageDialog); diff --git a/src/editor/extensions/ext-storage/ext-storage.js b/src/editor/extensions/ext-storage/ext-storage.js index 13d80719..6a69fc01 100644 --- a/src/editor/extensions/ext-storage/ext-storage.js +++ b/src/editor/extensions/ext-storage/ext-storage.js @@ -1,4 +1,3 @@ -/* globals seSelect */ /** * @file ext-storage.js * @@ -44,7 +43,6 @@ export default { // to change, set the "emptyStorageOnDecline" config setting to true // in svgedit-config-iife.js/svgedit-config-es.js. const { - emptyStorageOnDecline, // When the code in svg-editor.js prevents local storage on load per // user request, we also prevent storing on unload here so as to // avoid third-party sites making XSRF requests or providing links @@ -56,31 +54,8 @@ export default { // the "noStorageOnLoad" config setting to true in svgedit-config-*.js. noStorageOnLoad, forceStorage - } = svgEditor.configObj.curConfig; - const {storage, updateCanvas} = svgEditor; - - /** - * Replace `storagePrompt` parameter within URL. - * @param {string} val - * @returns {void} - * @todo Replace the string manipulation with `searchParams.set` - */ - function replaceStoragePrompt (val) { - val = val ? 'storagePrompt=' + val : ''; - const loc = top.location; // Allow this to work with the embedded editor as well - if (loc.href.includes('storagePrompt=')) { - /* - loc.href = loc.href.replace(/(?[&?])storagePrompt=[^&]*(?&?)/, function (n0, sep, amp) { - return (val ? sep : '') + val + (!val && amp ? sep : (amp || '')); - }); - */ - loc.href = loc.href.replace(/([&?])storagePrompt=[^&]*(&?)/, function (n0, n1, amp) { - return (val ? n1 : '') + val + (!val && amp ? n1 : (amp || '')); - }); - } else { - loc.href += (loc.href.includes('?') ? '&' : '?') + val; - } - } + } = svgEditor.curConfig; + const {storage} = svgEditor; /** * Sets SVG content as a string with "svgedit-" and the current @@ -99,40 +74,6 @@ export default { } } - /** - * Set the cookie to expire. - * @param {string} cookie - * @returns {void} - */ - function expireCookie (cookie) { - document.cookie = encodeURIComponent(cookie) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT'; - } - - /** - * Expire the storage cookie. - * @returns {void} - */ - function removeStoragePrefCookie () { - expireCookie('svgeditstore'); - } - - /** - * Empties storage for each of the current preferences. - * @returns {void} - */ - function emptyStorage () { - setSVGContentStorage(''); - Object.keys(svgEditor.curPrefs).forEach((name) => { - name = 'svg-edit-' + name; - if (storage) { - storage.removeItem(name); - } - expireCookie(name); - }); - } - - // emptyStorage(); - /** * Listen for unloading: If and only if opted in by the user, set the content * document and preferences into storage: @@ -180,12 +121,15 @@ export default { name: 'storage', async langReady ({lang}) { const storagePrompt = new URL(top.location).searchParams.get('storagePrompt'); + // eslint-disable-next-line no-unused-vars const strings = await loadExtensionTranslation(svgEditor.pref('lang')); + /* const { - message /* , storagePrefsAndContent, storagePrefsOnly, + message, storagePrefsAndContent, storagePrefsOnly, storagePrefs, storageNoPrefsOrContent, storageNoPrefs, - rememberLabel, rememberTooltip */ + rememberLabel, rememberTooltip } = strings; + */ // No need to run this one-time dialog again just because the user // changes the language @@ -214,83 +158,13 @@ export default { ) // ...then show the storage prompt. )) { - /* - const options = []; - if (storage) { - options.unshift( - {value: 'prefsAndContent', text: storagePrefsAndContent}, - {value: 'prefsOnly', text: storagePrefsOnly}, - {value: 'noPrefsOrContent', text: storageNoPrefsOrContent} - ); - } else { - options.unshift( - {value: 'prefsOnly', text: storagePrefs}, - {value: 'noPrefsOrContent', text: storageNoPrefs} - ); - } - */ - const options = storage ? ['prefsAndContent', 'prefsOnly', 'noPrefsOrContent'] : ['prefsOnly', 'noPrefsOrContent']; - + const options = Boolean(storage); // Open select-with-checkbox dialog // From svg-editor.js svgEditor.storagePromptState = 'waiting'; - /* JFH !!!!! - const {response: pref, checked} = await $.select( - message, - options, - null, - null, - { - label: rememberLabel, - checked: true, - tooltip: rememberTooltip - } - ); - */ - const pref = await seSelect(message, options); - if (pref && pref !== 'noPrefsOrContent') { - // Regardless of whether the user opted - // to remember the choice (and move to a URL which won't - // ask them again), we have to assume the user - // doesn't even want to remember their not wanting - // storage, so we don't set the cookie or continue on with - // setting storage on beforeunload - document.cookie = 'svgeditstore=' + encodeURIComponent(pref) + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; // 'prefsAndContent' | 'prefsOnly' - // If the URL was configured to always insist on a prompt, if - // the user does indicate a wish to store their info, we - // don't want ask them again upon page refresh so move - // them instead to a URL which does not always prompt - if (storagePrompt === 'true' /* && checked */) { - replaceStoragePrompt(); - return; - } - } else { // The user does not wish storage (or cancelled, which we treat equivalently) - removeStoragePrefCookie(); - if (pref && // If the user explicitly expresses wish for no storage - emptyStorageOnDecline - ) { - emptyStorage(); - } - if (pref /* && checked */) { - // Open a URL which won't set storage and won't prompt user about storage - replaceStoragePrompt('false'); - return; - } - } - - // It should be enough to (conditionally) add to storage on - // beforeunload, but if we wished to update immediately, - // we might wish to try setting: - // svgEditor.setConfig({noStorageOnLoad: true}); - // and then call: - // svgEditor.loadContentAndPrefs(); - - // We don't check for noStorageOnLoad here because - // the prompt gives the user the option to store data - setupBeforeUnloadListener(); - - svgEditor.storagePromptState = 'closed'; - updateCanvas(true); + const $storageDialog = document.getElementById('se-storage-dialog'); + $storageDialog.setAttribute('dialog', 'open'); + $storageDialog.setAttribute('storage', options); } else if (!noStorageOnLoad || forceStorage) { setupBeforeUnloadListener(); } diff --git a/src/editor/images/linejoin_mitter.svg b/src/editor/images/linejoin_miter.svg similarity index 100% rename from src/editor/images/linejoin_mitter.svg rename to src/editor/images/linejoin_miter.svg diff --git a/src/editor/index.html b/src/editor/index.html index d0361171..7fdf03cb 100644 --- a/src/editor/index.html +++ b/src/editor/index.html @@ -127,15 +127,23 @@ title="Change rotation angle"> + + Align Left + Align Center + Align Right + Align Top + Align Middle + Align Bottom +
- - - - + + + +
@@ -154,33 +162,30 @@ - + + selected objects + largest object + smallest object + page +
- - + +
- - + +
- - + +
- +
- - - - + + + +
- - + +
- - - - -
-
- - - - + + + + + + + +
@@ -236,25 +239,16 @@
-
- - -
+ +
Sans-serif
+
Serif
+
Cursive
+
Fantasy
+
Monospace
+
Courier
+
Helvetica
+
Times
+
@@ -286,8 +280,8 @@
- - + + - - - - - - - - - -
- - -
+ + + ... + - - + - . + - .. + + + + + + + + + + + + + 0% + 25% + 50% + 75% + 100% + +
- -
diff --git a/src/editor/svgedit.css b/src/editor/svgedit.css index 79282d4f..ce37d056 100644 --- a/src/editor/svgedit.css +++ b/src/editor/svgedit.css @@ -379,25 +379,6 @@ hr { /*—————————————————————————————*/ -.tool_button:hover, -.push_button:hover, -.buttonup:hover, -.buttondown, -.tool_button_current, -.push_button_pressed -{ - background-color: #ffc !important; -} - -.tool_button_current, -.push_button_pressed, -.buttondown { - background-color: #f4e284 !important; - -webkit-box-shadow: inset 1px 1px 2px rgba(0,0,0,0.4), 1px 1px 0 white !important; - -moz-box-shadow: inset 1px 1px 2px rgba(0,0,0,0.4), 1px 1px 0 white !important; - box-shadow: inset 1px 1px 2px rgba(0,0,0,0.4), 1px 1px 0 white !important; -} - #tools_top { position: absolute; left: 108px; @@ -405,7 +386,6 @@ hr { top: 2px; height: 40px; border-bottom: none; - overflow: auto; } #tools_top .tool_sep { @@ -515,12 +495,6 @@ input[type=text] { padding: 2px; } -#tools_left .tool_button, -#tools_left .tool_button_current { - position: relative; - z-index: 11; -} - .dropdown { position: relative; } @@ -592,21 +566,6 @@ input[type=text] { margin-right: 0; } -.tool_button, -.push_button, -.tool_button_current, -.push_button_pressed -{ - height: 24px; - width: 24px; - margin: 2px 2px 4px 2px; - padding: 3px; - box-shadow: inset 1px 1px 2px white, 1px 1px 1px rgba(0,0,0,0.3); - background-color: #E8E8E8; - cursor: pointer; - border-radius: 3px; -} - #main_menu li#tool_open, #main_menu li#tool_import { position: relative; overflow: hidden; @@ -683,10 +642,6 @@ input[type=text] { float: right; } -.dropdown li.tool_button { - width: 24px; -} - #stroke_expand { width: 0; overflow: hidden;