PageRenderTime 64ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/sandbox/semantic-interaction/trunk/lib/vie/vie.js

http://iks-project.googlecode.com/
JavaScript | 1202 lines | 666 code | 149 blank | 387 comment | 105 complexity | 57daebba4d1ce5f7f67d9216799129e6 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. // VIE - Vienna IKS Editables
  2. // (c) 2011 Henri Bergius, IKS Consortium
  3. // VIE may be freely distributed under the MIT license.
  4. // For all details and documentation:
  5. // http://wiki.iks-project.eu/index.php/VIE
  6. //
  7. // [VIE](http://wiki.iks-project.eu/index.php/VIE) enables you to make
  8. // [RDFa](http://en.wikipedia.org/wiki/RDFa) -annotated content on your
  9. // web pages editable.
  10. //
  11. // For example, if your page contains the following mark-up:
  12. //
  13. // <p xmlns:dc="http://purl.org/dc/elements/1.1/"
  14. // about="http://www.example.com/books/wikinomics">
  15. // In his latest book
  16. // <cite property="dc:title">Wikinomics</cite>,
  17. // <span property="dc:creator">Don Tapscott</span>
  18. // explains deep changes in technology,
  19. // demographics and business.
  20. // The book is due to be published in
  21. // <span property="dc:date" content="2006-10-01">October 2006</span>.
  22. // </p>
  23. //
  24. // Then with VIE you can get a proper Backbone Model object for that. First
  25. // scan your page for RDFa entities:
  26. //
  27. // VIE.RDFaEntities.getInstances();
  28. //
  29. // And then just access the entity by subject:
  30. //
  31. // var myBook = VIE.EntityManager.getBySubject('<http://www.example.com/books/wikinomics>');
  32. // alert(myBook.get('dc:title')); // "Wikinomics"
  33. //
  34. // Properties of the entity may also be modified, and these changes will
  35. // also happen on the page itself:
  36. //
  37. // myBook.set({'dc:title':'Wikinomics, Second Edition'});
  38. //
  39. // You can also access the entities via the `VIE.EntityManager.entities` Backbone Collection.
  40. (function() {
  41. // Initial setup
  42. // -------------
  43. //
  44. // The VIE library is fully contained inside a `VIE` namespace. The
  45. // namespace is available in both CommonJS and the browser.
  46. var VIE;
  47. if (typeof exports !== 'undefined') {
  48. VIE = exports;
  49. } else {
  50. VIE = this.VIE = {};
  51. }
  52. // ### Handle dependencies
  53. //
  54. // VIE tries to load its dependencies automatically.
  55. // Please note that this autoloading functionality only works on the server.
  56. // On browser Backbone needs to be included manually.
  57. // Require [underscore.js](http://documentcloud.github.com/underscore/)
  58. // using CommonJS require if it isn't yet loaded.
  59. //
  60. // On node.js underscore.js can be installed via:
  61. //
  62. // npm install underscore
  63. var _ = this._;
  64. if (!_ && (typeof require !== 'undefined')) { _ = require('underscore')._; }
  65. if (!_) {
  66. throw 'VIE requires underscore.js to be available';
  67. }
  68. // Require [Backbone.js](http://documentcloud.github.com/backbone/)
  69. // using CommonJS require if it isn't yet loaded.
  70. //
  71. // On node.js Backbone.js can be installed via:
  72. //
  73. // npm install backbone
  74. var Backbone = this.Backbone;
  75. if (!Backbone && (typeof require !== 'undefined')) { Backbone = require('backbone'); }
  76. if (!Backbone) {
  77. throw 'VIE requires Backbone.js to be available';
  78. }
  79. // Require [jQuery](http://jquery.com/) using CommonJS require if it
  80. // isn't yet loaded.
  81. //
  82. // On node.js jQuery can be installed via:
  83. //
  84. // npm install jquery
  85. var jQuery = this.jQuery;
  86. if (!jQuery && (typeof require !== 'undefined')) { jQuery = require('jquery'); }
  87. if (!jQuery) {
  88. throw 'VIE requires jQuery to be available';
  89. }
  90. // VIE.EntityManager
  91. // -------------
  92. //
  93. // VIE.EntityManager keeps track of all RDFa entities loaded via VIE. This
  94. // means that entities matched by a common subject can be treated as singletons.
  95. //
  96. // It is possible to access all loaded entities via the
  97. // `VIE.EntityManager.entities` Backbone Collection.
  98. VIE.EntityManager = {
  99. entities: null,
  100. initializeCollection: function() {
  101. if (VIE.EntityManager.entities === null) {
  102. VIE.EntityManager.entities = new VIE.RDFEntityCollection();
  103. }
  104. },
  105. // ### VIE.EntityManager.getBySubject
  106. //
  107. // It is possible to get an entity that has been loaded from the page
  108. // via the `getBySubject` method. If the entity cannot be found this method
  109. // will return `null`.
  110. //
  111. // The entities accessed this way are singletons, so multiple calls to same
  112. // subject will all return the same `VIE.RDFEntity` instance.
  113. //
  114. // Subjects may be either wrapped in `<` and `>` or not.
  115. //
  116. // Example:
  117. //
  118. // var myBook = VIE.EntityManager.getBySubject('<http://www.example.com/books/wikinomics>');
  119. getBySubject: function(subject) {
  120. VIE.EntityManager.initializeCollection();
  121. if (typeof subject === 'string' &&
  122. VIE.RDFa._isReference(subject)) {
  123. subject = VIE.RDFa._fromReference(subject);
  124. }
  125. if (typeof subject === 'object')
  126. {
  127. return VIE.EntityManager.entities.detect(function(item) {
  128. if (item.id === subject) {
  129. return true;
  130. }
  131. return false;
  132. });
  133. }
  134. return VIE.EntityManager.entities.get(subject);
  135. },
  136. // ### VIE.EntityManager.getByType
  137. //
  138. // Get list of RDF Entities matching the given type.
  139. getByType: function(type) {
  140. VIE.EntityManager.initializeCollection();
  141. if (VIE.RDFa._isReference(type)) {
  142. type = VIE.RDFa._fromReference(type);
  143. }
  144. return VIE.EntityManager.entities.select(function(entity) {
  145. if (entity.type === type) {
  146. return true;
  147. }
  148. return false;
  149. });
  150. },
  151. // ### VIE.EntityManager.getPredicate
  152. //
  153. // Get requested predicate from all loaded entities.
  154. getPredicate: function(predicate) {
  155. var predicates = [];
  156. _.forEach(VIE.EntityManager.entities.pluck('dcterms:hasPart'), function(property) {
  157. if (property) {
  158. predicates.push(property);
  159. }
  160. });
  161. return predicates;
  162. },
  163. // ### VIE.EntityManager.getByJSONLD
  164. //
  165. // Another way to get or load entities is by passing EntityManager a valid
  166. // JSON-LD object.
  167. //
  168. // This can be either called with a JavaScript object representing JSON-LD,
  169. // or with a JSON-LD string.
  170. //
  171. // Example:
  172. //
  173. // var json = '{"@": "<http://www.example.com/books/wikinomics>","dc:title": "Wikinomics","dc:creator": "Don Tapscott","dc:date": "2006-10-01"}';
  174. // var objectInstance = VIE.EntityManager.getByJSONLD(json);
  175. getByJSONLD: function(jsonld, options) {
  176. VIE.EntityManager.initializeCollection();
  177. var entityInstance;
  178. var properties;
  179. if (typeof jsonld !== 'object') {
  180. try {
  181. jsonld = jQuery.parseJSON(jsonld);
  182. } catch (e) {
  183. return null;
  184. }
  185. }
  186. // The entities accessed this way are singletons, so multiple calls
  187. // to same subject (`@` in JSON-LD) will all return the same
  188. // `VIE.RDFEntity` instance.
  189. if (typeof jsonld['@'] !== 'undefined') {
  190. entityInstance = VIE.EntityManager.getBySubject(jsonld['@']);
  191. }
  192. if (entityInstance) {
  193. properties = VIE.EntityManager._JSONtoProperties(jsonld, entityInstance.attributes, entityInstance.id);
  194. entityInstance.set(properties, options);
  195. if (!entityInstance.type &&
  196. typeof jsonld.a !== 'undefined') {
  197. entityInstance.type = VIE.RDFa._fromReference(jsonld.a);
  198. }
  199. return entityInstance;
  200. }
  201. properties = VIE.EntityManager._JSONtoProperties(jsonld, {}, VIE.EntityManager._normalizeSubject(jsonld['@']));
  202. entityInstance = new VIE.RDFEntity(properties);
  203. // Namespace prefixes are handled by the `#` property of JSON-LD.
  204. // We map this to the `namespaces` property of our `VIE.RDFEntity`
  205. // instance.
  206. if (typeof jsonld['#'] !== 'undefined') {
  207. entityInstance.namespaces = jsonld['#'];
  208. }
  209. // Types are handled by the `a` property of JSON-LD. We map this
  210. // to the `type` property of our `VIE.RDFEntity` instance.
  211. if (typeof jsonld.a !== 'undefined') {
  212. entityInstance.type = VIE.RDFa._fromReference(jsonld.a);
  213. }
  214. // Normalize the subject, handling both proper JSON-LD and JSON-LD
  215. // anonymous entities extracted from RDFa
  216. entityInstance.id = VIE.EntityManager._normalizeSubject(jsonld['@']);
  217. VIE.EntityManager.registerModel(entityInstance);
  218. return entityInstance;
  219. },
  220. // All new entities must be added to the `entities` collection.
  221. registerModel: function(model) {
  222. model.id = VIE.EntityManager._normalizeSubject(model.id);
  223. if (VIE.EntityManager.entities.indexOf(model) === -1) {
  224. VIE.EntityManager.entities.add(model);
  225. }
  226. },
  227. _normalizeSubject: function(subject) {
  228. // Subjects are handled by the `@` property of JSON-LD. We map this
  229. // to the `id` property of our `VIE.RDFEntity` instance.
  230. if (typeof subject === 'string') {
  231. if (VIE.RDFa._isReference(subject)) {
  232. subject = VIE.RDFa._fromReference(subject);
  233. }
  234. return subject;
  235. }
  236. // When handling anonymous entities coming from RDFa, we keep their
  237. // containing element as the ID so they can be matched
  238. if (typeof subject === 'object') {
  239. return subject;
  240. }
  241. return undefined;
  242. },
  243. // Create a list of Models for referenced properties
  244. _referencesToModels: function(value) {
  245. if (!_.isArray(value)) {
  246. value = [value];
  247. }
  248. var models = [];
  249. _.forEach(value, function(subject) {
  250. models.push(VIE.EntityManager.getByJSONLD({
  251. '@': subject
  252. }));
  253. });
  254. return models;
  255. },
  256. // Helper for cleaning up JSON-LD so that it can be used as properties
  257. // of a Backbone Model.
  258. _JSONtoProperties: function(jsonld, instanceProperties, instanceId) {
  259. var properties;
  260. var references;
  261. var property;
  262. properties = jQuery.extend({}, jsonld);
  263. delete properties['@'];
  264. delete properties.a;
  265. delete properties['#'];
  266. _.each(properties, function(propertyValue, property) {
  267. if (VIE.RDFa._isReference(propertyValue) ||
  268. typeof propertyValue === 'object') {
  269. references = VIE.EntityManager._referencesToModels(propertyValue);
  270. if (instanceProperties[property] instanceof VIE.RDFEntityCollection) {
  271. // Object already has this reference collection, keep it
  272. // and add new references
  273. jQuery.each(references, function() {
  274. if (instanceProperties[property].indexOf(this) === -1) {
  275. instanceProperties[property].add(this);
  276. }
  277. });
  278. properties[property] = instanceProperties[property];
  279. }
  280. else {
  281. properties[property] = new VIE.RDFEntityCollection(references);
  282. if (instanceId) {
  283. properties[property].subject = VIE.EntityManager._normalizeSubject(instanceId);
  284. properties[property].predicate = property;
  285. }
  286. }
  287. }
  288. });
  289. return properties;
  290. },
  291. // Helper for removing existing information about loaded entities.
  292. cleanup: function() {
  293. VIE.EntityManager.entities = new VIE.RDFEntityCollection();
  294. }
  295. };
  296. // VIE.RDFEntity
  297. // -------------
  298. //
  299. // VIE.RDFEntity defines a common [Backbone Model](http://documentcloud.github.com/backbone/#Model)
  300. // for RDF entities handled in VIE.
  301. //
  302. // Attributes that are references to other entities are exposed as
  303. // `VIE.RDFEntityCollection` objects containing those entities.
  304. VIE.RDFEntity = Backbone.Model.extend({
  305. namespaces: {},
  306. type: '',
  307. // Get the subject of a RDF entity. For persistent entities full URL
  308. // subjects will be returned wrapped in `<` and `>`.
  309. // For non-persistent entities an anonymous `_:bnodeX` will be returned,
  310. // with `X` matching the local `cid` number of the entity instance.
  311. //
  312. // CURIEs will be returned as-is.
  313. getSubject: function() {
  314. if (typeof this.id === 'string') {
  315. if (this.id.substr(0, 7) === 'http://') {
  316. return VIE.RDFa._toReference(this.id);
  317. }
  318. return this.id;
  319. }
  320. return this.cid.replace('c', '_:bnode');
  321. },
  322. // VIE's entities have a method for generating [JSON-LD](http://json-ld.org/)
  323. // representations of themselves. JSON-LD is a lightweight format for handling
  324. // Linked Data (RDF) information.
  325. //
  326. // Using the book example from above, we could call:
  327. //
  328. // myBook.toJSONLD();
  329. //
  330. // And we would get a JSON object looking like the following:
  331. //
  332. // {
  333. // '@': '<http://www.example.com/books/wikinomics>',
  334. // 'dc:title': 'Wikinomics',
  335. // 'dc:creator': 'Don Tapscott',
  336. // 'dc:date': '2006-10-01'
  337. // }
  338. //
  339. // Calling `JSON.stringify()` for this object would produce:
  340. //
  341. //
  342. // {
  343. // "@": "<http://www.example.com/books/wikinomics>",
  344. // "dc:title": "Wikinomics",
  345. // "dc:creator": "Don Tapscott",
  346. // "dc:date": "2006-10-01"
  347. // }
  348. toJSONLD: function() {
  349. var instance = this;
  350. var instanceLD = {};
  351. var property;
  352. _.each(instance.attributes, function(attributeValue, property) {
  353. attributeValue = instance.get(property);
  354. if (attributeValue instanceof VIE.RDFEntityCollection) {
  355. instanceLD[property] = attributeValue.map(function(referenceInstance) {
  356. if (referenceInstance.id) {
  357. return VIE.RDFa._toReference(referenceInstance.id);
  358. } else {
  359. return referenceInstance.cid.replace('c', '_:bnode');
  360. }
  361. });
  362. } else {
  363. instanceLD[property] = attributeValue;
  364. }
  365. });
  366. instanceLD['@'] = instance.getSubject();
  367. if (instance.namespaces.length > 0) {
  368. instanceLD['#'] = instance.namespaces;
  369. }
  370. if (instance.type) {
  371. instanceLD.a = VIE.RDFa._toReference(instance.type);
  372. }
  373. return instanceLD;
  374. }
  375. });
  376. // VIE.RDFEntityCollection
  377. // -----------------------
  378. //
  379. // VIE.RDFEntityCollection defines a common [Backbone Collection](http://documentcloud.github.com/backbone/#Collection)
  380. // for references to RDF entities handled in VIE.
  381. VIE.RDFEntityCollection = Backbone.Collection.extend({
  382. model: VIE.RDFEntity,
  383. initialize: function() {
  384. this.bind('add', this.registerItem);
  385. },
  386. registerItem: function(entityInstance, collection) {
  387. if (collection === VIE.EntityManager.entities) {
  388. return;
  389. }
  390. VIE.EntityManager.registerModel(entityInstance);
  391. }
  392. });
  393. // VIE.RDFaEntities
  394. // -------------
  395. //
  396. // VIE.RDFaEntities provide mapping between RDFa on a page and Backbone Views.
  397. // When you load RDFa entities from a page, new `VIE.RDFEntity` objects will
  398. // be instantiated for them, and the DOM element the RDFa comes from will
  399. // be registered as a `VIE.RDFaView` instance.
  400. //
  401. // If you're working with RDFa -annotated content and want to access it as
  402. // Backbone Models, then VIE.RDFaEntities is the main access point.
  403. VIE.RDFaEntities = {
  404. // RDFaEntities manages a list of Views so that every view instance will be
  405. // a singleton.
  406. Views: [],
  407. CollectionViews: [],
  408. // ### VIE.RDFaEntities.getInstance
  409. //
  410. // The `getInstance` method can be used for retrieving a single Backbone
  411. // Model for a given RDFa -annotated DOM element. It accepts
  412. // [jQuery selectors](http://api.jquery.com/category/selectors/)
  413. // and returns a `VIE.RDFEntity` instance matching the content. If no valid
  414. // RDFa entities can be found from the element, then it returns `null`.
  415. //
  416. // Example:
  417. //
  418. // var myBook = VIE.RDFaEntities.getInstance('p[about]');
  419. // alert(myBook.get('dc:title')); // "Wikinomics"
  420. getInstance: function(element) {
  421. element = jQuery(element);
  422. var entityInstance;
  423. var jsonld;
  424. jsonld = VIE.RDFa.readEntity(element);
  425. if (!jsonld) {
  426. return null;
  427. }
  428. entityInstance = VIE.EntityManager.getByJSONLD(jsonld);
  429. VIE.RDFaEntities._registerView(entityInstance, element);
  430. return entityInstance;
  431. },
  432. _getViewInstance: function(element, collection) {
  433. var viewInstance;
  434. var viewArray = VIE.RDFaEntities.Views;
  435. element = jQuery(element);
  436. if (collection) {
  437. viewArray = VIE.RDFaEntities.CollectionViews;
  438. }
  439. jQuery.each(viewArray, function() {
  440. if (this.el.get(0) === element.get(0)) {
  441. viewInstance = this;
  442. return false;
  443. }
  444. });
  445. return viewInstance;
  446. },
  447. // Helper for registering views for a collection
  448. _registerCollectionView: function(collectionInstance, element) {
  449. var viewInstance;
  450. var template;
  451. element = jQuery(element);
  452. // Check whether we already have a View instantiated for the DOM element
  453. viewInstance = VIE.RDFaEntities._getViewInstance(element, true);
  454. if (viewInstance) {
  455. return viewInstance;
  456. }
  457. template = element.children(':first-child');
  458. viewInstance = new VIE.RDFaCollectionView({
  459. collection: collectionInstance,
  460. model: collectionInstance.model,
  461. el: element,
  462. elementTemplate: template,
  463. tagName: element.get(0).nodeName
  464. });
  465. VIE.RDFaEntities.CollectionViews.push(viewInstance);
  466. return viewInstance;
  467. },
  468. // Helper for registering views for an entity
  469. _registerView: function(entityInstance, element) {
  470. var viewInstance;
  471. element = jQuery(element);
  472. // Check whether we already have a View instantiated for the DOM element
  473. viewInstance = VIE.RDFaEntities._getViewInstance(element);
  474. if (viewInstance) {
  475. return viewInstance;
  476. }
  477. viewInstance = new VIE.RDFaView({
  478. model: entityInstance,
  479. el: element,
  480. tagName: element.get(0).nodeName
  481. });
  482. VIE.RDFaEntities.Views.push(viewInstance);
  483. // Find collection elements, and create collection views for them
  484. _.each(entityInstance.attributes, function(attributeValue, property) {
  485. attributeValue = entityInstance.get(property);
  486. if (attributeValue instanceof VIE.RDFEntityCollection) {
  487. jQuery.each(VIE.RDFa._getElementByPredicate(property, element), function() {
  488. VIE.RDFaEntities._registerCollectionView(attributeValue, this);
  489. });
  490. }
  491. });
  492. return viewInstance;
  493. },
  494. // ### VIE.RDFaEntities.getInstances
  495. //
  496. // Get a list of Backbone Model instances for all RDFa-marked content in
  497. // an element. The method accepts [jQuery selectors](http://api.jquery.com/category/selectors/)
  498. // as argument. If no selector is given, then the whole HTML document will
  499. // be searched.
  500. //
  501. // Example:
  502. //
  503. // var allInstances = VIE.RDFaEntities.getInstances();
  504. // alert(allInstances[0].get('dc:title')); // "Wikinomics"
  505. getInstances: function(element) {
  506. var entities = [];
  507. var entity;
  508. if (typeof element === 'undefined') {
  509. element = jQuery(document);
  510. }
  511. jQuery(VIE.RDFa.subjectSelector, element).add(jQuery(element).filter(VIE.RDFa.subjectSelector)).each(function() {
  512. entity = VIE.RDFaEntities.getInstance(this);
  513. if (entity) {
  514. entities.push(entity);
  515. }
  516. });
  517. return entities;
  518. },
  519. // Helper for removing existing references to Views loaded for RDFa entities.
  520. cleanup: function() {
  521. VIE.RDFaEntities.Views = [];
  522. }
  523. };
  524. // VIE.RDFaView
  525. // -------------
  526. //
  527. // VIE.RDFaView defines a common [Backbone View](http://documentcloud.github.com/backbone/#View)
  528. // for all RDFa -annotated elements on a page that have been loaded as
  529. // `VIE.RDFEntity` objects.
  530. //
  531. // In normal operation, the RDFaView objects are automatically handled by
  532. // `VIE.RDFaEntities`.
  533. VIE.RDFaView = Backbone.View.extend({
  534. // We ensure view gets updated when properties of the Entity change.
  535. initialize: function() {
  536. _.bindAll(this, 'render');
  537. this.model.bind('change', this.render);
  538. },
  539. // Rendering a view means writing the properties of the Entity back to
  540. // the element containing our RDFa annotations.
  541. render: function() {
  542. VIE.RDFa.writeEntity(this.el, this.model.toJSONLD());
  543. return this;
  544. }
  545. });
  546. // VIE.RDFaCollectionView
  547. // ----------------------
  548. //
  549. // VIE.RDFaCollectionView defines a common Backbone View for Collection properties
  550. VIE.RDFaCollectionView = Backbone.View.extend({
  551. elementTemplate: null,
  552. itemViews: {},
  553. // Ensure the collection view gets updated when items get added or removed
  554. initialize: function() {
  555. this.elementTemplate = this.options.elementTemplate;
  556. _.bindAll(this, 'addItem', 'removeItem', 'refreshItems');
  557. this.collection.bind('add', this.addItem);
  558. this.collection.bind('remove', this.removeItem);
  559. this.collection.bind('refresh', this.refreshItems);
  560. },
  561. // When a collection is refreshed, we empty the collection list and
  562. // add each child separately
  563. refreshItems: function(collectionInstance) {
  564. var collectionView = this;
  565. jQuery(this.el).empty();
  566. collectionInstance.forEach(function(itemInstance) {
  567. collectionView.addItem(itemInstance);
  568. });
  569. },
  570. // When adding new items we create a new element of the child type
  571. // and append it to the list.
  572. addItem: function(itemInstance, collection) {
  573. if (collection !== this.collection) {
  574. return;
  575. }
  576. if (!this.elementTemplate ||
  577. this.elementTemplate.length === 0) {
  578. return;
  579. }
  580. var itemView = VIE.RDFaEntities._registerView(itemInstance, VIE.RDFa._cloneElement(this.elementTemplate));
  581. var itemViewElement = itemView.render().el;
  582. if (itemInstance.id &&
  583. typeof itemInstance.id === 'string') {
  584. VIE.RDFa.setSubject(itemViewElement, itemInstance.id);
  585. } else {
  586. itemInstance.id = itemViewElement.get(0);
  587. }
  588. // Figure out where to place the element based on its order
  589. var itemOrder = this.collection.indexOf(itemInstance) - 1;
  590. var childElements = jQuery(this.el).children();
  591. if (childElements.length === 0)
  592. {
  593. jQuery(this.el).append(itemViewElement);
  594. } else {
  595. jQuery(this.el).children().each(function(index, element) {
  596. if (index === itemOrder) {
  597. jQuery(element).before(itemViewElement);
  598. return false;
  599. }
  600. });
  601. }
  602. this.trigger('add', itemView);
  603. itemViewElement.show();
  604. // If the new instance doesn't yet have an identifier, bind it to
  605. // the HTML representation of itself. This safeguards from duplicates.
  606. if (!itemInstance.id) {
  607. itemInstance.id = VIE.RDFa.getSubject(itemViewElement);
  608. }
  609. // Ensure we catch all inferred predicates. We add these via JSONLD
  610. // so the references get properly Collectionized.
  611. jQuery(itemViewElement).parent('[rev]').each(function() {
  612. var properties = {
  613. '@': itemInstance.id
  614. };
  615. var predicate = jQuery(this).attr('rev');
  616. properties[predicate] = VIE.RDFa.getSubject(this);
  617. VIE.EntityManager.getByJSONLD(properties);
  618. });
  619. this.itemViews[itemInstance] = itemView;
  620. },
  621. // When removing items from Collection we remove their views from the DOM.
  622. removeItem: function(itemInstance) {
  623. if (typeof this.itemViews[itemInstance] === 'undefined') {
  624. return;
  625. }
  626. this.trigger('remove', this.itemViews[itemInstance]);
  627. this.itemViews[itemInstance].remove();
  628. }
  629. });
  630. // VIE.RDFa
  631. // --------
  632. //
  633. // RDFa reading and writing utilities. VIE.RDFa acts as a mapping tool between
  634. // [JSON-LD](http://json-ld.org/) -encoded RDF triples and RDFa -annotated content
  635. // on a page.
  636. VIE.RDFa = {
  637. Namespaces: {},
  638. // By default we look for RDF subjects based on elements that have a
  639. // `about`, `typeof` or `src` attribute. In addition, the full HTML page
  640. // is regarded as a valid subject.
  641. //
  642. // For more specialized scenarios this can be overridden:
  643. //
  644. // VIE.RDFa.subjectSelector = '[about]';
  645. subjectSelector: '[about],[typeof],[src],html',
  646. // By default we look for RDF predicates based on elements that have a
  647. // `property` or `rel` attribute.
  648. //
  649. // For more specialized scenarios this can be overridden:
  650. //
  651. // VIE.RDFa.predicateSelector = '[property]';
  652. predicateSelector: '[property],[rel]',
  653. // ### VIE.RDFa.getSubject
  654. //
  655. // Get the RDF subject for an element. The method accepts
  656. // [jQuery selectors](http://api.jquery.com/category/selectors/) as
  657. // arguments. If no argument is given, then the _base URL_ of the
  658. // page is used.
  659. //
  660. // Returns the subject as a string if one can be found, and if the
  661. // given element has no valid subjects returns `undefined`.
  662. //
  663. // Example:
  664. //
  665. // var subject = VIE.RDFa.getSubject('p[about]');
  666. // alert(subject); // <http://www.example.com/books/wikinomics>
  667. getSubject: function(element) {
  668. if (typeof document !== 'undefined') {
  669. if (element === document) {
  670. return document.baseURI;
  671. }
  672. }
  673. var subject;
  674. jQuery(element).closest(VIE.RDFa.subjectSelector).each(function() {
  675. if (jQuery(this).attr('about')) {
  676. subject = jQuery(this).attr('about');
  677. return true;
  678. }
  679. if (jQuery(this).attr('src')) {
  680. subject = jQuery(this).attr('src');
  681. return true;
  682. }
  683. if (jQuery(this).attr('typeof')) {
  684. subject = this;
  685. return true;
  686. }
  687. // We also handle baseURL outside browser context by manually
  688. // looking for the `<base>` element inside HTML head.
  689. if (jQuery(this).get(0).nodeName === 'HTML') {
  690. jQuery(this).find('base').each(function() {
  691. subject = jQuery(this).attr('href');
  692. });
  693. }
  694. });
  695. if (!subject) {
  696. return undefined;
  697. }
  698. if (typeof subject === 'object') {
  699. return subject;
  700. }
  701. return VIE.RDFa._toReference(subject);
  702. },
  703. // Set subject for an element
  704. setSubject: function(element, subject) {
  705. jQuery(element).attr('about', subject);
  706. },
  707. // Get predicate for an element
  708. getPredicate: function(element) {
  709. var propertyName;
  710. element = jQuery(element);
  711. propertyName = element.attr('property');
  712. if (!propertyName) {
  713. propertyName = element.attr('rel');
  714. }
  715. return propertyName;
  716. },
  717. // ### VIE.RDFa.readEntity
  718. //
  719. // Get a JSON-LD object for an RDFa-marked entity in
  720. // an element. The method accepts [jQuery selectors](http://api.jquery.com/category/selectors/)
  721. // as argument. If the element contains no RDFa entities, the this method
  722. // returns `null`.
  723. //
  724. // Example:
  725. //
  726. // var jsonld = VIE.RDFa.readEntity('p[about]');
  727. //
  728. // Would return a JSON-LD object looking like the following:
  729. //
  730. // {
  731. // '@': '<http://www.example.com/books/wikinomics>',
  732. // 'dc:title': 'Wikinomics',
  733. // 'dc:creator': 'Don Tapscott',
  734. // 'dc:date': '2006-10-01'
  735. // }
  736. readEntity: function(element) {
  737. var entity;
  738. var subject;
  739. var namespaces = {};
  740. var namespace;
  741. var type;
  742. var propertyName;
  743. subject = VIE.RDFa.getSubject(element);
  744. entity = VIE.RDFa._getElementProperties(subject, element, false);
  745. if (jQuery.isEmptyObject(entity)) {
  746. return null;
  747. }
  748. // We also try to resolve namespaces used in the RDFa entity. If they
  749. // can be found, we will write them to the `#` property of the object.
  750. for (propertyName in entity) {
  751. if (entity.hasOwnProperty(propertyName)) {
  752. var propertyParts = propertyName.split(':');
  753. if (propertyParts.length === 2) {
  754. namespace = VIE.RDFa._resolveNamespace(propertyParts[0], element);
  755. if (namespace) {
  756. namespaces[propertyParts[0]] = namespace;
  757. }
  758. }
  759. }
  760. }
  761. if (!jQuery.isEmptyObject(namespaces)) {
  762. entity['#'] = namespaces;
  763. }
  764. // If the RDF type is defined, that will be set to the [`a` property](http://json-ld.org/spec/latest/#specifying-the-type)
  765. // of the JSON-LD object.
  766. type = VIE.RDFa._getElementValue(element, 'typeof');
  767. if (type) {
  768. entity.a = VIE.RDFa._toReference(type);
  769. }
  770. entity['@'] = subject;
  771. return entity;
  772. },
  773. // ### VIE.RDFa.readEntities
  774. //
  775. // Get a list of JSON-LD objects for RDFa-marked entities in
  776. // an element. The method accepts [jQuery selectors](http://api.jquery.com/category/selectors/)
  777. // as argument. If no selector is given, then the whole HTML document will
  778. // be searched.
  779. //
  780. // Example:
  781. //
  782. // var jsonldEntities = VIE.RDFa.readEntities();
  783. // JSON.stringify(jsonldEntities[0]);
  784. //
  785. // Would produce something like:
  786. //
  787. // {
  788. // "@": "<http://www.example.com/books/wikinomics>",
  789. // "dc:title": "Wikinomics",
  790. // "dc:creator": "Don Tapscott",
  791. // "dc:date": "2006-10-01"
  792. // }
  793. readEntities: function(element) {
  794. var entities = [];
  795. var entity;
  796. if (typeof element === 'undefined') {
  797. element = jQuery(document);
  798. }
  799. jQuery(VIE.RDFa.subjectSelector, element).add(jQuery(element).filter(VIE.RDFa.subjectSelector)).each(function() {
  800. entity = VIE.RDFa.readEntity(this);
  801. if (entity) {
  802. entities.push(entity);
  803. }
  804. });
  805. return entities;
  806. },
  807. // ### VIE.RDFa.writeEntity
  808. //
  809. // Write the contents of a JSON-LD object into the given DOM element. This
  810. // method accepts [jQuery selectors](http://api.jquery.com/category/selectors/)
  811. // as arguments.
  812. //
  813. // Only properties matching RDFa-annotated predicates found found from
  814. // the selected DOM element will be written.
  815. writeEntity: function(element, jsonld) {
  816. VIE.RDFa.findPredicateElements(VIE.RDFa.getSubject(element), element, true).each(function() {
  817. var propertyElement = jQuery(this);
  818. var propertyName = propertyElement.attr('property');
  819. if (typeof jsonld[propertyName] === 'undefined') {
  820. jsonld[propertyName] = propertyName;
  821. }
  822. // Before writing to DOM we check that the value has actually changed.
  823. if (VIE.RDFa._readPropertyValue(propertyName, propertyElement) !== jsonld[propertyName]) {
  824. VIE.RDFa._writePropertyValue(propertyElement, jsonld[propertyName]);
  825. }
  826. });
  827. return this;
  828. },
  829. // ### VIE.RDFa.findPredicateElements
  830. //
  831. // Find RDFa-annotated predicates for a given subject inside the DOM. This
  832. // method accepts [jQuery selectors](http://api.jquery.com/category/selectors/)
  833. // as arguments.
  834. //
  835. // The method returns a list of matching DOM elements.
  836. //
  837. // Only predicates matching the given subject will be returned.
  838. // You can also tell whether to allow nested predicates to be returned,
  839. // which is useful for example when instantiating WYSIWYG editors for
  840. // editable properties, as most editors do not like getting nested.
  841. findPredicateElements: function(subject, element, allowNestedPredicates) {
  842. if (typeof subject === 'string' &&
  843. !VIE.RDFa._isReference(subject)) {
  844. subject = VIE.RDFa._toReference(subject);
  845. }
  846. return jQuery(element).find(VIE.RDFa.predicateSelector).add(jQuery(element).filter(VIE.RDFa.predicateSelector)).filter(function() {
  847. if (VIE.RDFa.getSubject(this) !== subject) {
  848. return false;
  849. }
  850. if (!allowNestedPredicates) {
  851. if (!jQuery(this).parents('[property]').length) {
  852. return true;
  853. }
  854. return false;
  855. }
  856. return true;
  857. });
  858. },
  859. // Figure out if a given value is a wrapped reference
  860. _isReference: function(value) {
  861. var matcher = new RegExp("^\\<([^\\>]*)\\>$");
  862. if (matcher.exec(value)) {
  863. return true;
  864. }
  865. return false;
  866. },
  867. // In JSON-LD all references are surrounded by `<` and `>`. Convert a regular
  868. // textual value to this format.
  869. _toReference: function(value) {
  870. return '<' + value + '>';
  871. },
  872. // In JSON-LD all references are surrounded by `<` and `>`. Convert reference
  873. // to a regular textual value.
  874. _fromReference: function(reference) {
  875. if (_.isArray(reference)) {
  876. return _.map(reference, function(ref) {
  877. return VIE.RDFa._fromReference(ref);
  878. });
  879. }
  880. return reference.substring(1, reference.length - 1);
  881. },
  882. // Get value of a DOM element defining a RDFa predicate.
  883. _readPropertyValue: function(propertyName, element) {
  884. // The `content` attribute can be used for providing machine-readable
  885. // values for elements where the HTML presentation differs from the
  886. // actual value.
  887. var content = element.attr('content');
  888. if (content) {
  889. return content;
  890. }
  891. // The `resource` attribute can be used to link a predicate to another
  892. // RDF resource.
  893. var resource = element.attr('resource');
  894. if (resource) {
  895. return VIE.RDFa._toReference(resource);
  896. }
  897. // `href` attribute also links to another RDF resource.
  898. var href = element.attr('href');
  899. if (href &&
  900. element.attr('rel') === propertyName) {
  901. return VIE.RDFa._toReference(href);
  902. }
  903. // If the predicate is a relation, we look for identified child objects
  904. // and provide their identifiers as the values. To protect from scope
  905. // creep, we only support direct descentants of the element where the
  906. // `rel` attribute was set.
  907. if (element.attr('rel')) {
  908. var value = [];
  909. jQuery(element).children(VIE.RDFa.subjectSelector).each(function() {
  910. value.push(VIE.RDFa.getSubject(this));
  911. });
  912. return value;
  913. }
  914. // If none of the checks above matched we return the HTML contents of
  915. // the element as the literal value.
  916. return element.html();
  917. },
  918. // Write a value to a DOM element defining a RDFa predicate.
  919. _writePropertyValue: function(element, value) {
  920. // For now we don't deal with multivalued properties when writing
  921. // contents.
  922. if (value instanceof Array) {
  923. return true;
  924. }
  925. // The `content` attribute can be used for providing machine-readable
  926. // values for elements where the HTML presentation differs from the
  927. // actual value.
  928. var content = element.attr('content');
  929. if (content) {
  930. element.attr('content', value);
  931. return;
  932. }
  933. // The `resource` attribute can be used to link a predicate to another
  934. // RDF resource.
  935. var resource = element.attr('resource');
  936. if (resource) {
  937. element.attr('resource', value);
  938. }
  939. // Property has inline value. Change the HTML contents of the property
  940. // element to match the new value.
  941. element.html(value);
  942. },
  943. // Namespace resolution, find namespace declarations from inside
  944. // a DOM element.
  945. _resolveNamespace: function(prefix, element) {
  946. if (typeof VIE.RDFa.Namespaces[prefix] !== 'undefined') {
  947. return VIE.RDFa.Namespaces[prefix];
  948. }
  949. jQuery('[xmlns\\:' + prefix + ']').each(function() {
  950. VIE.RDFa.Namespaces[prefix] = jQuery(this).attr('xmlns:' + prefix);
  951. });
  952. return VIE.RDFa.Namespaces[prefix];
  953. },
  954. // Get the value of an attribute from the element or from one of its children
  955. _getElementValue: function(element, propertyName) {
  956. element = jQuery(element);
  957. if (typeof element.attr(propertyName) !== 'undefined')
  958. {
  959. return element.attr(propertyName);
  960. }
  961. return element.children('[' + propertyName + ']').attr(propertyName);
  962. },
  963. // Get elements matching a given subject and predicate
  964. _getElementByPredicate: function(predicate, element) {
  965. var subject = VIE.RDFa.getSubject(element);
  966. return jQuery(element).find(VIE.RDFa.predicateSelector).add(jQuery(element).filter(VIE.RDFa.predicateSelector)).filter(function() {
  967. if (VIE.RDFa.getPredicate(this) !== predicate) {
  968. return false;
  969. }
  970. if (VIE.RDFa.getSubject(this) !== subject) {
  971. return false;
  972. }
  973. return true;
  974. });
  975. },
  976. // Get JSON-LD properties from a DOM element.
  977. _getElementProperties: function(subject, element, emptyValues) {
  978. var containerProperties = {};
  979. VIE.RDFa.findPredicateElements(subject, element, true).each(function() {
  980. var propertyName;
  981. var propertyValue;
  982. var objectProperty = jQuery(this);
  983. propertyName = VIE.RDFa.getPredicate(this);
  984. propertyValue = VIE.RDFa._readPropertyValue(propertyName, objectProperty);
  985. if (propertyValue === null &&
  986. !emptyValues) {
  987. return;
  988. }
  989. if (typeof containerProperties[propertyName] !== 'undefined') {
  990. if (containerProperties[propertyName] instanceof Array) {
  991. if (emptyValues) {
  992. return;
  993. }
  994. containerProperties[propertyName].push(propertyValue);
  995. return;
  996. }
  997. // Multivalued properties, are converted to an Array
  998. var previousValue = containerProperties[propertyName];
  999. containerProperties[propertyName] = [];
  1000. if (emptyValues) {
  1001. return;
  1002. }
  1003. containerProperties[propertyName].push(previousValue);
  1004. containerProperties[propertyName].push(propertyValue);
  1005. return;
  1006. }
  1007. if (emptyValues) {
  1008. containerProperties[propertyName] = '';
  1009. return;
  1010. }
  1011. containerProperties[propertyName] = propertyValue;
  1012. });
  1013. if (jQuery(element).get(0).tagName !== 'HTML') {
  1014. jQuery(element).parent('[rev]').each(function() {
  1015. containerProperties[jQuery(this).attr('rev')] = VIE.RDFa.getSubject(this);
  1016. });
  1017. }
  1018. return containerProperties;
  1019. },
  1020. // Create an anonymized clone of an element
  1021. _cloneElement: function(element) {
  1022. element = jQuery(element).clone(false);
  1023. if (typeof element.attr('about') !== 'undefined')
  1024. {
  1025. // Direct match with container
  1026. element.attr('about', '');
  1027. }
  1028. element.find('[about]').attr('about', '');
  1029. var subject = VIE.RDFa.getSubject(element);
  1030. VIE.RDFa.findPredicateElements(subject, element, false).each(function() {
  1031. jQuery(this).html('');
  1032. });
  1033. return element;
  1034. },
  1035. // Helper for removing existing namespaces information.
  1036. cleanup: function() {
  1037. VIE.RDFa.Namespaces = {};
  1038. }
  1039. };
  1040. // VIE.cleanup()
  1041. // -------------
  1042. //
  1043. // By default VIE keeps track of all RDF entities, RDFa views and namespaces
  1044. // handled. If you want to clear all of these (for example in unit tests),
  1045. // then call:
  1046. //
  1047. // VIE.cleanup();
  1048. VIE.cleanup = function() {
  1049. VIE.EntityManager.cleanup();
  1050. VIE.RDFaEntities.cleanup();
  1051. VIE.RDFa.cleanup();
  1052. };
  1053. }).call(this);