diff --git a/i18n/en.json b/i18n/en.json index 530b9a5..59a42a6 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,36 +1,43 @@ { "@metadata": { "authors": [ "Lucas Werkmeister" ] }, "tool-name": "Wikidata Image Positions", "skip-to-main-content": "Skip to main content", "nav-documentation": "Documentation", "nav-toolforge": "Wikimedia Toolforge", "nav-source-code": "Source code", "nav-login": "Log in", "nav-logged-in": "{{GENDER:$2|Logged in}} as $1.", "index-paragraph-1": "This tool shows $1 qualifiers on $2 or $3 statements as areas on the relevant image.", "index-paragraph-2": "For statements on [$1 Wikidata] items, they are shown on the item’s $2 (or other property). For statements on [$3 Structured Data on Commons] files, they are shown on the file itself.", "index-heading-by-item-and-property": "By item and property", "index-label-item-id": "Item ID", "index-placeholder-item-id": "$1 or $2 or similar", "index-label-property-id-optional": "Property ID (optional)", "index-button-load": "Load", "index-button-preview-iiif": "Preview IIIF", "index-button-iiif-manifest": "IIIF Manifest", "index-heading-by-file": "By file", "index-label-file-title": "File title", "index-placeholder-file-title": "$1 or $2 or $3 or similar", "index-heading-by-iiif": "By IIIF region and property", "index-label-iiif-region": "IIIF region", "image-scale": "Image scale: ", "image-depicted-without-region": "Depicted with no region specified:", "image-named-places-on-map-without-region": "Named places on map with no region specified:", + "image-button-add-region": "add region", + "image-button-use-region": "use this region", + "image-button-cancel": "cancel", + "image-button-editing-statement": "editing statement…", + "image-button-edit-region": "Edit a region", + "image-button-select-region": "Select (click) a region to edit", + "image-button-loading": "loading…", "alert-not-logged-in": "You are not logged in, so you can only view existing regions and statements. To add new statements or regions or edit existing ones, [$1 log in].", "alert-noscript": "JavaScript is disabled, so you can only view existing regions and statements. To add new statements or regions or edit existing ones, enable JavaScript.", "settings": "Settings", "settings-label-interface-language-code": "User interface language", "settings-save": "Save" } diff --git a/i18n/qqq.json b/i18n/qqq.json index c2a798e..86c6233 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -1,38 +1,45 @@ { "@metadata": { "authors": [ "Amire80", "Lucas Werkmeister", "Siebrand" ] }, "tool-name": "The name of the tool. This message is currently used for the page title, navigation bar / menu, and heading on the index page. Those uses could later be split up into separate messages if necessary.", "skip-to-main-content": "Text for a link that allows users of assistive technologies to skip unimportant parts of the page (the navigation section).", "nav-documentation": "Text for a link to the tool’s documentation in its navigation bar / menu.\n{{Identical|Documentation}}", "nav-toolforge": "Text for a link to the tool’s hosting platform, Wikimedia Toolforge, in its navigation bar / menu.", "nav-source-code": "Text for a link to the tool’s source code in its navigation bar / menu.", "nav-login": "Text for a link to log into the tool in its navigation bar / menu.\n{{Identical|Log in}}", "nav-logged-in": "Indicator in the navbar that the user is logged in.\n\nParameters:\n* $1 – the user name as a link to the user page.\n* $2 – the user name as plain text. Can be used with the GENDER magic word. If GENDER is not needd, the translation can leave out $2 completely.", "index-paragraph-1": "First paragraph on the tool’s index page, describing the tool.\n\nParameters:\n* $1 - link to the [[:d:Property:P2677|relative position within image]] property on Wikidata (with localized label)\n* $2 - link to the [[:d:Property:P180|depicts]] property on Wikidata (with localized label)\n* $3 - link to the [[:d:Property:P9664|named place on map]] property on Wikidata (with localized label)", "index-paragraph-2": "Second paragraph of the tool’s index page, further describing how the tool works.\n\nParameters:\n* $1 - URL for linking to Wikidata\n* $2 - link to the [[:d:Property:P18|image]] property on Wikidata (with localized label)\n* $3 - URL for linking to Structured Data on Commons", "index-heading-by-item-and-property": "Heading on the tool’s index page. “Item” and “property” refer to Wikidata entities ({{msg-mw|wikibase-entity-item}}, {{msg-mw|wikibase-entity-property}}). “By” has the same meaning as in e.g. {{msg-mw|special-itembytitle}}.", "index-label-item-id": "Label for an input for a Wikidata item ID.", "index-placeholder-item-id": "Placeholder for an input for a Wikidata item ID.\n\nParameters:\n* $1 - one possible format (item ID)\n* $2 - another possible format (full item URL)", "index-label-property-id-optional": "Label for an optional input for a Wikidata property ID.", "index-button-load": "Label for several buttons to load data.", "index-button-preview-iiif": "Label for a button to preview an IIIF manifest on another tool. IIIF ([[:w:International Image Interoperability Framework|International Image Interoperability Framework]]) is not usually translated.", "index-button-iiif-manifest": "Label for a button to generate a IIIF manifest. IIIF ([[:w:International Image Interoperability Framework|International Image Interoperability Framework]]) is not usually translated.", "index-heading-by-file": "Heading on the tool’s index page. “File” refers to media files on Wikimedia Commons. “By” has the same meaning as in e.g. {{msg-mw|special-itembytitle}}.", "index-label-file-title": "Label for an input for a Wikimedia Commons file.", "index-placeholder-file-title": "Placeholder for an input for a Wikimedia Commons file.\n\nParameters:\n* $1 - one possible format (file name)\n* $2 - another possible format (MediaInfo entity ID)\n* $3 - another possible format (full file URL)", "index-heading-by-iiif": "Heading on the tool’s index page. IIIF ([[:w:International Image Interoperability Framework|International Image Interoperability Framework]]) is not usually translated. “Property” refers to Wikidata entities ({{msg-mw|wikibase-entity-property}}). “By” has the same meaning as in e.g. {{msg-mw|special-itembytitle}}.", "index-label-iiif-region": "Label for an input for a IIIF region. The whole message is wrapped in a link to the relevant IIIF specification. IIIF ([[:w:International Image Interoperability Framework|International Image Interoperability Framework]]) is not usually translated.", "image-scale": "Label for an input (or set of inputs) that let the user scale the size of the image between 0× and 5× (a kind of “zoom in” or “zoom out”).", "image-depicted-without-region": "Message above a list of one or more items that are linked using the [[:d:Property:P2677|depicts]] property on Wikidata, but without a [[:d:Property:P2677|relative position within image]] qualifier. The translation should follow the label of the “depicts” property on Wikidata. (See also {{msg-wm|wikidata-image-positions-image-named-places-on-map-without-region}}.)", "image-named-places-on-map-without-region": "Message above a list of one or more items that are linked using the [[:d:Property:P9664|named place on map]] property on Wikidata, but without a [[:d:Property:P2677|relative position within image]] qualifier. The translation should follow the label of the “named place on map” property on Wikidata. (See also {{msg-wm|wikidata-image-positions-image-depicted-without-region}}.)", + "image-button-add-region": "Label for a button to add a region ([[:d:Property:P2677|relative position within image]] qualifier) to a depicted item on the image.", + "image-button-use-region": "Label for a button to stop editing the image region and publish it.", + "image-button-cancel": "Label for a button to cancel adding or editing a region.\n{{Identical|Cancel}}", + "image-button-editing-statement": "Label for a button while a statement is being edited to add a [[:d:Property:P2677|relative position within image]] qualifier; this label is shown after the button has been clicked, so it’s really an informational message, not a call to action. “Statement” refers to a Wikibase statement, compare e.g. {{msg-mw|wikibase-statementgrouplistview-add}}, {{msg-mw|wikibase-statementsection-statements}}, and [[Template:Identical/Statement]].", + "image-button-edit-region": "Label for a button to select one of the existing regions on the image for editing.", + "image-button-select-region": "Label for the button from {{msg-wm|wikidata-image-positions-image-button-edit-region}} after it has been clicked, prompting the user to click one of the existing regions by clicking on it.", + "image-button-loading": "Label for a button while data is being loaded in the background. (The button isn’t actionable during this time, so this is more an informational message.)", "alert-not-logged-in": "Message shown when the user is not logged into the tool. The parts in <noscript> are additionally only shown to users who have JavaScript disabled, and effectively duplicate the information from {{msg-wm|wikidata-image-positions-alert-noscript}}.\n\nParameters:\n* $1 - URL for the login link", "alert-noscript": "Message shown when the user is logged in, but has JavaScript disabled. The entire message is wrapped in <noscript> by the tool; compare also {{msg-wm|wikidata-image-positions-alert-not-logged-in}}.", "settings": "The tool’s settings (preferences, options, etc.). This message is currently used for the heading on the settings page as well as the link in the navigation bar / menu; those uses could later be split up into separate messages if necessary.", "settings-label-interface-language-code": "Label for one of the tool’s settings, for the language the tool’s user interface is in.", "settings-save": "Label for the button to save the settings." } diff --git a/static/image-edit.js b/static/image-edit.js index 2abf9a3..5d19c42 100644 --- a/static/image-edit.js +++ b/static/image-edit.js @@ -1,547 +1,548 @@ import { createApp } from 'vue'; import * as codex from 'codex'; import * as codexIcons from 'codex-icons'; import Session, { set } from 'm3api/browser.js'; function setup() { 'use strict'; const csrfTokenElement = document.getElementById('csrf_token'), baseUrl = document.querySelector('link[rel=index]').href.replace(/\/$/, ''), mainDataset = document.getElementsByTagName('main')[0].dataset, + translations = JSON.parse(mainDataset.translations), depictedPropertiesLabels = JSON.parse(mainDataset.depictedPropertiesLabels), depictedPropertiesMessages = JSON.parse(mainDataset.depictedPropertiesMessages); /** Make a key event handler that calls the given callback when Esc is pressed. */ function onEscape(callback) { return function(eKey) { if (eKey.key === 'Escape') { return callback.apply(this, arguments); } }; } function addEditButtons() { document.querySelectorAll('.wd-image-positions--depicted-without-region').forEach(addEditButton); } function addEditButton(element) { const entity = element.closest('.wd-image-positions--entity'), depictedId = element.firstChild.dataset.entityId, scaleInput = entity.querySelector('.wd-image-positions--scale'), wrapper = entity.querySelector('.wd-image-positions--wrapper'), image = wrapper.firstElementChild; const button = document.createElement('button'); button.type = 'button'; button.classList.add('btn', 'btn-secondary', 'btn-sm', 'ms-2'); - button.textContent = 'add region'; + button.textContent = translations['image-button-add-region']; button.addEventListener('click', onClick); element.append(button); let cropper = null; let doneCallback = null; const onKeyDown = onEscape(cancelEditing); let cancelButton = null; function onClick() { if (cropper === null) { - button.textContent = 'loading...'; + button.textContent = translations['image-button-loading']; wrapper.classList.add('wd-image-positions--active'); image.classList.add('wd-image-positions--active'); button.classList.add('wd-image-positions--active'); scaleInput.disabled = true; doneCallback = ensureImageCroppable(image); cropper = new Cropper(image.firstElementChild, { viewMode: 2, movable: false, rotatable: true, // we don’t rotate the image ourselves, but this allows cropper.js to respect JPEG orientation scalable: false, zoomable: false, checkCrossOrigin: false, autoCrop: false, ready: function() { - button.textContent = 'use this region'; + button.textContent = translations['image-button-use-region']; cancelButton = document.createElement('button'); cancelButton.type = 'button'; cancelButton.classList.add('btn', 'btn-secondary', 'btn-sm', 'wd-image-positions--active', 'ms-2'); - cancelButton.textContent = 'cancel'; + cancelButton.textContent = translations['image-button-cancel']; cancelButton.addEventListener('click', cancelEditing); element.append(cancelButton); }, }); document.addEventListener('keydown', onKeyDown); } else { - if (button.textContent === 'loading...') { + if (button.textContent === translations['image-button-loading']) { return; } const cropData = cropper.getData(); if (!cropData.width || !cropData.height) { window.alert('Please select a region first. (Drag the mouse across an area, then adjust as needed.)'); return; } const depicted = document.createElement('div'); depicted.classList.add('wd-image-positions--depicted') const propertyId = [...element.closest('.wd-image-positions--depicteds-without-region').classList] .filter(klass => klass.startsWith('wd-image-positions--depicteds-without-region__')) .map(klass => klass.slice('wd-image-positions--depicteds-without-region__'.length))[0]; depicted.classList.add(`wd-image-positions--depicted__${propertyId}`) if (depictedId !== undefined) { depicted.dataset.entityId = depictedId; } depicted.dataset.statementId = element.dataset.statementId; depicted.append(element.firstChild.cloneNode(true)); image.append(depicted); - button.textContent = 'editing statement…'; + button.textContent = translations['image-button-editing-statement']; const subject = { id: entity.dataset.entityId, domain: entity.dataset.entityDomain }; saveCropper(subject, image, depicted, cropper).then( function() { element.remove(); if (image.querySelectorAll('.wd-image-positions--depicted').length === 1) { addEditRegionButton(entity); } }, function() { element.remove(); }, ).then(doneCallback).finally(() => { document.removeEventListener('keydown', onKeyDown); scaleInput.disabled = false; }); cropper = null; } } function cancelEditing() { cropper.destroy(); cropper = null; doneCallback(); wrapper.classList.remove('wd-image-positions--active'); image.classList.remove('wd-image-positions--active'); document.removeEventListener('keydown', onKeyDown); - button.textContent = 'add region'; + button.textContent = translations['image-button-add-region']; button.classList.remove('wd-image-positions--active'); scaleInput.disabled = false; if (cancelButton !== null) { cancelButton.remove(); cancelButton = null; } } } /** * Ensure that the image element is suitable for cropper.js, * by temporarily changing its src to the last (presumed highest-resolution) srcset entry. * The srcset is assumed to contain PNG/JPG thumbs, * whereas the src may be in an unsupported image format, such as TIFF. * * @param {HTMLElement} image The .wd-image-positions--image containing the * (*not* the itself) * @return {function} Callback to restore the image to its original src, * to be called after the cropper has been destroyed. */ function ensureImageCroppable(image) { const img = image.querySelector('img'), originalSrc = img.src; if (!/\.(?:jpe?g|png|gif)$/i.test(originalSrc)) { img.src = img.srcset.split(' ').slice(-2)[0]; } return function() { img.src = originalSrc; }; } /** * Save the cropper as a region qualifier for the depicted. * * @param {{ id: string, domain: string}} subject The subject entity * @param {HTMLElement} image The .wd-image-positions--image (*not* the ) * @param {HTMLElement} depicted The .wd-image-positions--depicted, * with a dataset containing a statementId, optional entityId and optional qualifierHash * @param {Cropper} cropper The cropper (will be destroyed) * @return {Promise} */ function saveCropper(subject, image, depicted, cropper) { const wrapper = image.parentElement; wrapper.classList.remove('wd-image-positions--active'); image.classList.remove('wd-image-positions--active'); const cropData = cropper.getData(), canvasData = cropper.getCanvasData(), x = 100 * cropData.x / canvasData.naturalWidth, y = 100 * cropData.y / canvasData.naturalHeight, w = 100 * cropData.width / canvasData.naturalWidth, h = 100 * cropData.height / canvasData.naturalHeight; // note: the browser rounds the percentages a bit, // and we’ll use the rounded values for the IIIF region depicted.style.left = `${x}%`; depicted.style.top = `${y}%`; depicted.style.width = `${w}%`; depicted.style.height = `${h}%`; cropper.destroy(); function pct(name) { return depicted.style[name].replace('%', ''); } const iiifRegion = `pct:${pct('left')},${pct('top')},${pct('width')},${pct('height')}`; const statementId = depicted.dataset.statementId, qualifierHash = depicted.dataset.qualifierHash, csrfToken = csrfTokenElement.textContent, formData = new FormData(); formData.append('statement_id', statementId); if (qualifierHash) { formData.append('qualifier_hash', qualifierHash); } formData.append('iiif_region', iiifRegion); formData.append('_csrf_token', csrfToken); return fetch(`${baseUrl}/api/v2/add_qualifier/${subject.domain}`, { method: 'POST', body: formData, credentials: 'include', }).then(response => { if (response.ok) { return response.json().then(json => { depicted.dataset.qualifierHash = json.qualifier_hash; }); } else { return response.text().then(text => { window.alert(`An error occurred:\n\n${text}\n\nThe region drawn is ${iiifRegion}, if you want to add it manually.`); throw new Error('Saving failed'); }); } }); } function addEditRegionButtons() { document.querySelectorAll('.wd-image-positions--entity').forEach(addEditRegionButton); } function addEditRegionButton(entityElement) { const wrapper = entityElement.querySelector('.wd-image-positions--wrapper'), image = wrapper.firstElementChild; if (!image.querySelector('.wd-image-positions--depicted')) { return; } const scaleInput = entityElement.querySelector('.wd-image-positions--scale'); const button = document.createElement('button'); button.type = 'button'; button.classList.add('btn', 'btn-secondary'); - button.textContent = 'Edit a region'; + button.textContent = translations['image-button-edit-region']; button.addEventListener('click', addEditRegionListeners); const cancelButton = document.createElement('button'); cancelButton.type = 'button'; cancelButton.classList.add('btn', 'btn-secondary', 'wd-image-positions--active', 'ms-2'); - cancelButton.textContent = 'cancel'; + cancelButton.textContent = translations['image-button-cancel']; const buttonWrapper = document.createElement('div'); buttonWrapper.append(button); // cancelButton is not appended yet entityElement.append(buttonWrapper); const fieldSet = entityElement.querySelector('fieldset'); if (fieldSet) { entityElement.append(fieldSet); // move after buttonWrapper } let onKeyDown = null; function addEditRegionListeners() { - button.textContent = 'Select a region to edit'; + button.textContent = translations['image-button-select-region']; button.classList.add('wd-image-positions--active'); for (const depicted of entityElement.querySelectorAll('.wd-image-positions--depicted')) { depicted.addEventListener('click', editRegion); } button.removeEventListener('click', addEditRegionListeners); onKeyDown = onEscape(cancelSelectRegion); document.addEventListener('keydown', onKeyDown); buttonWrapper.append(cancelButton); cancelButton.addEventListener('click', cancelSelectRegion); } function editRegion(event) { event.preventDefault(); wrapper.classList.add('wd-image-positions--active'); image.classList.add('wd-image-positions--active'); scaleInput.disabled = true; for (const depicted of entityElement.querySelectorAll('.wd-image-positions--depicted')) { depicted.removeEventListener('click', editRegion); } const depicted = event.target.closest('.wd-image-positions--depicted'); document.removeEventListener('keydown', onKeyDown); cancelButton.removeEventListener('click', cancelSelectRegion); onKeyDown = onEscape(cancelEditRegion); document.addEventListener('keydown', onKeyDown); cancelButton.addEventListener('click', cancelEditRegion); const doneCallback = ensureImageCroppable(image); const cropper = new Cropper(image.firstElementChild, { viewMode: 2, movable: false, rotatable: true, // we don’t rotate the image ourselves, but this allows cropper.js to respect JPEG orientation scalable: false, zoomable: false, checkCrossOrigin: false, ready: function() { const canvasData = cropper.getCanvasData(); cropper.setData({ x: Math.round(parseFloat(depicted.style.left) * canvasData.naturalWidth / 100), y: Math.round(parseFloat(depicted.style.top) * canvasData.naturalHeight / 100), width: Math.round(parseFloat(depicted.style.width) * canvasData.naturalWidth / 100), height: Math.round(parseFloat(depicted.style.height) * canvasData.naturalHeight / 100), }); - button.textContent = 'use this region'; + button.textContent = translations['image-button-use-region']; button.addEventListener('click', doEditRegion); }, }); function doEditRegion() { button.removeEventListener('click', doEditRegion); - button.textContent = 'editing statement…'; + button.textContent = translations['image-button-editing-statement']; const subject = { id: entityElement.dataset.entityId, domain: entityElement.dataset.entityDomain }; saveCropper(subject, image, depicted, cropper).then( function() { - button.textContent = 'Edit a region'; + button.textContent = translations['image-button-edit-region']; button.classList.remove('wd-image-positions--active'); button.addEventListener('click', addEditRegionListeners); }, function() { - button.textContent = 'Edit a region'; + button.textContent = translations['image-button-edit-region']; button.classList.remove('wd-image-positions--active'); button.addEventListener('click', addEditRegionListeners); }, ).then(doneCallback).finally(() => { document.removeEventListener('keydown', onKeyDown); scaleInput.disabled = false; cancelButton.remove(); }); } function cancelEditRegion() { cropper.destroy(); doneCallback(); wrapper.classList.remove('wd-image-positions--active'); image.classList.remove('wd-image-positions--active'); button.removeEventListener('click', doEditRegion); - button.textContent = 'Edit a region'; + button.textContent = translations['image-button-edit-region']; button.addEventListener('click', addEditRegionListeners); button.classList.remove('wd-image-positions--active'); document.removeEventListener('keydown', onKeyDown); scaleInput.disabled = false; cancelButton.remove(); } } function cancelSelectRegion() { for (const depicted of entityElement.querySelectorAll('.wd-image-positions--depicted')) { depicted.removeEventListener('click', editRegion); } - button.textContent = 'Edit a region'; + button.textContent = translations['image-button-edit-region']; button.addEventListener('click', addEditRegionListeners); button.classList.remove('wd-image-positions--active'); document.removeEventListener('keydown', onKeyDown); cancelButton.remove(); } } function addNewDepictedForm(entityElement) { const session = new Session( 'www.wikidata.org', { formatversion: 2, origin: '*', }, { userAgent: 'Wikidata-Image-Positions (https://wd-image-positions.toolforge.org/)', } ); const entity = entityElement.closest('.wd-image-positions--entity'), subjectId = entity.dataset.entityId, subjectDomain = entity.dataset.entityDomain, newDepictedFormRoot = document.createElement('div'); entityElement.append(newDepictedFormRoot); createApp({ template: `

Add more statements:

Add statement
Add “unknown value” statement
`, components: codex, data() { const properties = Object.entries(depictedPropertiesLabels).map(entry => ({ value: entry[0], label: entry[1].value, })); return { disabled: false, properties, selectedProperty: properties[0].value, selectedItem: null, searchResults: [], searchValue: '', searchLimit: 5, searchOffset: 0, ...codexIcons, }; }, methods: { async onSearchInput(value) { this.searchValue = value; this.searchOffset = 0; if (!value) { this.searchResults = []; return; } const searchResults = await this.doSearch(value, this.searchOffset); if (this.searchValue !== value) { return; // changed during the request } this.searchResults = searchResults; this.searchOffset += this.searchLimit; }, async onSearchLoadMore() { const value = this.searchValue; const moreResults = await this.doSearch(value, this.searchOffset); if (this.searchValue !== value) { return; // changed during the request } this.searchResults.push(...moreResults); this.searchOffset += this.searchLimit; }, async doSearch(value, offset) { const response = await session.request({ action: 'wbsearchentities', search: value, language: 'en', type: 'item', limit: this.searchLimit, continue: this.searchOffset, props: set(), }); return response.search.map(result => ({ value: result.id, label: result.display?.label?.value, description: result.display?.description?.value, match: result.match.type === 'alias' ? `(${result.match.text})` : '', language: { label: result.display?.label?.language, description: result.display?.description?.language, match: result.match.type === 'alias' ? result.match.language : undefined, }, })); }, onAddItem() { if (!this.selectedItem) { return; } const formData = new FormData(); formData.append('snaktype', 'value'); formData.append('property_id', this.selectedProperty); formData.append('item_id', this.selectedItem); this.addStatement(formData); }, onAddNonValue(snakType) { const formData = new FormData(); formData.append('snaktype', snakType); this.addStatement(formData); }, addStatement(formData) { this.disabled = true; formData.append('entity_id', subjectId); formData.append('_csrf_token', csrfTokenElement.textContent); fetch(`${baseUrl}/api/v1/add_statement/${subjectDomain}`, { method: 'POST', body: formData, credentials: 'include', }).then(response => { if (response.ok) { return response.json().then(json => { const statementId = json.depicted.statement_id; const propertyId = json.depicted.property_id; let depictedsWithoutRegionList = entityElement.querySelector( `.wd-image-positions--depicteds-without-region__${propertyId} ul`, ); if (!depictedsWithoutRegionList) { const depictedsWithoutRegionDiv = document.createElement('div'), depictedsWithoutRegionText = document.createTextNode( depictedPropertiesMessages[propertyId], ); depictedsWithoutRegionList = document.createElement('ul'); depictedsWithoutRegionDiv.classList.add('wd-image-positions--depicteds-without-region'); depictedsWithoutRegionDiv.classList.add(`wd-image-positions--depicteds-without-region__${propertyId}`); depictedsWithoutRegionDiv.append(depictedsWithoutRegionText, depictedsWithoutRegionList); newDepictedFormRoot.insertAdjacentElement('beforebegin', depictedsWithoutRegionDiv); } const depicted = document.createElement('li'); depicted.classList.add('wd-image-positions--depicted-without-region'); depicted.dataset.statementId = statementId; depicted.innerHTML = json.depicted_item_link; depictedsWithoutRegionList.append(depicted); addEditButton(depicted); }); } else { return response.text().then(error => { window.alert(`An error occurred:\n\n${error}`); }); } }).finally(() => { this.disabled = false; }); } }, }).mount(newDepictedFormRoot); } function addNewDepictedForms() { document.querySelectorAll('.wd-image-positions--entity').forEach(entityElement => { addNewDepictedForm(entityElement); }); } addEditButtons(); addEditRegionButtons(); addNewDepictedForms(); } setup(); diff --git a/templates/images.html b/templates/images.html index 3ddaee5..a35d26f 100644 --- a/templates/images.html +++ b/templates/images.html @@ -1,27 +1,34 @@ {% extends "base.html" %} {% block head %} {{ super() }} {% if user_logged_in() %} {% endif %} {% endblock head %} {% block main_tag_attributes %}{{ super() }} data-depicted-properties-labels='{{ depicted_properties_labels() | tojson }}' data-depicted-properties-messages='{{ depicted_properties_messages() | tojson }}' data-translations='{{ { 'image-scale': message('image-scale'), + 'image-button-add-region': message('image-button-add-region'), + 'image-button-use-region': message('image-button-use-region'), + 'image-button-cancel': message('image-button-cancel'), + 'image-button-editing-statement': message('image-button-editing-statement'), + 'image-button-edit-region': message('image-button-edit-region'), + 'image-button-select-region': message('image-button-select-region'), + 'image-button-loading': message('image-button-loading'), } | tojson }}'{% endblock main_tag_attributes %}