Widget Template Source

10 views
Skip to first unread message

Robin Ildefonso Ildefonso

unread,
Jan 14, 2025, 7:47:57 PMJan 14
to structr
Hello, I'm creating a new template from a widget, and I'm trying to integrate it from my HTML jQuery code. I'm not sure why my code is not working, but it works fine locally. See the source code below. Also, the logs show an unlicensed feature. Can you help me with this?

Screenshot 2025-01-15 at 8.47.00 AM.png

<structr:shared-template name="Tailwind Main Page" data-structr-meta-content-type="text/html">
<!DOCTYPE html>
<html class="h-full bg-gray-100">

<head>
<title>Structr - ${localize(titleize(page.name, '-'))}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script> -->
<script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
green: '#84ba39',
greenhover: '#97cc4d'
}
}
}
}
$(document).ready(function () {
console.log('tessssss')
});
</script>

<!-- <script src="https://code.jquery.com/jquery-3.7.1.js" integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script> -->
</head>

<body class="h-full antialiased font-sans"
onclick="document.querySelectorAll('.dynamic-menu:not(.hidden)').forEach(m => m.classList.toggle('hidden'))">
${render(children)}
</body>

</html>
</structr:shared-template>

<structr:template src="Tailwind Main Page">

<form id="dynamicForm" class="bg-white p-4 rounded shadow-md mb-6">
<div class="flex flex-col md:flex-row bg-white rounded-lg justify-between font-bold mb-4" id="formHeader">
<p class="py-2 px-4"><span class="text-red-500">*</span> Level</p>
<p class="py-2 px-4"><span class="text-red-500">*</span> Title</p>
<p class="py-2 px-4"><span class="text-red-500">*</span> Content Type</p>
<p class="py-2 px-4">Action</p>
</div>
<div class="w-full border-t mt-4" id="form-row">
<div class="flex flex-col md:flex-row justify-between items-center" id="form-input">
<div>
<select name="levelType[]" class="border rounded p-2 m-3 required">
<option value="1">1</option>
<option value="2">2</option>
</select>
</div>
<div>
<textarea name="text[]" class="w-full border text-sm rounded p-2 m-3 required"></textarea>
</div>
<div>
<select name="listType[]" class="border rounded p-2 m-3 required">
<option value="bullet">Bullet</option>
<option value="numerical">Numerical</option>
<option value="alphabetical">Alphabetical</option>
<option value="roman">Roman Numeral</option>
</select>
</div>
<div>
<button type="button" onclick="addRow()" class="bg-green-500 text-white px-4 py-2 rounded m-1">Add Row</button>
<button type="button" onclick="deleteRow(this)" class="bg-red-500 text-white px-4 py-2 rounded m-1" disabled>Delete</button>
</div>
</div>
<div id="accordion-collapse" data-accordion="collapse">
<h2 id="accordion-collapse-heading-1">
<button onclick="initAccord()" type="button" id="accordion1" class="flex items-center justify-between w-full p-2 text-sm font-medium rtl:text-right text-gray-500 border border-gray-200 rounded-t-xl active:text-white active:bg-blue-500 active:font-bold" data-accordion-target="#accordion-collapse-body-1" aria-expanded="true" aria-controls="accordion-collapse-body-1">
<span>Subcontent</span>
<svg data-accordion-icon class="w-3 h-3 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5 5 1 1 5"/>
</svg>
</button>
</h2>
<div id="accordion-collapse-body-1" class="hidden" aria-labelledby="accordion-collapse-heading-1">
<div class="bg-white" id="subList">
<div class="flex flex-row justify-between border-b p-4">
<h2 class="text-lg font-bold mb-4">Subcontent List</h2>
<button type="button" onclick="addSubcontent(this)" class="bg-blue-500 text-white px-4 py-2 rounded">Add Subcontent</button>
</div>
<div id="subcontentList-1" class="m-4"></div>
</div>
<div id="subcontent-form" class="bg-white p-6 hidden">
<h3 class="text-lg font-bold mb-4">Add Subcontent</h3>
<div class="mb-4">
<div class="flex">
<div class="flex items-center me-4">
<input id="toggleImage" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="toggleImage" class="ms-2 text-sm font-medium text-gray-900">Image</label>
</div>
<div class="flex items-center me-4">
<input checked id="toggleParagraph" type="checkbox" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500">
<label for="toggleParagraph" class="ms-2 text-sm font-medium text-gray-900">Paragraph</label>
</div>
</div>
</div>
<div class="mb-4 border-black rounded" id="image-id" hidden>
<label class="block mb-2 text-sm font-medium text-gray-900" for="file_input">Upload Image</label>
<input class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 focus:outline-none" id="file_input" type="file">
<p class="mt-1 text-sm text-gray-500 mb-2">JPEG, PNG, JPG or SVG (MAX. 800x400px).</p>
<label class="block mb-2 text-sm font-normal text-gray-900">Image Description</label>
<textarea id="imageDescription" class="w-full border rounded p-2"></textarea>
</div>
<div class="mb-6" id="paragraph-id">
<label class="block mb-2 text-sm font-medium text-gray-900">Add Paragraph</label>
<div id="richTextEditor"></div>
</div>
<button type="button" onclick="addSubcontentToList(this)" class="bg-green-500 text-white px-4 py-2 rounded">Add</button>
</div>
</div>
</div>
</div>
</form>


integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4=" crossorigin="anonymous"></script>
<script>
let quill;
const subcontentMap = new Map();
let editIndex = null;
let accordionIndex = 2;
let subcontentListIndex = 1;

function initializeQuill() {
if (!$(".ql-toolbar").length) {
quill = new Quill("#richTextEditor", {
theme: "snow"
});
} else {
quill.root.innerHTML = "";
}
}

function addSubcontent(button) {
const rowElement = $(button).closest('#form-row');
rowElement.find(`#subcontent-form`).removeClass("hidden");
initializeQuill();
}

function addSubcontentToList() {
const content = quill.root.innerHTML;

const isImageChecked = $("#toggleImage").is(":checked");
const isParagraphChecked = $("#toggleParagraph").is(":checked");

let imagePreview = "";
let imageDescription = "";

if (isImageChecked) {
const fileInput = $("#file_input")[0].files;
const imageDescriptionInput = $("#imageDescription").val();

if (fileInput.length > 0) {
const reader = new FileReader();
reader.onload = function (e) {
imagePreview = `<img src="${e.target.result}" style="max-height: 400px; max-width: 400px;" alt="Uploaded Image"/>`;
imageDescription = imageDescriptionInput
? `<span class="text-gray-700 mt-2">${imageDescriptionInput}</span>`
: "";

saveSubcontent(imagePreview, isParagraphChecked ? content : "");
};
reader.readAsDataURL(fileInput[0]);
} else {
alert("Please select an image to upload.");
return;
}
} else {
saveSubcontent("", isParagraphChecked ? content : "");
}
}

function saveSubcontent(imageContent, paragraphContent) {
const combinedContent = `
${imageContent ? `<div>${imageContent}</div>` : ""}
${paragraphContent ? `<div>${paragraphContent}</div>` : ""}
`;

const subcontents = subcontentMap.get(subcontentListIndex) || [];

if (editIndex !== null) {
subcontents[editIndex] = combinedContent;
} else {
subcontents.push(combinedContent);
}

subcontentMap.set(subcontentListIndex, subcontents);
showSubcontentList();
}

