├── .gitignore
├── Module.php
├── asset
├── css
│ └── nesteddatatype.css
└── js
│ ├── jquery-ui.min.js
│ └── nesteddatatype.js
├── config
├── module.config.php
└── module.ini
├── readme.md
├── src
├── Controller
│ └── IndexController.php
├── DataType
│ └── NestedDataType.php
├── Exception
│ └── NestedDataTypeFactory.php
└── Service
│ └── NestedDataTypeFactory.php
└── view
└── nested-data-type
├── data-type
└── nested-data-type.phtml
└── item
└── sidebar-select.phtml
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/Module.php:
--------------------------------------------------------------------------------
1 | getServiceLocator()->get('Omeka\Acl');
22 | $acl->allow(null, 'NestedDataType\Controller\Index');
23 | }
24 |
25 | public function attachListeners(SharedEventManagerInterface $sharedEventManager)
26 | {
27 | $sharedEventManager->attach(
28 | \Omeka\Api\Representation\ItemRepresentation::class,
29 | 'rep.resource.title',
30 | [$this, 'handleResourceTitle']
31 | );
32 | $sharedEventManager->attach(
33 | \Omeka\Api\Representation\ItemSetRepresentation::class,
34 | 'rep.resource.title',
35 | [$this, 'handleResourceTitle']
36 | );
37 | $sharedEventManager->attach(
38 | \Omeka\Api\Representation\MediaRepresentation::class,
39 | 'rep.resource.title',
40 | [$this, 'handleResourceTitle']
41 | );
42 | $sharedEventManager->attach(
43 | \Annotate\Api\Representation\AnnotationRepresentation::class,
44 | 'rep.resource.title',
45 | [$this, 'handleResourceTitle']
46 | );
47 | $sharedEventManager->attach(
48 | 'Omeka\Controller\Admin\Item',
49 | 'view.add.after',
50 | [$this, 'renderAssets']
51 | );
52 | $sharedEventManager->attach(
53 | 'Omeka\Controller\Admin\Item',
54 | 'view.edit.after',
55 | [$this, 'renderAssets']
56 | );
57 | $sharedEventManager->attach(
58 | 'Omeka\Controller\Admin\ItemSet',
59 | 'view.add.after',
60 | [$this, 'renderAssets']
61 | );
62 | $sharedEventManager->attach(
63 | 'Omeka\Controller\Admin\ItemSet',
64 | 'view.edit.after',
65 | [$this, 'renderAssets']
66 | );
67 | $sharedEventManager->attach(
68 | 'Omeka\Controller\Admin\Media',
69 | 'view.add.after',
70 | [$this, 'renderAssets']
71 | );
72 | $sharedEventManager->attach(
73 | 'Omeka\Controller\Admin\Media',
74 | 'view.edit.after',
75 | [$this, 'renderAssets']
76 | );
77 | $sharedEventManager->attach(
78 | 'Omeka\DataType\Manager',
79 | 'service.registered_names',
80 | [$this, 'addResourcesClassesServices']
81 | );
82 | }
83 |
84 | public function renderAssets(Event $event)
85 | {
86 | $view = $event->getTarget();
87 | $assetUrl = $view->plugin('assetUrl');
88 | $view->headLink()
89 | ->appendStylesheet($assetUrl('css/nesteddatatype.css', 'NestedDataType'));
90 | $view->headScript()
91 | ->appendFile($assetUrl('js/jquery-ui.min.js', 'NestedDataType'));
92 | $view->headScript()
93 | ->appendFile($assetUrl('js/nesteddatatype.js', 'NestedDataType'));
94 | }
95 |
96 | public function addResourcesClassesServices(Event $event)
97 | {
98 | $resourcesClasses = $this->getServiceLocator()->get('Omeka\ApiManager')->search('resource_classes')->getContent();
99 |
100 | $names = $event->getParam('registered_names');
101 |
102 | foreach ($resourcesClasses as $class) {
103 | $names[] = 'nesteddatatype#' . $class->term();
104 | }
105 |
106 | $event->setParam('registered_names', $names);
107 | }
108 |
109 | /**
110 | * Manage the parsing of the title.
111 | *
112 | * @param Event $event
113 | */
114 | public function handleResourceTitle(Event $event): void
115 | {
116 | $resource = $event->getTarget();
117 | $template = $resource->resourceTemplate();
118 |
119 | if ($template && $property = $template->titleProperty()) {
120 | $title = $resource->value($property->term());
121 | $properties = json_decode($title, true);
122 |
123 | $event->setParam('title', (string) $title);
124 | if (isset($properties[0]['@type'])) {
125 | $values = [];
126 |
127 | foreach ($properties[0] as $key => $val) {
128 | if (is_array($val) || is_object($val)) {
129 | foreach ($val as $innerKey => $innerVal) {
130 | if ($innerVal['@value']) {
131 | $values[$key] = $innerVal['@value'];
132 | continue;
133 | }
134 | if ($innerVal['label']) {
135 | $values[$key] = $innerVal['label'];
136 | continue;
137 | }
138 | foreach ($innerVal as $secondKey => $secondVal) {
139 | $values[$key] = $secondVal['@value'];
140 | if ($secondVal['@value']) {
141 | $values[$key] = $secondVal['@value'];
142 | }
143 | if ($secondVal['label']) {
144 | $values[$key] = $secondVal['label'];
145 | }
146 | }
147 | }
148 | }
149 | }
150 | $cleanedTitle = implode('; ', $values);
151 | $event->setParam('title', (string) $cleanedTitle);
152 | } else {
153 | $event->setParam('title', (string) $title);
154 | }
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/asset/css/nesteddatatype.css:
--------------------------------------------------------------------------------
1 | .ml {
2 | margin-left: 10px;
3 | }
4 |
5 | .nested-data-type_repeat_property {
6 | background: white;
7 | position: relative;
8 | border-bottom: 1px solid rgba(0,0,0,0.35);
9 | }
10 |
11 | .nested-data-type_handle {
12 | width: 12px;
13 | display: inline;
14 | background-color:rgba(0,0,0,0.05);
15 | border-bottom: 1px solid rgba(0,0,0,0.35);
16 | align-self: stretch;
17 | border-right: 0;
18 | margin: 0;
19 | line-height: 34px;
20 | }
21 |
22 | .nested-data-type_handle::before {
23 | content: "";
24 | font-family: "Font Awesome 5 Free";
25 | font-weight: 900;
26 | opacity: .35;
27 | font-size: 10px;
28 | line-height: 12px;
29 | }
30 |
31 |
32 | .hidden, .hidden, .hidden .value-uri {
33 | display: none;
34 | }
35 |
36 | .nested-data-type_entity_label {
37 | padding-left: 5px;
38 | font-weight: 800;
39 | background: rgba(0,0,0,0.1);
40 | border-bottom: 1px solid rgba(0,0,0,0.10);
41 | }
42 |
43 | .nested-data-type_repeat_property option, .nested-data-type_repeat_property input {
44 | color: rgb(0 0 0 / 35%);
45 | }
46 |
47 | .nested-data-type_add_property {
48 | margin-left: 10px !important;
49 | cursor: pointer;
50 | }
51 |
52 | .inputs .nested-data-type_button {
53 | margin: unset;
54 | padding: unset;
55 | background: none;
56 | box-shadow: none;
57 | }
58 |
59 | .nested-data-type_button.nested-data-type_add_property, .nested-data-type_button.nested-data-type_add_resource {
60 | background-color: rgba(0,0,0,0.08);
61 | color: #676767;
62 | border-radius: 3px;
63 | border: 0;
64 | box-shadow: 0 0 0 1px rgb(0 0 0 / 15%) inset;
65 | padding: 6px 10px;
66 | display: inline-block;
67 | text-align: center;
68 | cursor: pointer;
69 | font-size: 14px;
70 | line-height: 12px;
71 | min-height: 0px;
72 | margin-bottom: 12px;
73 | }
74 |
75 | .nested-data-type_add_property[class*="o-icon-"]::before {
76 | padding-right: 5px;
77 | }
78 |
79 | .o-icon-delete.nested-data-type_remove_property, .nested-data-type_hide_property {
80 | position: absolute;
81 | display: block;
82 | right: -25px;
83 | /* top: 50%; */
84 | top: 60px;
85 | width: fit-content;
86 | color: #cdcdcd;
87 | cursor: pointer;
88 | }
89 |
90 | .nested-data-type_hide_property {
91 | /* top: 20%; */
92 | top: 30px;
93 | right: -27px;
94 | }
95 |
96 | .nested-data-type_repeat_class {
97 | margin-left: 1px;
98 | }
99 |
100 | .nested-data-type_repeat_class input {
101 | width: calc(50% - 2px);
102 | background: #FFF8E5;
103 | }
104 |
105 | .inputs .nested-data-type_repeat_property datalist {
106 | margin: 0;
107 | border-radius: 0;
108 | }
109 |
110 | .nested-data-type_repeat_property_list {
111 | display: inline-flex;
112 | width: 100%;
113 | }
114 |
115 | .nested-data-type-dropwdown {
116 | padding-left: 6px;
117 | width: calc(100% - 30px);
118 | background: none;
119 | border: none;
120 | border-bottom: 1px solid rgba(0,0,0,0.35);
121 | background: rgba(0,0,0,0.05);
122 | height: 36px;
123 | }
124 |
125 | .hidden {
126 | display: none;
127 | }
128 |
129 | .inputs .nested-data-type_add_class {
130 | font-size: .75rem;
131 | height: fit-content;
132 | border-radius: 0;
133 | width: 30px;
134 | background: rgba(0,0,0,0.05);
135 | border-bottom: 1px solid rgba(0,0,0,0.35);
136 | vertical-align: bottom;
137 | }
138 |
139 | .nested-data-type_property_dropdown::-webkit-calendar-picker-indicator {
140 | opacity: 100;
141 | }
142 |
143 | .nested-data-type_properties .input ~ .input {
144 | border-bottom: unset;
145 | }
146 |
147 | .nested-data-type_properties .input label {
148 | z-index: 1;
149 | display: flex;
150 | /* padding: 6px 0 6px 6px; */
151 | width: auto;
152 | font-weight: bold;
153 | color: rgba(0,0,0,0.35);
154 | }
155 |
156 | .nested-data-type_properties .input textarea {
157 | margin: -6px 0 -6px 0;
158 | /* height: 36px; */
159 | /* color: #222; */
160 | border: 0;
161 | padding: 6px;
162 | font-weight: normal;
163 | background: transparent;
164 | min-height: 54px !important;
165 | }
166 |
167 | .o-title.items {
168 | line-height: 1.6rem;
169 | min-height: 54px !important;
170 | }
171 |
172 | .nested-data-type_properties .items a:after {
173 | font-family: "Font Awesome 5 Free";
174 | content: " \f1b2";
175 | }
176 |
--------------------------------------------------------------------------------
/asset/js/nesteddatatype.js:
--------------------------------------------------------------------------------
1 | $(document).on('o:prepare-value', function (e, type, value, valueObj) {
2 | const thisValue = $(value);
3 | const container = thisValue.find('.nested-data-type_properties');
4 | container.sortable()
5 | const addBtn = thisValue.find('.nested-data-type_add_property');
6 |
7 | // Add a default Value to trigger the hydrate() function
8 | const defaultValue = thisValue.find('.nested-data-type_value').val('value');
9 | const defaultProperty = thisValue.find('.nested-data-type_property').val('value');
10 |
11 | let repeatProperty, select, textareaValue, textareaUri;
12 | const findItems = () => {
13 | repeatProperty = container.find('.nested-data-type_repeat_property').last();
14 | isHidden = repeatProperty.find('.nested-data-type_is-hidden');
15 | select = repeatProperty.find('.nested-data-type_property_dropdown');
16 | textareaValue = repeatProperty.find('.property-value');
17 | textareaUri = repeatProperty.find('.property-uri');
18 | innerClass = repeatProperty.find('.inner-class');
19 | innerProperty = repeatProperty.find('.inner-property');
20 | renderedLink = repeatProperty.find('.items');
21 | }
22 |
23 | const cloneItem = (idx) => {
24 | let item = `
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
39 |
40 |
41 |
44 |
45 |
46 |
47 |
48 |
`;
49 |
50 | container.append(container.append(item));
51 | }
52 |
53 | const structureField = (obj, type, insertVal = '') => {
54 | return obj.attr({ 'data-value-key': `${type}` }).val(insertVal);
55 | }
56 |
57 | const structureInnerLinks = (insertVal, url) => {
58 | let title;
59 |
60 | $.ajaxSetup({
61 | async: false
62 | });
63 | $.getJSON(url.replace("/admin/item/", "/api/items/"), function (data) {
64 | title = data["o:title"];
65 | });
66 |
67 | let link = ``
68 | container.append(container.find('.nested-data-type_repeat_property').last().append(link));
69 | }
70 |
71 | // Replace item on click
72 | // container.on('click', '.re-link', function (e) {
73 | // e.preventDefault();
74 | // openSidebar()
75 | // });
76 |
77 | // Add item on click
78 | addBtn.on('click', function (e) {
79 | e.preventDefault();
80 | const num = container.find('.nested-data-type_repeat_property').length;
81 | cloneItem(num + 1);
82 | findItems();
83 |
84 | if (renderedLink) {
85 | renderedLink.remove();
86 | textareaValue.parent().parent().css('display', 'block');
87 | }
88 |
89 | container.find('.nested-data-type_repeat_property')
90 | .last()
91 | .find('.o-icon-private')
92 | .removeClass('o-icon-private')
93 | .addClass('o-icon-public')();
94 | });
95 |
96 | // Remove Button on click
97 | container.on('click', '.nested-data-type_remove_property', function (e) {
98 | e.preventDefault();
99 | const nextItems = $(this).parent().nextAll();
100 | $(this).parent().remove();
101 |
102 | // change the index number
103 | for (let index = 0; index < nextItems.length; index++) {
104 | const element = $(nextItems[index]).find("input[data-value-key]");
105 | for (let item = 0; item < element.length; item++) {
106 | const dataValueKey = $(element[item]).attr('data-value-key').split('-');
107 | const updatedIndex = dataValueKey.join('-') + "-" + (dataValueKey.pop() - 1);
108 | structureField($(element[item]), updatedIndex, insertVal = element[item].value)
109 | }
110 | }
111 | });
112 |
113 | // Show properties on click
114 | container.on('click', '.nested-data-type_hide_property', function (e) {
115 | e.preventDefault();
116 | const isHiddenInput = $(this).parent().find('.nested-data-type_is-hidden');
117 | const dataKey = isHiddenInput.attr('data-value-key');
118 | const hide = isHiddenInput.attr({ 'data-value-key': dataKey }).val();
119 |
120 | if (hide != "true") {
121 | $(this).removeClass('o-icon-public').addClass('o-icon-private');
122 | isHiddenInput.attr({ 'data-value-key': dataKey }).val("true");
123 | }
124 | else {
125 | $(this).removeClass('o-icon-private').addClass('o-icon-public');
126 | isHiddenInput.attr({ 'data-value-key': dataKey }).val("");
127 | }
128 | });
129 |
130 | // Add Class on click
131 | container.on('click', '.nested-data-type_add_class', function (e) {
132 | e.preventDefault();
133 | $(this).parent().next().children().val('')
134 | $(this).parent().next().toggle();
135 | });
136 |
137 |
138 | // Prepares the fields to be rendered in the frontend
139 | if (0 === type.indexOf('nesteddatatype#')) {
140 | if (valueObj != undefined) {
141 | const p = valueObj["@value"];
142 | const k = Object.keys(p[0]);
143 | renderFields(p, k)
144 | }
145 |
146 | else {
147 | let templateId = $('#resource-template-select').val();
148 | let templateUrl = `/api/resource_templates/${templateId}`
149 | getTemplateJson(templateUrl).then(function (returndata) {
150 | returndata["o:resource_template_property"].forEach(element => {
151 | if (element["o:data_type"] == type) {
152 | let p = JSON.parse(element["o:data"][0]["default_value"]);
153 | let k = Object.keys(p[0]);
154 | renderFields(p, k)
155 | }
156 | });
157 | });
158 | }
159 | }
160 |
161 | $(document).on('click', '.nested-data-type__resource_link', function (e) {
162 | e.preventDefault();
163 |
164 | const resource = JSON.parse($(this.parentElement).attr('data-resource-values'));
165 | const id = resource['@id'];
166 | const label = resource['display_title'];
167 | const url = resource['url'];
168 |
169 | if (thisValue.is('.selecting-resource')) {
170 | const num = container.find('.nested-data-type_repeat_property').length;
171 |
172 | cloneItem(num + 1);
173 | findItems();
174 |
175 | if (renderedLink) {
176 | renderedLink.remove();
177 | textareaValue.parent().parent().css('display', 'block');
178 | }
179 |
180 | structureField(textareaValue, `property-value-${num + 1}`, insertVal = label);
181 | structureField(textareaUri, `property-uri-${num + 1}`, insertVal = id);
182 |
183 | container.find('.nested-data-type_repeat_property')
184 | .last()
185 | .find('.input')
186 | .css('display', 'none');
187 |
188 | container.find('.nested-data-type_repeat_property')
189 | .last()
190 | .find('.o-title.items')
191 | .remove();
192 |
193 | container.find('.nested-data-type_repeat_property')
194 | .last()
195 | .find('.o-icon-private')
196 | .removeClass('o-icon-private')
197 | .addClass('o-icon-public')
198 |
199 | structureInnerLinks(label, url);
200 | };
201 | });
202 |
203 | function renderFields(properties, keys) {
204 | keys.forEach((element, idx) => {
205 | let item = properties[0][element];
206 |
207 | for (i in item) {
208 | let val = item[i];
209 | if (typeof val === "object") {
210 |
211 | if (idx == 1) {
212 | findItems();
213 |
214 | // select.val(element);
215 | structureField(select, `property-label-${idx}`, insertVal = element);
216 |
217 | if (val['@value']) textareaValue.val(val['@value']);
218 | if (val['label']) textareaValue.val(val['label']);
219 | if (val['@id']) textareaUri.val(val['@id']);
220 | if (val['is_hidden']) {
221 | structureField(isHidden, `is-hidden-${idx}`, insertVal = 'true');
222 | container.find('.nested-data-type_hide_property').last().removeClass('o-icon-public').addClass('o-icon-private')
223 | };
224 | if (val['@id'] && val['@id'].includes('/api/items/')) {
225 |
226 | container.find('.nested-data-type_repeat_property')
227 | .last()
228 | .find('.input')
229 | .css('display', 'none');
230 |
231 | structureInnerLinks(val['label'], val['@id'].replace('/api/items/', '/admin/item/'));
232 | }
233 | }
234 |
235 | else if (idx > 1) {
236 | cloneItem(idx);
237 | findItems();
238 |
239 | container.find('.nested-data-type_hide_property').last().removeClass('o-icon-private').addClass('o-icon-public');
240 |
241 | if (renderedLink) {
242 | renderedLink.remove();
243 | textareaValue.parent().parent().css('display', 'block');
244 | }
245 |
246 | structureField(select, `property-label-${idx}`, insertVal = element);
247 | innerClass.parent().css('display', 'none');
248 | if (val['is_hidden']) {
249 | structureField(isHidden, `is-hidden-${idx}`, insertVal = 'true');
250 | container.find('.nested-data-type_hide_property').last().removeClass('o-icon-public').addClass('o-icon-private')
251 | };
252 | if (val['@value']) { structureField(textareaValue, `property-value-${idx}`, insertVal = val['@value']); };
253 | if (val['label']) { structureField(textareaValue, `property-value-${idx}`, insertVal = val['label']); }
254 | if (val['@id']) { structureField(textareaUri, `property-uri-${idx}`, insertVal = val['@id']); }
255 | if (val['@id'] && val['@id'].includes('/api/items/')) {
256 | container.find('.nested-data-type_repeat_property')
257 | .last()
258 | .find('.o-title.items')
259 | .remove();
260 |
261 | container.find('.nested-data-type_repeat_property')
262 | .last()
263 | .find('.input')
264 | .css('display', 'none');
265 |
266 | structureInnerLinks(val['label'], val['@id'].replace('/api/items/', '/admin/item/'));
267 | }
268 | }
269 |
270 | for (const [key, value] of Object.entries(val)) {
271 | if (key == '@type') { innerClass.val(value).parent().css('display', 'block') };
272 | if (idx == 1) {
273 | if (val[key]['@value']) {
274 | innerProperty.val(key);
275 | innerProperty.parent().css('display', 'block');
276 | textareaValue.val(val[key]['@value']);
277 | }
278 | if (val[key]['@id']) {
279 | innerProperty.val(key);
280 | textareaValue.val(val[key]['@id']);
281 | }
282 | if (val[key]['label']) textareaValue.val(val[key]['label']);
283 |
284 | if (val[key]['@id'] && val[key]['@id'].includes('/api/items/')) {
285 | container.find('.nested-data-type_repeat_property')
286 | .last()
287 | .find('.o-title.items')
288 | .remove();
289 |
290 | container.find('.nested-data-type_repeat_property')
291 | .last()
292 | .find('.input')
293 | .css('display', 'none');
294 | structureInnerLinks(val[key]['label'], val[key]['@id'].replace('/api/items/', '/admin/item/'));
295 | }
296 | }
297 | else if (idx > 1 && key != "is_hidden") {
298 | structureField(innerProperty, `inner-property-${idx}`, insertVal = key);
299 | if (key == '@type') { structureField(innerClass, `inner-class-${idx}`, insertVal = value); };
300 | if (val[key]['@value']) { structureField(textareaValue, `property-value-${idx}`, insertVal = val[key]['@value']); }
301 | if (val[key]['label']) { structureField(textareaValue, `property-value-${idx}`, insertVal = val[key]['label']); }
302 | if (val[key]['@id']) { structureField(textareaUri, `property-uri-${idx}`, insertVal = val[key]['@id']); };
303 |
304 | if (val[key]['@id'] && val[key]['@id'].includes('/api/items/')) {
305 | container.find('.nested-data-type_repeat_property')
306 | .last()
307 | .find('.o-title.items')
308 | .remove();
309 |
310 | container.find('.nested-data-type_repeat_property')
311 | .last()
312 | .find('.input')
313 | .css('display', 'none');
314 |
315 | structureInnerLinks(val[key]['label'], val[key]['@id'].replace('/api/items/', '/admin/item/'));
316 | }
317 | }
318 | }
319 | }
320 | }
321 | });
322 |
323 | }
324 | });
325 |
326 |
327 | function getTemplateJson(url) {
328 | return $.getJSON(url, function (data) {
329 | return data
330 | });
331 | }
332 |
333 | // function openSidebar() {
334 | // let sidebar = $(".sidebar");
335 | // sidebar.addClass('active');
336 | // sidebar.trigger('o:sidebar-opened');
337 | // if ($('.active.sidebar').length > 1) {
338 | // var highestIndex = 3; // The CSS currently defines the default sidebar z-index as 3.
339 | // $('.active.sidebar').each(function () {
340 | // var currentIndex = parseInt($(this).css('zIndex'), 10);
341 | // if (currentIndex > highestIndex) {
342 | // highestIndex = currentIndex;
343 | // }
344 | // });
345 | // sidebar.css('zIndex', highestIndex + 1);
346 |
347 | // // populate
348 | // var sidebarContent = sidebar.find('.sidebar-content');
349 | // sidebar.addClass('loading');
350 | // sidebarContent.empty();
351 |
352 | // $.get("/admin/nested-data-type/sidebar-select")
353 | // .done(function (data) {
354 | // sidebarContent.html(data);
355 | // $(sidebar).trigger('o:sidebar-content-loaded');
356 | // })
357 | // .fail(function () {
358 | // sidebarContent.html('' + Omeka.jsTranslate('Something went wrong') + '
');
359 | // })
360 | // .always(function () {
361 | // sidebar.removeClass('loading');
362 | // });
363 | // }
364 | // }
365 |
--------------------------------------------------------------------------------
/config/module.config.php:
--------------------------------------------------------------------------------
1 | [
4 | 'abstract_factories' => ['NestedDataType\Service\NestedDataTypeFactory'],
5 | ],
6 | 'controllers' => [
7 | 'invokables' => [
8 | 'NestedDataType\Controller\Index' => 'NestedDataType\Controller\IndexController',
9 | ],
10 | ],
11 | 'view_manager' => [
12 | 'template_path_stack' => [
13 | OMEKA_PATH . '/modules/NestedDataType/view',
14 | ],
15 | ],
16 | 'router' => [
17 | 'routes' => [
18 | 'admin' => [
19 | 'child_routes' => [
20 | 'nested-data-type' => [
21 | 'type' => 'Literal',
22 | 'options' => [
23 | 'route' => '/nested-data-type/sidebar-select',
24 | 'defaults' => [
25 | '__NAMESPACE__' => 'NestedDataType\Controller',
26 | 'controller' => 'Index',
27 | 'action' => 'sidebar-select',
28 | ],
29 | ],
30 | 'may_terminate' => true,
31 | ],
32 | ],
33 | ],
34 | ],
35 | ],
36 | ];
37 |
--------------------------------------------------------------------------------
/config/module.ini:
--------------------------------------------------------------------------------
1 | [info]
2 | name = "Nested Data Types"
3 | description= "Allow users to choose a specific resource class as datatype, and inner properties."
4 | author = "Giacomo Nanni"
5 | author_link = "https://github.com/sinanatra"
6 | version = "4.0.0"
7 | omeka_version_constraint = "^4.0.0"
8 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Nested Data Type module for Omeka S
2 |
3 | This module allows the user to choose a specific resource class as datatype while editing resource templates. Additionally, when editing an item, the user can select inner properties for the chosen class.
4 |
5 | It was initially designed to work with the nested CIDOC-CRM structure in Omeka S, avoiding creating an abundance of Omeka resources.
6 | However, it should only replace an Omeka resource, literal, or URI when strictly necessary.
7 |
8 | For instance:
9 |
10 | - crm:P43_has_dimension
11 | - crm:E54_Dimension
12 | - crm:P2_has_type
13 | - crm:P90_has_value
14 | - crm:P91_has_unit
15 |
16 | Can be structured as a multi input field:
17 |
18 | 
19 |
20 | The module fills the content of `@value` as json-ld based on the values from the multi input field.
21 | Specific keys can be ignored when rendering the `@value` with the `"is_hidden"` property, which can be activated by clicking on the eye icon.
22 |
23 | ```json
24 | {
25 | "type": "nesteddatatype#crm:E54_Dimension",
26 | "property_id": 1262,
27 | "property_label": "P43 has dimension",
28 | "is_public": true,
29 | "@value": [
30 | {
31 | "@type": "crm:E54_Dimension",
32 | "crm:P2_has_type": [
33 | {
34 | "@id": "http://localhost:8080/api/items/23685",
35 | "label": "width"
36 | }
37 | ],
38 | "crm:P90_has_value": [
39 | {
40 | "@value": "90"
41 | }
42 | ],
43 | "crm:P91_has_unit": [
44 | {
45 | "@id": "http://localhost:8080/api/items/23686",
46 | "label": "centimeters"
47 | }
48 | ]
49 | }
50 | ]
51 | }
52 | ```
53 |
54 | The content of `@value` is structured as JSON-LD that can be easily converted to other semantic web standars:
55 |
56 | ### RDFXML:
57 | ```xml
58 |
59 |
60 |
61 | 90
62 |
63 |
64 |
65 | ```
66 | ### Turtle:
67 | ```
68 | @prefix ns0: .
69 | @prefix xsd: .
70 |
71 | [] ns0:P43_has_dimension [
72 | a ns0:E54_Dimension ;
73 | ns0:P2_has_type ;
74 | ns0:P90_has_value "90"^^xsd:string ;
75 | ns0:P91_has_unit
76 | ] .
77 | ```
78 |
79 | ### N-Triples:
80 | ```
81 | _:genid1 _:genid2 .
82 | _:genid2 .
83 | _:genid2 .
84 | _:genid2 "90"^^ .
85 | _:genid2 .
86 | ```
87 |
88 | ## Installation
89 |
90 | * See general end user documentation for [Installing a module](http://omeka.org/s/docs/user-manual/modules/#installing-modules)
91 |
92 | ## Usage
93 |
94 | * Activate the NestedDataType module.
95 | * On the resource template, you can choose a resource class from previously installed vocabularies as datatype.
96 | * When you create or edit an item based on the template, the sidebar suggest you the specific class choose above.
97 | * A dropdown will help you in chosing an inner property to insert.
98 |
99 | ## Contribute
100 |
101 | Feel free to submit any issue or request.
102 |
103 | This project is still in development and I use it for my own projects. Don't use it on a production website if you're not sure of being able to correct my bugs.
104 |
--------------------------------------------------------------------------------
/src/Controller/IndexController.php:
--------------------------------------------------------------------------------
1 | setBrowseDefaults('created');
13 |
14 | $response = $this->api()->search('items', $this->params()->fromQuery());
15 | $this->paginator($response->getTotalResults());
16 |
17 | $view = new ViewModel;
18 | $view->setVariable('items', $response->getContent());
19 | $view->setVariable('search', $this->params()->fromQuery('search'));
20 | $view->setVariable('resourceClassId', $this->params()->fromQuery('resource_class_id'));
21 | $view->setVariable('itemSetId', $this->params()->fromQuery('item_set_id'));
22 | $view->setVariable('showDetails', false);
23 | $view->setTerminal(true);
24 | $view->setTemplate('/nested-data-type/item/sidebar-select');
25 | return $view;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/DataType/NestedDataType.php:
--------------------------------------------------------------------------------
1 | resourceClass = $resourceClass;
30 | $this->properties = $properties;
31 | $this->classes = $classes;
32 | }
33 |
34 | public function getName()
35 | {
36 | return 'nesteddatatype#'.$this->resourceClass->term();
37 | }
38 |
39 | public function getLabel()
40 | {
41 | return $this->resourceClass->term();
42 | }
43 |
44 | public function getOptgroupLabel()
45 | {
46 | return 'Nested Class'; // @translate
47 | }
48 |
49 | public function form(PhpRenderer $view)
50 | {
51 | return $view->partial('nested-data-type/data-type/nested-data-type', [
52 | 'dataType' => $this->getName(),
53 | 'label' => $this->getLabel(),
54 | 'properties' => $this->properties,
55 | 'classes' => $this->classes,
56 | 'resource' => $view->resource,
57 | ]);
58 | }
59 |
60 | public function getJsonLd(ValueRepresentation $value)
61 | {
62 | $values= json_decode($value->value(), true);
63 | $simpleValue = [];
64 |
65 | foreach ($values[0] as $key => $val) {
66 | if (is_array($val) || is_object($val)){
67 | foreach ($val as $innerKey => $innerVal) {
68 | if(!isset($innerVal['is_hidden'])){
69 | if(isset($innerVal['@value'])){
70 | $simpleValue[$key] = $innerVal['@value'];
71 | continue;
72 | }
73 | if(isset($innerVal['label'])){
74 | $simpleValue[$key] = $innerVal['label'];
75 | continue;
76 | }
77 | if (is_array($innerVal) || is_object($innerVal)){
78 | foreach ($innerVal as $secondKey => $secondVal) {
79 | if(isset($secondVal['@value'])){
80 | $simpleValue[$key] = $secondVal['@value'];
81 | }
82 | if(isset($secondVal['label'])){
83 | $simpleValue[$key] = $secondVal['label'];
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 |
92 | $jsonLd = [
93 | '@value' => $values,
94 | 'simpleValue' => implode('; ', $simpleValue),
95 | ];
96 |
97 | return $jsonLd;
98 | }
99 |
100 | /**
101 | * @param array $valueObject
102 | */
103 | public function isValid(array $valueObject){
104 |
105 | // $labels = array_map(
106 | // function ($prop){
107 | // return $prop->term();
108 | // },
109 | // $this->properties
110 | // );
111 |
112 | // foreach($valueObject as $key => $label) {
113 | // if (strpos($key, 'property-label') !== false) {
114 | // if(!in_array($label, $labels)){
115 | // return false;
116 | // }
117 | // }
118 | // }
119 |
120 | return true;
121 | }
122 |
123 | public function hydrate(array $valueObject, Value $value, AbstractEntityAdapter $adapter){
124 | $serviceLocator = $adapter->getServiceLocator();
125 |
126 | $prevLabel = '';
127 | $num = 0;
128 |
129 | if(strpos($valueObject['@value'],'@type') !== false){
130 | $value->setValue(json_encode($valueObject['@value']));
131 | }
132 |
133 |
134 | else {
135 | $properties = [];
136 |
137 | $properties = array_merge(
138 | ["@type" => $this->getLabel() ]
139 | );
140 |
141 |
142 | foreach($valueObject as $key => $label) {
143 |
144 | if ($prevLabel == $label) {
145 | $num += 1;
146 | }
147 |
148 | if (substr($key,0,15) !== 'property-label-'){
149 | continue;
150 | }
151 |
152 | $idx = (int) substr($key,15);
153 | $val = $valueObject["property-value-$idx"];
154 | $uri = $valueObject["property-uri-$idx"];
155 |
156 | // Update title from Omeka Id - to be cleaned.
157 | if (strpos($uri, '/api/items/') !== false) {
158 | try {
159 | $str = explode("/api/items/", $uri);
160 | $api = $serviceLocator->get('Omeka\ApiManager');
161 | $response = $api->read('items', $str[1]);
162 | $val = $response->getContent()->displayTitle();
163 | } catch (\Throwable $th) {}
164 | }
165 |
166 | $innerClass = $valueObject["inner-class-$idx"];
167 | $innerProp = $valueObject["inner-property-$idx"];
168 | $isHidden = $valueObject["is-hidden-$idx"];
169 |
170 | $prevLabel = $label;
171 |
172 | if ($innerClass && $innerProp){
173 | $properties[$label][$num] = array_merge(
174 | ["@type" => $innerClass ],
175 | [$innerProp => $uri ? ['@id' => $uri, 'label' => $val] : ['@value' => $val] ],
176 | $isHidden ? ['is_hidden' => $isHidden] : []
177 | );
178 | } else {
179 | $properties[$label][$num] = array_merge(
180 | $uri ? ['@id' => $uri, 'label' => $val] : ['@value' => $val],
181 | $isHidden ? ['is_hidden' => $isHidden] : []
182 | );
183 | }
184 | }
185 |
186 | $value->setValue(json_encode([$properties]));
187 | $prevLabel .= $label;
188 | }
189 | }
190 |
191 | public function render(PhpRenderer $view, ValueRepresentation $value){
192 |
193 | $values = json_decode($value->value(), true);
194 | $simpleValue = [];
195 |
196 | foreach ($values[0] as $key => $val) {
197 |
198 | if($key == '@type'){
199 | $simpleValue[$key] = $val;
200 | }
201 |
202 | if (is_array($val) || is_object($val)){
203 | foreach ($val as $innerKey => $innerVal) {
204 | if(!isset($innerVal['is_hidden'])){
205 | if(isset($innerVal['@value'])){
206 | $simpleValue[$key] = $innerVal['@value'];
207 | continue;
208 | }
209 | if(isset($innerVal['label'])){
210 | $simpleValue[$key] = $innerVal;
211 | continue;
212 | }
213 | if (is_array($innerVal) || is_object($innerVal)){
214 | foreach ($innerVal as $secondKey => $secondVal) {
215 | if(isset($secondVal['@value'])){
216 | $simpleValue[$key] = $secondVal['@value'];
217 | }
218 | if(isset($secondVal['label'])){
219 | $simpleValue[$key] = $secondVal;
220 | }
221 | }
222 | }
223 | }
224 | }
225 | }
226 | }
227 |
228 | // return json_encode($values);
229 | return "" . implode('', array_map(
230 | function ($v, $k) {
231 |
232 | if($k == '@type'){
233 | return "
" . str_replace("_", " ", explode(":", $v)[1]) . "
";
234 | }
235 |
236 | if(isset($v['label'])){
237 | $url = explode("items/", $v['@id'])[1];
238 | $v = "
" . $v['label']. "";
239 | }
240 | else {
241 | $v = "
" . $v . "";
242 | }
243 |
244 | $k = "
" . str_replace("_", " ", explode(":", $k)[1]) . "";
245 |
246 | return "
" . $k . "" .": " . "" . $v . "
";
247 | },
248 | $simpleValue,
249 | array_keys($simpleValue)
250 | )). "
";
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/src/Exception/NestedDataTypeFactory.php:
--------------------------------------------------------------------------------
1 | get('Omeka\ApiManager');
24 | $resourceClasses = $apiManager->search('resource_classes', ['term' => $term])->getContent();
25 |
26 | $vocabularyId = $resourceClasses[0]->vocabulary()->id();
27 | $properties = $apiManager->search('properties', ['vocabulary_id' => $vocabularyId])->getContent();
28 | $classes = $apiManager->search('resource_classes', ['vocabulary_id' => $vocabularyId])->getContent();
29 |
30 | $i = new NestedDataType($resourceClasses[0], $properties, $classes);
31 |
32 | return $i;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/view/nested-data-type/data-type/nested-data-type.phtml:
--------------------------------------------------------------------------------
1 | = $label ?>
2 |
3 |
4 |
5 |
10 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Link Item
45 |
46 |
--------------------------------------------------------------------------------
/view/nested-data-type/item/sidebar-select.phtml:
--------------------------------------------------------------------------------
1 | plugin('translate');
3 | $escape = $this->plugin('escapeHtml');
4 | $hyperlink = $this->plugin('hyperlink');
5 | $itemsFound = count($items) > 0;
6 | $expanded = $resourceClassId || $itemSetId || $id;
7 | ?>
8 |
9 |
10 |
11 |
12 |
13 |
58 |
59 | pagination('common/sidebar-pagination.phtml'); ?>
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 | %s',
75 | $this->thumbnail($item, 'square'),
76 | $escape($item->displayTitle())
77 | );
78 | $attrs = [
79 | 'class' => 'select-resource resource-link nested-data-type__resource_link',
80 | ];
81 | echo $hyperlink->raw($content, '#', $attrs);
82 | ?>
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------