diff --git a/static/image-edit.js b/static/image-edit.js
index 0d3201e..4476db9 100644
--- a/static/image-edit.js
+++ b/static/image-edit.js
@@ -1,548 +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 = translations['image-button-add-region'];
+ button.innerHTML = 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 = translations['image-button-loading'];
+ button.innerHTML = 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 = translations['image-button-use-region'];
+ button.innerHTML = 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 = translations['image-button-cancel'];
+ cancelButton.innerHTML = translations['image-button-cancel'];
cancelButton.addEventListener('click', cancelEditing);
element.append(cancelButton);
},
});
document.addEventListener('keydown', onKeyDown);
} else {
- if (button.textContent === translations['image-button-loading']) {
+ if (button.innerHTML === 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 = translations['image-button-editing-statement'];
+ button.innerHTML = 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 = translations['image-button-add-region'];
+ button.innerHTML = 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.trim().split(/\s+/).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 = translations['image-button-edit-region'];
+ button.innerHTML = 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 = translations['image-button-cancel'];
+ cancelButton.innerHTML = 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 = translations['image-button-select-region'];
+ button.innerHTML = 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 = translations['image-button-use-region'];
+ button.innerHTML = translations['image-button-use-region'];
button.addEventListener('click', doEditRegion);
},
});
function doEditRegion() {
button.removeEventListener('click', doEditRegion);
- button.textContent = translations['image-button-editing-statement'];
+ button.innerHTML = translations['image-button-editing-statement'];
const subject = { id: entityElement.dataset.entityId, domain: entityElement.dataset.entityDomain };
saveCropper(subject, image, depicted, cropper).then(
function() {
- button.textContent = translations['image-button-edit-region'];
+ button.innerHTML = translations['image-button-edit-region'];
button.classList.remove('wd-image-positions--active');
button.addEventListener('click', addEditRegionListeners);
},
function() {
- button.textContent = translations['image-button-edit-region'];
+ button.innerHTML = 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 = translations['image-button-edit-region'];
+ button.innerHTML = 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 = translations['image-button-edit-region'];
+ button.innerHTML = 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: `