diff --git a/collections/reports/labeljsongui.php b/collections/reports/labeljsongui.php
new file mode 100644
index 0000000000..18c37c86d6
--- /dev/null
+++ b/collections/reports/labeljsongui.php
@@ -0,0 +1,350 @@
+
+
+
+ Label Content Format Visual Editor
+ ';
+ echo '';
+ echo '';
+ }
+ ?>
+
+
+
+
+
+
+
+
+
Fields Available
+
+
+
+
+
+
+
+
+
+
Label Content Area
+
drag, drop & reorder fields here; click fields or lines to apply formats; toggle select/deselect by clicking once; reorder lines clicking on arrows
+
+
+
+ keyboard_arrow_upkeyboard_arrow_down
+
+
+
+
+
+
+
Label preview
+
content automatically displayed below
+
+
+
+
+
+
+
+
+
+
+
Line Options
+
+
+
+
+
+
+
+
+
+
+
diff --git a/css/symb/labelhelpers.css b/css/symb/labelhelpers.css
index eca9af3621..60f19c5836 100644
--- a/css/symb/labelhelpers.css
+++ b/css/symb/labelhelpers.css
@@ -24,7 +24,6 @@
.controls {
display: none;
}
-
.body {
width: auto;
margin-left: auto;
@@ -184,6 +183,18 @@
font-family: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
+.font-type-sans {
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+}
+
+.font-type-serif {
+ font-family: Georgia, Cambria, 'Times New Roman', Times, serif;
+}
+
+.font-type-mono {
+ font-family: Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
+}
+
.text-center {
text-align: center;
}
@@ -196,6 +207,18 @@
text-align: justify;
}
+.text-align-center {
+ text-align: center;
+}
+
+.text-align-right {
+ text-align: right;
+}
+
+.text-align-justify {
+ text-align: justify;
+}
+
/* Base font size should be defined
* in points or pixels, because these are
diff --git a/js/symb/collections.labeljsongui.js b/js/symb/collections.labeljsongui.js
new file mode 100644
index 0000000000..66d2927ce6
--- /dev/null
+++ b/js/symb/collections.labeljsongui.js
@@ -0,0 +1,1068 @@
+/**
+ * Symbiota Label Builder Functions
+ * Author: Laura Rocha Prado
+ * Version: 2020
+ */
+
+/** Creating Page Elements/Controls
+ ******************************
+ */
+
+// Defines formattable items in label (also used to create preview elements)
+// Replace with constructor based in query?
+const fieldProps = [
+ {
+ block: 'labelBlock',
+ name: 'Occurrence ID',
+ id: 'occid',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Collection ID',
+ id: 'collid',
+ group: 'collection',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Catalog Number',
+ id: 'catalogNumber',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Other Catalog Numbers',
+ id: 'otherCatalogNumbers',
+ group: 'specimen',
+ },
+ { block: 'labelBlock', name: 'Family', id: 'family', group: 'taxon' },
+ {
+ block: 'labelBlock',
+ name: 'Scientific Name',
+ id: 'scientificName',
+ group: 'taxon',
+ },
+ { block: 'labelBlock', name: 'Taxon Rank', id: 'taxonRank', group: 'taxon' },
+ {
+ block: 'labelBlock',
+ name: 'Infraspecific Epithet',
+ id: 'infraSpecificEpithet',
+ group: 'taxon',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Scientific Name Authorship',
+ id: 'scientificNameAuthorship',
+ group: 'taxon',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Parent Author',
+ id: 'parentAuthor',
+ group: 'taxon',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Identified By',
+ id: 'identifiedBy',
+ group: 'determination',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Date Identified',
+ id: 'dateIdentified',
+ group: 'determination',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Identification References',
+ id: 'identificationReferences',
+ group: 'determination',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Identification Remarks',
+ id: 'identificationRemarks',
+ group: 'determination',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Taxon Remarks',
+ id: 'taxonRemarks',
+ group: 'determination',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Identification Qualifier',
+ id: 'identificationQualifier',
+ group: 'determination',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Type Status',
+ id: 'typeStatus',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Recorded By',
+ id: 'recordedBy',
+ group: 'event',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Record Number',
+ id: 'recordNumber',
+ group: 'event',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Associated Collectors',
+ id: 'associatedCollectors',
+ group: 'event',
+ },
+ { block: 'labelBlock', name: 'Event Date', id: 'eventDate', group: 'event' },
+ { block: 'labelBlock', name: 'Year', id: 'year', group: 'event' },
+ { block: 'labelBlock', name: 'Month', id: 'month', group: 'event' },
+ { block: 'labelBlock', name: 'Month Name', id: 'monthName', group: 'event' },
+ { block: 'labelBlock', name: 'Day', id: 'day', group: 'event' },
+ {
+ block: 'labelBlock',
+ name: 'Verbatim Event Date',
+ id: 'verbatimEventDate',
+ group: 'event',
+ },
+ { block: 'labelBlock', name: 'Habitat', id: 'habitat', group: 'event' },
+ { block: 'labelBlock', name: 'Substrate', id: 'substrate', group: 'event' },
+ {
+ block: 'labelBlock',
+ name: 'Occurrence Remarks',
+ id: 'occurrenceRemarks',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Associated Taxa',
+ id: 'associatedTaxa',
+ group: 'taxon',
+ },
+ // { block: 'labelBlock', name: 'Dynamic Properties', id: 'dynamicProperties' },
+ {
+ block: 'labelBlock',
+ name: 'Verbatim Attributes',
+ id: 'verbatimAttributes',
+ group: 'event',
+ },
+ { block: 'labelBlock', name: 'Behavior', id: 'behavior', group: 'specimen' },
+ {
+ block: 'labelBlock',
+ name: 'Reproductive Condition',
+ id: 'reproductiveCondition',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Cultivation Status',
+ id: 'cultivationStatus',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Establishment Means',
+ id: 'establishmentMeans',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Life Stage',
+ id: 'lifeStage',
+ group: 'specimen',
+ },
+ { block: 'labelBlock', name: 'Sex', id: 'sex', group: 'specimen' },
+ {
+ block: 'labelBlock',
+ name: 'Individual Count',
+ id: 'individualCount',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Sampling Protocol',
+ id: 'samplingProtocol',
+ group: 'specimen',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Preparations',
+ id: 'preparations',
+ group: 'specimen',
+ },
+ { block: 'labelBlock', name: 'Country', id: 'country', group: 'locality' },
+ {
+ block: 'labelBlock',
+ name: 'State/Province',
+ id: 'stateProvince',
+ group: 'locality',
+ },
+ { block: 'labelBlock', name: 'County', id: 'county', group: 'locality' },
+ {
+ block: 'labelBlock',
+ name: 'Municipality',
+ id: 'municipality',
+ group: 'locality',
+ },
+ { block: 'labelBlock', name: 'Locality', id: 'locality', group: 'locality' },
+ {
+ block: 'labelBlock',
+ name: 'Decimal Latitude',
+ id: 'decimalLatitude',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Decimal Longitude',
+ id: 'decimalLongitude',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Geodetic Datum',
+ id: 'geodeticDatum',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Coordinate Uncertainty In Meters',
+ id: 'coordinateUncertaintyInMeters',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Verbatim Coordinates',
+ id: 'verbatimCoordinates',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Elevation In Meters',
+ id: 'elevationInMeters',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Verbatim Elevation',
+ id: 'verbatimElevation',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Minimum Depth In Meters',
+ id: 'minimumDepthInMeters',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Maximum Depth In Meters',
+ id: 'maximumDepthInMeters',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Verbatim Depth',
+ id: 'verbatimDepth',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Disposition',
+ id: 'disposition',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Storage Location',
+ id: 'storageLocation',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Duplicate Quantity',
+ id: 'duplicateQuantity',
+ group: 'locality',
+ },
+ {
+ block: 'labelBlock',
+ name: 'Date Last Modified',
+ id: 'dateLastModified',
+ group: 'event',
+ },
+];
+
+// Defines formatting buttons
+const formatsArr = [
+ { group: 'field', func: 'font-bold', icon: 'format_bold' },
+ { group: 'field', func: 'italic', icon: 'format_italic' },
+ { group: 'field', func: 'underline', icon: 'format_underlined' },
+ { group: 'field', func: 'uppercase', icon: 'format_size' },
+ { group: 'field-block', func: 'bar', icon: '', name: 'Add Bar' },
+];
+
+// Defines dropdown style groups
+const dropdownsArr = [
+ {
+ id: 'text',
+ name: 'font-size',
+ group: 'field',
+ options: [
+ { value: '', text: 'Font Size' },
+ { value: 'text-xs', text: 'X-Small' },
+ { value: 'text-sm', text: 'Small' },
+ { value: 'text-base', text: 'Normal' },
+ { value: 'text-lg', text: 'Large' },
+ { value: 'text-xl', text: 'X-Large' },
+ { value: 'text-2xl', text: '2X-Large' },
+ { value: 'text-3xl', text: '3X-Large' },
+ { value: 'text-4xl', text: '4X-Large' },
+ { value: 'text-5xl', text: '5X-Large' },
+ { value: 'text-6xl', text: '6X-Large' },
+ ],
+ },
+ {
+ id: 'font-type',
+ name: 'font-type',
+ group: 'field',
+ options: [
+ { value: '', text: 'Font Type' },
+ { value: 'font-type-sans', text: 'System Sans Serif' },
+ { value: 'font-type-serif', text: 'System Serif' },
+ { value: 'font-type-mono', text: 'System Mono' },
+ ],
+ },
+ {
+ id: 'text-align',
+ name: 'text-align',
+ group: 'field-block',
+ options: [
+ { value: '', text: 'Text Align' },
+ { value: 'text-align-center', text: 'Center' },
+ { value: 'text-align-right', text: 'Right' },
+ { value: 'text-align-justify', text: 'Justify' },
+ ],
+ },
+ {
+ id: 'mt',
+ name: 'spacing-top',
+ group: 'field-block',
+ options: [
+ { value: '', text: 'Line Spacing Top' },
+ { value: 'mt-0', text: 'Top: 0' },
+ { value: 'mt-1', text: 'Top: 1' },
+ { value: 'mt-2', text: 'Top: 2' },
+ { value: 'mt-3', text: 'Top: 3' },
+ { value: 'mt-4', text: 'Top: 4' },
+ { value: 'mt-5', text: 'Top: 5' },
+ { value: 'mt-6', text: 'Top: 6' },
+ { value: 'mt-8', text: 'Top: 8' },
+ { value: 'mt-10', text: 'Top: 10' },
+ { value: 'mt-12', text: 'Top: 12' },
+ ],
+ },
+ {
+ id: 'mb',
+ name: 'spacing-bottom',
+ group: 'field-block',
+ options: [
+ { value: '', text: 'Line Spacing Bottom' },
+ { value: 'mb-0', text: 'Bottom: 0' },
+ { value: 'mb-1', text: 'Bottom: 1' },
+ { value: 'mb-2', text: 'Bottom: 2' },
+ { value: 'mb-3', text: 'Bottom: 3' },
+ { value: 'mb-4', text: 'Bottom: 4' },
+ { value: 'mb-5', text: 'Bottom: 5' },
+ { value: 'mb-6', text: 'Bottom: 6' },
+ { value: 'mb-8', text: 'Bottom: 8' },
+ { value: 'mb-10', text: 'Bottom: 10' },
+ { value: 'mb-12', text: 'Bottom: 12' },
+ ],
+ },
+];
+
+const fieldDiv = document.getElementById('fields');
+const fieldListDiv = document.getElementById('fields-list');
+const controlDiv = document.getElementById('controls');
+const fieldsFilter = document.getElementById('fields-filter');
+const labelMid = document.getElementById('label-middle');
+
+// Initially creates all fields
+createFields(fieldProps);
+
+// Creates formatting (button) controls in page
+formatsArr.forEach((format) => {
+ let targetDiv = document.getElementById(`${format.group}-options`);
+ let btn = document.createElement('button');
+ btn.classList.add('control');
+ btn.disabled = true;
+ btn.dataset.func = format.func;
+ btn.dataset.group = format.group;
+ if (format.icon !== '') {
+ let icon = document.createElement('span');
+ icon.classList.add('material-icons');
+ icon.innerText = format.icon;
+ btn.appendChild(icon);
+ } else {
+ btn.innerText = format.name;
+ }
+ targetDiv.appendChild(btn);
+});
+
+// Creates formatting (dropdown) controls in page
+dropdownsArr.forEach((dropObj) => {
+ let targetDiv = document.getElementById(`${dropObj.group}-options`);
+ let slct = document.createElement('select');
+ slct.dataset.group = dropObj.group;
+ slct.classList.add('control');
+ slct.name = dropObj.name;
+ slct.id = dropObj.id;
+ slct.disabled = true;
+ dropObj.options.forEach((choice) => {
+ let opt = document.createElement('option');
+ opt.value = choice.value;
+ opt.innerText = choice.text;
+ slct.appendChild(opt);
+ });
+ targetDiv.appendChild(slct);
+});
+
+// Grabs elements
+const containers = document.querySelectorAll('.container');
+const draggables = document.querySelectorAll('.draggable');
+const build = document.getElementById('build-label');
+const preview = document.getElementById('preview-label');
+const controls = document.querySelectorAll('.control');
+const inputs = document.querySelectorAll('input');
+
+/** Methods
+ ******************************
+ */
+
+/**
+ * Displays user instructions overlay
+ */
+const overlay = document.getElementById('instructions');
+function openOverlay() {
+ overlay.classList.remove('hidden');
+}
+
+/**
+ * Hides user instructions overlay
+ */
+function closeOverlay() {
+ overlay.classList.add('hidden');
+}
+
+/**
+ * Filters array based on desired property
+ * @param {Array} arr Array to be filtered
+ * @param {Object} criteria Pair or pairs of property and criterion
+ */
+function filterObject(arr, criteria) {
+ return arr.filter(function (obj) {
+ return Object.keys(criteria).every(function (c) {
+ return obj[c] == criteria[c];
+ });
+ });
+}
+
+/**
+ * Removes object from array based on desired property
+ * @param {Array} arr Array to be cleaned
+ * @param {Object} criteria Pair or pairs of property and criterion
+ */
+function removeObject(arr, criteria) {
+ return arr.filter(function (obj) {
+ return Object.keys(criteria).every(function (c) {
+ return obj[c] !== criteria[c];
+ });
+ });
+}
+
+/**
+ * Gets list of fields currently available to drag to label build area
+ */
+function getCurrFields() {
+ let currFields = fieldProps;
+ let usedFields = document.querySelectorAll('#label-middle .draggable');
+ if (usedFields.length > 0) {
+ usedFields.forEach((usedField) => {
+ currFields = removeObject(currFields, { id: usedField.id });
+ });
+ }
+ return currFields;
+}
+
+/**
+ * Filters available fields on select option
+ */
+function filterFields() {
+ let value = this.value;
+ let filteredFields = '';
+ value === 'all'
+ ? (filteredFields = getCurrFields())
+ : (filteredFields = filterObject(getCurrFields(), { group: value }));
+ fieldListDiv.innerHTML = '';
+ createFields(filteredFields);
+}
+
+/**
+ * Creates draggable elements
+ * @param {Arr} arr Array with list of currently available fields
+ */
+function createFields(arr) {
+ arr.forEach((field) => {
+ let li = document.createElement('li');
+ li.innerHTML = field.name;
+ li.id = field.id;
+ if (field.block === 'labelBlock') {
+ li.draggable = 'true';
+ li.classList.add('draggable');
+ li.dataset.category = field.group;
+ li.addEventListener('dragstart', handleDragStart, false);
+ li.addEventListener('dragover', handleDragOver, false);
+ li.addEventListener('drop', handleDrop, false);
+ li.addEventListener('dragend', handleDragEnd, false);
+ fieldListDiv.appendChild(li);
+ }
+ });
+}
+
+/**
+ * Appends line (fieldBlock) to label builder
+ * Binded to button, adds editable div
+ */
+function addLine() {
+ let line = document.createElement('div');
+ line.classList.add('field-block', 'container');
+ let midBlocks = document.querySelectorAll('#label-middle > .field-block');
+ let up = document.createElement('span');
+ up.classList.add('material-icons');
+ up.innerText = 'keyboard_arrow_up';
+ line.appendChild(up);
+ let down = document.createElement('span');
+ down.classList.add('material-icons');
+ down.innerText = 'keyboard_arrow_down';
+ line.appendChild(down);
+ let lastBlock = midBlocks[midBlocks.length - 1];
+ lastBlock.parentNode.insertBefore(line, lastBlock.nextSibling);
+ line.draggable = true;
+ // Allows items to be added/reordered inside fieldBlock
+ line.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ const dragging = document.querySelector('.dragging');
+ dragging !== null ? line.appendChild(dragging) : '';
+ });
+}
+
+/**
+ * Refreshes label preview
+ * Triggered every time items are updated
+ */
+function refreshPreview() {
+ let labelList = [];
+ let fieldBlocks = document.querySelectorAll('#build-label .field-block');
+ // Builds array with directives (labelList)
+ fieldBlocks.forEach((block) => {
+ let itemsArr = [];
+ let items = block.querySelectorAll('li');
+ items.forEach((item) => {
+ let itemObj = {};
+ let className = Array.from(item.classList).filter(isPrintStyle);
+ itemObj.field = item.id;
+ itemObj.className = className;
+ itemObj.prefix = item.dataset.prefix;
+ itemObj.suffix = item.dataset.suffix;
+ itemsArr.push(itemObj);
+ });
+ labelList.push(itemsArr);
+ let fieldBlockStyles = Array.from(block.classList).filter(isPrintStyle);
+ fieldBlockStyles ? (itemsArr.className = fieldBlockStyles) : '';
+ let fieldBlockDelim = block.dataset.delimiter;
+ fieldBlockDelim
+ ? (itemsArr.delimiter = fieldBlockDelim)
+ : (itemsArr.delimiter = '');
+ });
+ // Clears preview div before appending elements
+ preview.innerHTML = '';
+ // Creates HTML elements and appends to preview div
+ labelList.forEach((labelItem, blockIdx) => {
+ let blockLen = labelItem.length;
+ let fieldBlock = document.createElement('div');
+ fieldBlock.classList.add('field-block');
+ let labelItemStyles = labelItem.className;
+ labelItemStyles.forEach((style) => {
+ fieldBlock.classList.add(style);
+ });
+ preview.appendChild(fieldBlock);
+ labelItem.forEach((field, fieldIdx) => {
+ createPreviewEl(field, fieldBlock);
+ let isLast = fieldIdx == blockLen - 1;
+ // Adds delimiter if existing up to last element
+ if (!isLast) {
+ let preview = document.getElementsByClassName(field.field);
+ let delim = document.createElement('span');
+ delim.innerText = labelItem.delimiter;
+ preview[0].after(delim);
+ }
+ });
+ });
+
+ return labelList;
+}
+
+/**
+ * Creates elements in preview div, based on controls in build
+ * @param {Object} element Field, constructed in `refreshPreview()`
+ * @param {DOM Node} parent DOM Node where element will be inserted
+ */
+function createPreviewEl(element, parent) {
+ // Grabs information from fieldProps array to create elements matching on id
+ let fieldInfo =
+ fieldProps[fieldProps.findIndex((x) => x.id === element.field)];
+ let div = document.createElement('div');
+ div.innerHTML = fieldInfo.name;
+ div.classList.add(fieldInfo.id);
+ div.classList.add(...element.className);
+ parent.appendChild(div);
+ let hasPrefix = element.prefix != undefined;
+ let hasSuffix = element.suffix != undefined;
+ if (hasPrefix) {
+ let currText = div.innerText;
+ let prefSpan = `${element.prefix}`;
+ div.innerHTML = prefSpan + currText;
+ }
+ if (hasSuffix) {
+ let sufSpan = document.createElement('span');
+ sufSpan.innerText = element.suffix;
+ div.appendChild(sufSpan);
+ }
+}
+
+/**
+ * Checks if class should be output in JSON
+ * @param {String} className found in item
+ */
+function isPrintStyle(className) {
+ const functionalStyles = [
+ 'draggable',
+ 'selected',
+ 'field-block',
+ 'container',
+ ];
+ return !functionalStyles.includes(className);
+}
+
+/**
+ * Generate JSON string for current configurations
+ * @param {Array} list Array of fields, built by `refreshPreview()`
+ */
+function generateJson(list) {
+ let labelBlocks = [];
+ // Parse nested array
+ Object.keys(list).forEach((index) => {
+ let fieldBlockObj = {};
+ // Joins array of className items for fields
+ let fieldItem = list[index];
+ fieldItem.map((prop) => {
+ prop.className.length > 0
+ ? (prop.className = prop.className.join(' '))
+ : delete prop.className;
+ });
+ fieldBlockObj.fieldBlock = fieldItem;
+ let fieldBlockDelim = fieldItem.delimiter;
+ fieldBlockDelim !== undefined
+ ? (fieldBlockObj.delimiter = fieldBlockDelim)
+ : '';
+ let fieldBlockStyles = fieldItem.className;
+ fieldBlockStyles.length > 0
+ ? (fieldBlockObj.className = fieldItem.className.join(' '))
+ : delete fieldBlockObj.className;
+ labelBlocks.push(fieldBlockObj);
+ });
+ let json = JSON.stringify(labelBlocks, null, 2);
+ console.log(json);
+ return json;
+}
+
+/**
+ * Prints JSON in interface
+ *
+ */
+function printJson() {
+ let list = refreshPreview();
+ let dummy = document.getElementById('dummy');
+ let copyBtn = document.getElementById('copyBtn');
+ console.log(list);
+ console.log(list[0].length);
+ let isEmpty = list[0].length == 0;
+ let message = '';
+ if (isEmpty) {
+ dummy.style.display = 'none';
+ copyBtn.style.display = 'none';
+ alert(
+ 'Label format is empty! Please drag some items to the build area before trying again'
+ );
+ } else {
+ let json = generateJson(refreshPreview());
+ copyBtn.style.display = 'inline-block';
+ dummy.value = json;
+ dummy.style.display = 'block';
+ dummy.style.height = '300px';
+ dummy.style.width = '100%';
+ }
+}
+
+/**
+ * Copies JSON output to user's clipboard
+ */
+function copyJson() {
+ dummy.select();
+ dummy.setSelectionRange(0, 99999); /* For mobile devices */
+ document.execCommand('copy');
+ /* Alert the copied text */
+ alert('Copied JSON to clipboard');
+}
+
+/**
+ * Toggles select/deselect clicked element
+ * @param {DOM Node} element
+ */
+function toggleSelect(element) {
+ element.classList.toggle('selected');
+ let isSelected = element.classList.contains('selected');
+ return isSelected;
+}
+
+/**
+ * Toggles formatting controls based on filter and state
+ * @param {String} filter Class of formatting control (field or field-block)
+ * @param {Boolean} bool
+ */
+function activateControls(filter, bool) {
+ let filtered = document.querySelectorAll(`[data-group=${filter}]`);
+ filtered.forEach((control) => {
+ bool ? (control.disabled = false) : (control.disabled = true);
+ });
+}
+
+/**
+ * Deactivates all controls
+ */
+function deactivateControls() {
+ controls.forEach((control) => {
+ control.disabled = true;
+ });
+}
+
+/**
+ * Gets selected item state (formatted classes)
+ * @param {DOM Node} item Field in build label area
+ */
+function getState(item) {
+ let formatList = Array.from(item.classList);
+ // Removes '.draggable' and '.selected' from array
+ printableList = formatList.filter(isPrintStyle);
+
+ if (printableList.length > 0) {
+ // Render state of each formatting button
+ printableList.forEach((formatItem) => {
+ // Check if class is a choice in a dropdown by matching first part of class
+ let strArr = formatItem.split('-');
+ let str = '';
+ strArr.length == 3
+ ? (str = strArr[0] + '-' + strArr[1])
+ : (str = strArr[0]);
+ console.log(str);
+ // Loop through each item in array
+ dropdownsArr.forEach((dropdown) => {
+ let isDropdownStyle = str === dropdown.id;
+ if (isDropdownStyle) {
+ let selDropdown = document.getElementById(str);
+ selDropdown.value = formatItem;
+ }
+ });
+ controls.forEach((control) => {
+ // Select that format and activate it
+ if (formatItem === control.dataset.func) {
+ control.classList.add('selected');
+ }
+ });
+ });
+ }
+
+ // Get state of prefix/suffix for fields
+ let hasPrefix = item.dataset.prefix != null;
+ let prefixInput = document.getElementById('prefix');
+ hasPrefix ? (prefixInput.value = item.dataset.prefix) : '';
+ let hasSuffix = item.dataset.suffix != null;
+ let suffixInput = document.getElementById('suffix');
+ hasSuffix ? (suffixInput.value = item.dataset.suffix) : '';
+}
+
+/**
+ * Applies selected control styles to selected items
+ * @param {String} control ID of formatting control (button or select)
+ * @param {Array} selectedItems Items to be formatted (selected)
+ * @param {Boolean} bool If style will be added or removed, depends on state of control (important for buttons)
+ */
+function toggleStyle(control, selectedItems, bool) {
+ selectedItems.forEach((item) => {
+ // Double-checking if item is selected
+ if (item.classList.contains('selected')) {
+ // Deals with buttons
+ // if formatting button is selected, add class, else remove
+ bool
+ ? item.classList.add(control.dataset.func)
+ : item.classList.remove(control.dataset.func);
+ //
+ } else {
+ return false;
+ }
+ refreshPreview();
+ });
+}
+
+/**
+ * Applies selected dropdown styles to selected items
+ * @param {String} dropdown ID of dropdown
+ * @param {Array} selectedItems Items to be formatted (selected)
+ */
+function addReplaceStyle(dropdown, selectedItems) {
+ // Deals with selection
+ dropdown.addEventListener('input', function () {
+ selectedItems.forEach((item) => {
+ let option = dropdown.value;
+ if (option !== '') {
+ // Check if item already has styles in this group
+ let group = new RegExp(`${dropdown.id}-*`);
+ let hasGroup = item.className.split(' ').some(function (c) {
+ return group.test(c);
+ });
+ if (item.classList.contains('selected')) {
+ if (!hasGroup) {
+ // If not, add it
+ item.classList.add(option);
+ console.log(`added ${option} to ${item.id}`);
+ } else {
+ // If yes, replace it
+ item.classList.forEach((className) => {
+ if (className.startsWith(dropdown.id)) {
+ item.classList.remove(className);
+ }
+ });
+ item.classList.add(option);
+ }
+ }
+ }
+ });
+ });
+ refreshPreview();
+}
+
+/**
+ * Clears/resets controls state
+ */
+function resetControls() {
+ controls.forEach((control) => {
+ // Deal with select input
+ let isDropdown = control.tagName === 'SELECT';
+ isDropdown ? (control.value = '') : '';
+ control.classList.remove('selected');
+ let isInput = control.tagName === 'INPUT';
+ isInput ? (control.value = '') : '';
+ });
+}
+
+/**
+ * Updates optional field content (prefix/suffix)
+ * @param {DOM Node} content Optional content input
+ * @param {DOM Node} item Field to be modified
+ */
+function updateFieldContent(content, item) {
+ let option = content.id;
+ item.setAttribute('data-' + option, content.value);
+ console.log(content, item);
+}
+
+/**
+ * Tags dragging elements and copies their content
+ * @param {Event} e
+ */
+function handleDragStart(e) {
+ dragSrcEl = this;
+ this.classList.add('dragging');
+ e.dataTransfer.effectAllowed = 'move';
+}
+
+/**
+ * Moves content of dragged element when done moving
+ * @param {Event} e
+ */
+function handleDragOver(e) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+ e.dataTransfer.dropEffect = 'move';
+ return false;
+}
+
+/**
+ * Reorders element based on position when dropped
+ * @param {Event} e
+ */
+let dragSrcEl = null;
+function handleDrop(e) {
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ }
+ if (dragSrcEl != this) {
+ this.parentNode.insertBefore(dragSrcEl, this);
+ }
+ return false;
+}
+
+/**
+ * Removes tag from dragging element
+ * @param {Event} e
+ */
+function handleDragEnd(e) {
+ this.classList.remove('dragging');
+ refreshPreview();
+ return false;
+}
+
+/** Event Listeners
+ ******************************
+ */
+fieldsFilter.onchange = filterFields;
+
+draggables.forEach((draggable) => {
+ draggable.addEventListener('dragstart', handleDragStart, false);
+ draggable.addEventListener('dragover', handleDragOver, false);
+ draggable.addEventListener('drop', handleDrop, false);
+ draggable.addEventListener('dragend', handleDragEnd, false);
+});
+
+containers.forEach((container) => {
+ container.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ const dragging = document.querySelector('.dragging');
+ dragging !== null ? container.appendChild(dragging) : '';
+ });
+});
+
+// Elements in '#label-middle'
+labelMid.addEventListener('click', (e) => {
+ if (e.target.matches('.material-icons')) {
+ console.log(e.target.innerText);
+ if (e.target.innerText === 'keyboard_arrow_up') {
+ let first = labelMid.getElementsByClassName('field-block')[0];
+ console.log(first);
+ let curr = e.target.parentNode;
+ // reorder only if item is not first in list already
+ if (curr !== first) {
+ let prev = e.target.parentNode.previousSibling;
+ // move current into prev
+ prev.replaceWith(curr);
+ // insert current after prev
+ curr.parentNode.insertBefore(prev, curr.nextSibling);
+ }
+ } else if (e.target.innerText === 'keyboard_arrow_down') {
+ let next = e.target.parentNode.nextSibling;
+ let curr = e.target.parentNode;
+ if (next) {
+ // move current into next
+ curr.replaceWith(next);
+ // insert current after next
+ next.parentNode.insertBefore(curr, next.nextSibling);
+ }
+ }
+ refreshPreview();
+ } else {
+ // Toggle select clicked item (on formattables only)
+ toggleSelect(e.target);
+ // Everytime item is clicked, display list of selected items:
+ let selectedItems = build.querySelectorAll('.selected');
+ // console.log(selectedItems);
+
+ // When element is selected, activate formatting buttons
+ // depends on number of elements in page (at least one selected).
+ let isAnySelected = selectedItems.length > 0;
+
+ if (isAnySelected) {
+ let itemType = '';
+ let numSelected = build.querySelectorAll('.selected');
+ // Gets formatting information for individually selected item
+ if (numSelected.length > 1) {
+ // If there is more than one type of selected items, deactivate controls
+ let selected = build.querySelectorAll('.selected');
+ let typeArr = [];
+ selected.forEach((item) => {
+ typeArr.push(Array.from(item.classList).join(' '));
+ });
+ let uniqueTypeSet = new Set(typeArr);
+ // console.log(uniqueTypeSet);
+ if (uniqueTypeSet.size > 1) {
+ // deactivate controls
+ deactivateControls();
+ } else {
+ (' ');
+ }
+ resetControls();
+ } else if (numSelected.length == 1) {
+ // Refreshes buttons according to applied styles in selected item
+ let item = build.querySelector('.selected');
+ if (item.matches('.draggable')) {
+ itemType = 'field';
+ // deactivate 'field-block' items
+ activateControls(itemType, isAnySelected);
+ getState(item);
+ } else if (item.matches('.field-block')) {
+ itemType = 'field-block';
+ // deactivate 'field' items
+ activateControls(itemType, isAnySelected);
+ getState(item);
+ }
+ } else {
+ return false;
+ }
+ } else {
+ resetControls();
+ deactivateControls();
+ }
+ }
+});
+
+// Formatting controls
+controlDiv.addEventListener('click', (e) => {
+ // Gets selected items to format
+ let formatItems = build.querySelectorAll('.selected');
+ let isFormatSelected = toggleSelect(e.target);
+ let isButton = e.target.tagName === 'BUTTON';
+ let isDropdown = e.target.tagName === 'SELECT';
+ // Buttons
+ if (isButton) {
+ toggleStyle(e.target, formatItems, isFormatSelected);
+ } else if (isDropdown) {
+ addReplaceStyle(e.target, formatItems);
+ }
+});
+
+// Field and Block options (prefix/suffix, delimiters)
+// Listen to input changes
+inputs.forEach((input) => {
+ input.addEventListener('input', (e) => {
+ let formatItem = build.querySelector('.selected');
+ updateFieldContent(e.target, formatItem);
+ console.log(e.target, formatItem);
+ refreshPreview();
+ });
+});