function editSubcontent(index) {
const subcontents = subcontentMap.get(subcontentListIndex);
const contentToEdit = subcontents[index];

const doc = $("<div>").html(contentToEdit);
const image = doc.find("img");
const description = doc.find("span"); //no span detected to contentToEdit ("currVal: 'undefined'")
const paragraph = doc.find("p");

const imageSrc = image.attr("src");
const descriptionText = description.text();
const paragraphContent = paragraph.html();
console.log("Content to edit:", contentToEdit);
console.log("Paragraph content:", paragraphContent);
console.log("Description text:", descriptionText);

if (image.length) {
$("#toggleImage").prop("checked", true).trigger("change");
$("#file_input").val("");
$("#imageDescription").val(descriptionText);
} else {
$("#toggleImage").prop("checked", false).trigger("change");
}

if (paragraph.length) {
$("#toggleParagraph").prop("checked", true).trigger("change");
quill.root.innerHTML = paragraphContent;
} else {
$("#toggleParagraph").prop("checked", false).trigger("change");
quill.root.innerHTML = "Error fetching paragraph content.";
}

editIndex = index;
$("#subcontent-form").removeClass("hidden");
}
function showSubcontentList() {

console.log("Previous:", subcontentListIndex);

const subcontentList = $(`#subcontentList-${subcontentListIndex}`);
const newSubcontentList = `subcontentList-${subcontentListIndex}`;
const currSubList = subcontentList;

const subcontents = subcontentMap.get(subcontentListIndex) || [];
subcontents.forEach((content, index) => {
const item = $("<div></div>")
.addClass("p-2 border rounded bg-gray-100 flex justify-between items-center overflow-hidden m-2")
.html(`
<div class="overflow-hidden truncate" style="max-width: 70%;">${content}</div>
<div>
<button type="button" onclick="editSubcontent(${index})" class="bg-blue-500 text-white px-2 py-1 rounded mr-1">
<i class="fas fa-edit"></i>
</button>
<button type="button" onclick="deleteSubcontent(this, ${index})" class="bg-red-500 text-white px-2 py-1 rounded">
<i class="fas fa-trash"></i>
</button>
</div>
`);
currSubList.append(item);
});
subcontentListIndex++;
console.log("Current:", subcontentListIndex);
}

function deleteSubcontent(button, index) {
const subcontents = subcontentMap.get(subcontentListIndex);
subcontents.splice(index, 1);
subcontentMap.set(subcontentListIndex, subcontents);
$(button).closest("div").parent().remove();
}

function toggleVisibility(checkboxId, divId) {
$("#" + checkboxId).on("change", function() {
if ($(this).is(":checked")) {
$("#" + divId).show();
} else {
$("#" + divId).hide();
}
});
}

function initAccord() {
$(document).on("click", "[data-accordion-target]", function () {
const target = $(this).data("accordion-target");
$(target).toggleClass("hidden");
$(this).find("svg").toggleClass("rotate-180");
});
}

function addRow() {
const row = $("#dynamicForm").find("#form-row").first();
const newRow = row.clone();

newRow.attr("data-row-id", subcontentListIndex);

newRow.find("textarea").val("");
newRow.find("select").prop("selectedIndex", 0);

const accordionHeading = newRow.find('[id^="accordion-collapse-heading"]');
const accordionBody = newRow.find('[id^="accordion-collapse-body"]');
const subcontentList = newRow.find('[id^="subcontentList"]');
const newAccordionHeadingId = `accordion-collapse-heading-${accordionIndex}`;
const newAccordionBodyId = `accordion-collapse-body-${accordionIndex}`;

accordionHeading.attr("id", newAccordionHeadingId);
accordionHeading.attr("aria-controls", newAccordionHeadingId);
accordionBody.attr("id", newAccordionBodyId);
accordionBody.attr("aria-labelledby", newAccordionHeadingId);

$(`#${newAccordionBodyId}`).addClass("hidden");

const accordionButton = newRow.find("[data-accordion-target]");
accordionButton.attr("data-accordion-target", `#${newAccordionBodyId}`);

const buttons = newRow.find("button");
buttons.eq(0).attr("onclick", "addRow()");
buttons.eq(1).attr("onclick", "deleteRow(this)").prop("disabled", false);

subcontentMap.set(subcontentListIndex, []);

$("#dynamicForm").append(newRow);

accordionIndex++;
}

function deleteRow(button) {
const row = $(button).closest("#form-row");
row.remove();
}

function generateNewRowId() {
return `row-${Date.now()}`;
}

$(document).ready(function () {
console.log('tges')
const firstRowLevelType = $("#dynamicForm").find("#form-row").first().find('select[name="levelType[]"]');
firstRowLevelType.val("1").prop("disabled", true);

toggleVisibility("toggleParagraph", "paragraph-id");
toggleVisibility("toggleImage", "image-id");

initializeQuill();
});
</script>
</structr:template>

Kai Schwaiger

unread,
Jan 16, 2025, 4:43:29 AMJan 16
to str...@googlegroups.com
Hi,

the warning regarding "localize" comes from the <title> element where this function is called.

Then there are two things to look out for when using this:

1. Since this is run through an HTML-Importer, HTML characters need to be escaped if the are supposed to be used as a literal.
   This is mainly in the <script>. I’ll show an example and you’ll need to adapt your whole page widget to this:

Current code:
reader.onload = function (e) {
imagePreview = `<img src="${e.target.result}" style="max-height: 400px; max-width: 400px;" alt="Uploaded Image"/>`;
imageDescription = imageDescriptionInput
? `<span class="text-gray-700 mt-2">${imageDescriptionInput}</span>`
: "";

saveSubcontent(imagePreview, isParagraphChecked ? content : "");
};

The HTML characters would need to be escaped as follows:

reader.onload = function (e) {
imagePreview = `&lt;img src="${e.target.result}" style="max-height: 400px; max-width: 400px;" alt="Uploaded Image"/&gt;`;
imageDescription = imageDescriptionInput
? `&lt;span class="text-gray-700 mt-2"&gt;${imageDescriptionInput}&lt;/span&gt;`
: "";

saveSubcontent(imagePreview, isParagraphChecked ? content : "");
};

2. There is one more caveat when we are using regular elements in the tree. Because structr has its own template language, template expressions are automatically replaced upon rendering of the page (see the title element for an example).
    This kind of templating has existed in structr since the beginning and has always been in the form of ${expression….}. In 2020, JavaScript added its own template expression which is identical. This leads to a clash and thus, we can not use JavaScript template literals in DOM-elements. You can put such code in files though. So, taking this into account, the above code would need to be changed to:


reader.onload = function (e) {
imagePreview = '&lt;img src="' + e.target.result + '" style="max-height: 400px; max-width: 400px;" alt="Uploaded Image"/&gt;';
imageDescription = imageDescriptionInput
? '&lt;span class="text-gray-700 mt-2"&gt;' + imageDescriptionInput + '&lt;/span&gt;'
: "";

saveSubcontent(imagePreview, isParagraphChecked ? content : "");
};

Hope that explains the problems you encountered and provides input to achieve your desired functionality!

Best,
Kai


attachment.png


https://structr.com
Kai Schwaiger
kai.sc...@structr.com
Mobil
+49 151 / 419 811 76

Structr GmbH
Hanauer Landstraße 291
60314 Frankfurt am Main
Deutschland / Germany
Geschäftsführer: Axel Morgner
Sitz der Ges.: Frankfurt a. M.
HR: AG Frankfurt a. M. HRB 92262
Umsatzsteuer-ID: DE 279486092 




On 15. Jan 2025, at 01:47, Robin Ildefonso Ildefonso <ildefonso...@gmail.com> wrote:

Hello, I'm creating a new template from a widget, and I'm trying to integrate it from my HTML jQuery code. I'm not sure why my code is not working, but it works fine locally. See the source code below. Also, the logs show an unlicensed feature. Can you help me with this?

--
You received this message because you are subscribed to the Google Groups "structr" group.
To unsubscribe from this group and stop receiving emails from it, send an email to structr+u...@googlegroups.com.
To view this discussion, visit https://groups.google.com/d/msgid/structr/ff6d202b-cef6-49f4-be0e-b1b3187fc04dn%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages