maquette.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. (function (global) {
  2. "use strict";
  3. // Utilities
  4. var emptyArray = [];
  5. var extend = function (base, overrides) {
  6. var result = {};
  7. Object.keys(base).forEach(function (key) {
  8. result[key] = base[key];
  9. });
  10. if(overrides) {
  11. Object.keys(overrides).forEach(function (key) {
  12. result[key] = overrides[key];
  13. });
  14. }
  15. return result;
  16. };
  17. // Hyperscript helper functions
  18. var appendChildren = function (parentSelector, insertions, main) {
  19. for(var i = 0; i < insertions.length; i++) {
  20. var item = insertions[i];
  21. if(Array.isArray(item)) {
  22. appendChildren(parentSelector, item, main);
  23. } else {
  24. if(item !== null && item !== undefined) {
  25. if(!item.hasOwnProperty("vnodeSelector")) {
  26. item = toTextVNode(item);
  27. }
  28. main.push(item);
  29. }
  30. }
  31. }
  32. };
  33. var toTextVNode = function (data) {
  34. return {
  35. vnodeSelector: "",
  36. properties: undefined,
  37. children: undefined,
  38. text: (data === null || data === undefined) ? "" : data.toString(),
  39. domNode: null
  40. };
  41. };
  42. // Render helper functions
  43. var missingTransition = function() {
  44. throw new Error("Provide a transitions object to the projectionOptions to do animations");
  45. };
  46. var defaultProjectionOptions = {
  47. namespace: undefined,
  48. eventHandlerInterceptor: undefined,
  49. styleApplyer: function(domNode, styleName, value) {
  50. // Provides a hook to add vendor prefixes for browsers that still need it.
  51. domNode.style[styleName] = value;
  52. },
  53. transitions: {
  54. enter: missingTransition,
  55. exit: missingTransition
  56. }
  57. };
  58. var applyDefaultProjectionOptions = function (projectionOptions) {
  59. return extend(defaultProjectionOptions, projectionOptions);
  60. };
  61. var setProperties = function (domNode, properties, projectionOptions) {
  62. if(!properties) {
  63. return;
  64. }
  65. var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
  66. for(var propName in properties) {
  67. var propValue = properties[propName];
  68. if(propName === "class" || propName === "className" || propName === "classList") {
  69. throw new Error("Property " + propName + " is not supported, use 'classes' instead.");
  70. } else if(propName === "classes") {
  71. // object with string keys and boolean values
  72. for(var className in propValue) {
  73. if(propValue[className]) {
  74. domNode.classList.add(className);
  75. }
  76. }
  77. } else if(propName === "styles") {
  78. // object with string keys and string (!) values
  79. for(var styleName in propValue) {
  80. var styleValue = propValue[styleName];
  81. if(styleValue) {
  82. if(typeof styleValue !== "string") {
  83. throw new Error("Style values may only be strings");
  84. }
  85. projectionOptions.styleApplyer(domNode, styleName, styleValue);
  86. }
  87. }
  88. } else if(propName === "key") {
  89. continue;
  90. } else if(propValue === null || propValue === undefined) {
  91. continue;
  92. } else {
  93. var type = typeof propValue;
  94. if(type === "function") {
  95. if(eventHandlerInterceptor && (propName.lastIndexOf("on", 0) === 0)) { // lastIndexOf(,0)===0 -> startsWith
  96. propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers
  97. if(propName === "oninput") {
  98. (function () {
  99. // record the evt.target.value, because IE sometimes does a requestAnimationFrame between changing value and running oninput
  100. var oldPropValue = propValue;
  101. propValue = function (evt) {
  102. evt.target["oninput-value"] = evt.target.value;
  103. oldPropValue.apply(this, [evt]);
  104. };
  105. }());
  106. }
  107. }
  108. domNode[propName] = propValue;
  109. } else if(type === "string" && propName !== "value") {
  110. domNode.setAttribute(propName, propValue);
  111. } else {
  112. domNode[propName] = propValue;
  113. }
  114. }
  115. }
  116. };
  117. var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
  118. if(!properties) {
  119. return;
  120. }
  121. var propertiesUpdated = false;
  122. for(var propName in properties) {
  123. // assuming that properties will be nullified instead of missing is by design
  124. var propValue = properties[propName];
  125. var previousValue = previousProperties[propName];
  126. if(propName === "classes") {
  127. var classList = domNode.classList;
  128. for(var className in propValue) {
  129. var on = !!propValue[className];
  130. var previousOn = !!previousValue[className];
  131. if(on === previousOn) {
  132. continue;
  133. }
  134. propertiesUpdated = true;
  135. if(on) {
  136. classList.add(className);
  137. } else {
  138. classList.remove(className);
  139. }
  140. }
  141. } else if(propName === "styles") {
  142. for(var styleName in propValue) {
  143. var newStyleValue = propValue[styleName];
  144. var oldStyleValue = previousValue[styleName];
  145. if(newStyleValue === oldStyleValue) {
  146. continue;
  147. }
  148. propertiesUpdated = true;
  149. if(newStyleValue) {
  150. if(typeof newStyleValue !== "string") {
  151. throw new Error("Style values may only be strings");
  152. }
  153. projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
  154. } else {
  155. projectionOptions.styleApplyer(domNode, styleName, "");
  156. }
  157. }
  158. } else {
  159. if(!propValue && typeof previousValue === "string") {
  160. propValue = "";
  161. }
  162. if(propName === "value") { // value can be manipulated by the user directly and using event.preventDefault() is not an option
  163. if(domNode[propName] !== propValue && domNode["oninput-value"] !== propValue) {
  164. domNode[propName] = propValue; // Reset the value, even if the virtual DOM did not change
  165. domNode["oninput-value"] = undefined;
  166. } // else do not update the domNode, otherwise the cursor position would be changed
  167. if(propValue !== previousValue) {
  168. propertiesUpdated = true;
  169. }
  170. } else if(propValue !== previousValue) {
  171. var type = typeof propValue;
  172. if(type === "function") {
  173. throw new Error("Functions may not be updated on subsequent renders (property: " + propName +
  174. "). Hint: declare event handler functions outside the render() function.");
  175. }
  176. if(type === "string") {
  177. domNode.setAttribute(propName, propValue);
  178. } else {
  179. if(domNode[propName] !== propValue) { // Comparison is here for side-effects in Edge with scrollLeft and scrollTop
  180. domNode[propName] = propValue;
  181. }
  182. }
  183. propertiesUpdated = true;
  184. }
  185. }
  186. }
  187. return propertiesUpdated;
  188. };
  189. var addChildren = function (domNode, children, projectionOptions) {
  190. if(!children) {
  191. return;
  192. }
  193. for(var i = 0; i < children.length; i++) {
  194. createDom(children[i], domNode, undefined, projectionOptions);
  195. }
  196. };
  197. var same = function (vnode1, vnode2) {
  198. if(vnode1.vnodeSelector !== vnode2.vnodeSelector) {
  199. return false;
  200. }
  201. if(vnode1.properties && vnode2.properties) {
  202. return vnode1.properties.key === vnode2.properties.key;
  203. }
  204. return !vnode1.properties && !vnode2.properties;
  205. };
  206. var findIndexOfChild = function (children, sameAs, start) {
  207. if(sameAs.vnodeSelector !== "") {
  208. // Never scan for text-nodes
  209. for(var i = start; i < children.length; i++) {
  210. if(same(children[i], sameAs)) {
  211. return i;
  212. }
  213. }
  214. }
  215. return -1;
  216. };
  217. var nodeAdded = function (vNode, transitions) {
  218. if(vNode.properties) {
  219. var enterAnimation = vNode.properties.enterAnimation;
  220. if(enterAnimation) {
  221. if(typeof enterAnimation === "function") {
  222. enterAnimation(vNode.domNode, vNode.properties);
  223. } else {
  224. transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
  225. }
  226. }
  227. }
  228. };
  229. var nodeToRemove = function (vNode, transitions) {
  230. var domNode = vNode.domNode;
  231. if(vNode.properties) {
  232. var exitAnimation = vNode.properties.exitAnimation;
  233. if(exitAnimation) {
  234. domNode.style.pointerEvents = "none";
  235. var removeDomNode = function () {
  236. if(domNode.parentNode) {
  237. domNode.parentNode.removeChild(domNode);
  238. }
  239. };
  240. if(typeof exitAnimation === "function") {
  241. exitAnimation(domNode, removeDomNode, vNode.properties);
  242. return;
  243. } else {
  244. transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
  245. return;
  246. }
  247. }
  248. }
  249. if(domNode.parentNode) {
  250. domNode.parentNode.removeChild(domNode);
  251. }
  252. };
  253. var checkDistinguishable = function(childNodes, indexToCheck, parentVNode, operation) {
  254. var childNode = childNodes[indexToCheck];
  255. if (childNode.vnodeSelector === "") {
  256. return; // Text nodes need not be distinguishable
  257. }
  258. var key = childNode.properties ? childNode.properties.key : undefined;
  259. if (!key) { // A key is just assumed to be unique
  260. for (var i = 0; i < childNodes.length; i++) {
  261. if (i !== indexToCheck) {
  262. var node = childNodes[i];
  263. if (same(node, childNode)) {
  264. if (operation === "added") {
  265. throw new Error(parentVNode.vnodeSelector + " had a " + childNode.vnodeSelector + " child " +
  266. "added, but there is now more than one. You must add unique key properties to make them distinguishable.");
  267. } else {
  268. throw new Error(parentVNode.vnodeSelector + " had a " + childNode.vnodeSelector + " child " +
  269. "removed, but there were more than one. You must add unique key properties to make them distinguishable.");
  270. }
  271. }
  272. }
  273. }
  274. }
  275. };
  276. var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
  277. if(oldChildren === newChildren) {
  278. return false;
  279. }
  280. oldChildren = oldChildren || emptyArray;
  281. newChildren = newChildren || emptyArray;
  282. var oldChildrenLength = oldChildren.length;
  283. var newChildrenLength = newChildren.length;
  284. var transitions = projectionOptions.transitions;
  285. var oldIndex = 0;
  286. var newIndex = 0;
  287. var i;
  288. var textUpdated = false;
  289. while(newIndex < newChildrenLength) {
  290. var oldChild = (oldIndex < oldChildrenLength) ? oldChildren[oldIndex] : undefined;
  291. var newChild = newChildren[newIndex];
  292. if(oldChild !== undefined && same(oldChild, newChild)) {
  293. textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
  294. oldIndex++;
  295. } else {
  296. var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
  297. if(findOldIndex >= 0) {
  298. // Remove preceding missing children
  299. for(i = oldIndex; i < findOldIndex; i++) {
  300. nodeToRemove(oldChildren[i], transitions);
  301. checkDistinguishable(oldChildren, i, vnode, "removed");
  302. }
  303. textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
  304. oldIndex = findOldIndex + 1;
  305. } else {
  306. // New child
  307. createDom(newChild, domNode, (oldIndex < oldChildrenLength) ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
  308. nodeAdded(newChild, transitions);
  309. checkDistinguishable(newChildren, newIndex, vnode, "added");
  310. }
  311. }
  312. newIndex++;
  313. }
  314. if(oldChildrenLength > oldIndex) {
  315. // Remove child fragments
  316. for(i = oldIndex; i < oldChildrenLength; i++) {
  317. nodeToRemove(oldChildren[i], transitions);
  318. checkDistinguishable(oldChildren, i, vnode, "removed");
  319. }
  320. }
  321. return textUpdated;
  322. };
  323. var createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
  324. var domNode, i, c, start = 0, type, found;
  325. var vnodeSelector = vnode.vnodeSelector;
  326. if(vnodeSelector === "") {
  327. domNode = vnode.domNode = document.createTextNode(vnode.text);
  328. if(insertBefore !== undefined) {
  329. parentNode.insertBefore(domNode, insertBefore);
  330. } else {
  331. parentNode.appendChild(domNode);
  332. }
  333. } else {
  334. for (i = 0; i <= vnodeSelector.length; ++i) {
  335. c = vnodeSelector.charAt(i);
  336. if (i === vnodeSelector.length || c === '.' || c === '#') {
  337. type = vnodeSelector.charAt(start - 1);
  338. found = vnodeSelector.slice(start, i);
  339. if (type === ".") {
  340. domNode.classList.add(found);
  341. } else if (type === "#") {
  342. domNode.id = found;
  343. } else {
  344. if (found === "svg") {
  345. projectionOptions = extend(projectionOptions, { namespace: "http://www.w3.org/2000/svg" });
  346. }
  347. if (projectionOptions.namespace !== undefined) {
  348. domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
  349. } else {
  350. domNode = vnode.domNode = document.createElement(found);
  351. }
  352. if (insertBefore !== undefined) {
  353. parentNode.insertBefore(domNode, insertBefore);
  354. } else {
  355. parentNode.appendChild(domNode);
  356. }
  357. }
  358. start = i + 1;
  359. }
  360. }
  361. initPropertiesAndChildren(domNode, vnode, projectionOptions);
  362. }
  363. };
  364. var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
  365. addChildren(domNode, vnode.children, projectionOptions); // children before properties, needed for value property of <select>.
  366. if(vnode.text) {
  367. domNode.textContent = vnode.text;
  368. }
  369. setProperties(domNode, vnode.properties, projectionOptions);
  370. if(vnode.properties && vnode.properties.afterCreate) {
  371. vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
  372. }
  373. };
  374. var updateDom = function (previous, vnode, projectionOptions) {
  375. var domNode = previous.domNode;
  376. if(!domNode) {
  377. throw new Error("previous node was not rendered");
  378. }
  379. var textUpdated = false;
  380. if(previous === vnode) {
  381. return textUpdated; // we assume that nothing has changed
  382. }
  383. var updated = false;
  384. if(vnode.vnodeSelector === "") {
  385. if(vnode.text !== previous.text) {
  386. domNode.nodeValue = vnode.text;
  387. textUpdated = true;
  388. }
  389. } else {
  390. if(vnode.vnodeSelector.lastIndexOf("svg", 0) === 0) { // lastIndexOf(needle,0)===0 means StartsWith
  391. projectionOptions = extend(projectionOptions, { namespace: "http://www.w3.org/2000/svg" });
  392. }
  393. if(previous.text !== vnode.text) {
  394. updated = true;
  395. if(vnode.text === undefined) {
  396. domNode.removeChild(domNode.firstChild); // the only textnode presumably
  397. } else {
  398. domNode.textContent = vnode.text;
  399. }
  400. }
  401. updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
  402. updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
  403. if(vnode.properties && vnode.properties.afterUpdate) {
  404. vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
  405. }
  406. }
  407. if(updated && vnode.properties && vnode.properties.updateAnimation) {
  408. vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
  409. }
  410. vnode.domNode = previous.domNode;
  411. return textUpdated;
  412. };
  413. /**
  414. * Represents a {@link VNode} tree that has been rendered to a real DOM tree.
  415. * @interface Projection
  416. */
  417. var createProjection = function (vnode, projectionOptions) {
  418. if(!vnode.vnodeSelector) {
  419. throw new Error("Invalid vnode argument");
  420. }
  421. return {
  422. /**
  423. * Updates the projection with the new virtual DOM tree.
  424. * @param {VNode} updatedVnode - The updated virtual DOM tree. Note: The selector for the root of the tree must remain constant.
  425. * @memberof Projection#
  426. */
  427. update: function (updatedVnode) {
  428. if(vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
  429. throw new Error("The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)");
  430. }
  431. updateDom(vnode, updatedVnode, projectionOptions);
  432. vnode = updatedVnode;
  433. },
  434. /**
  435. * The DOM node that is used as the root of this {@link Projection}.
  436. * @type {Element}
  437. * @memberof Projection#
  438. */
  439. domNode: vnode.domNode
  440. };
  441. };
  442. // Declaration of interfaces and callbacks, before the @exports maquette
  443. /**
  444. * A virtual representation of a DOM Node. Maquette assumes that {@link VNode} objects are never modified externally.
  445. * Instances of {@link VNode} can be created using {@link module:maquette.h}.
  446. * @interface VNode
  447. */
  448. /**
  449. * A CalculationCache object remembers the previous outcome of a calculation along with the inputs.
  450. * On subsequent calls the previous outcome is returned if the inputs are identical.
  451. * This object can be used to bypass both rendering and diffing of a virtual DOM subtree.
  452. * Instances of {@link CalculationCache} can be created using {@link module:maquette.createCache}.
  453. * @interface CalculationCache
  454. */
  455. /**
  456. * Keeps an array of result objects synchronized with an array of source objects.
  457. * Mapping provides a {@link Mapping#map} function that updates the {@link Mapping#results}.
  458. * The {@link Mapping#map} function can be called multiple times and the results will get created, removed and updated accordingly.
  459. * A {@link Mapping} can be used to keep an array of components (objects with a `renderMaquette` method) synchronized with an array of data.
  460. * Instances of {@link Mapping} can be created using {@link module:maquette.createMapping}.
  461. * @interface Mapping
  462. */
  463. /**
  464. * Used to create and update the DOM.
  465. * Use {@link Projector#append}, {@link Projector#merge}, {@link Projector#insertBefore} and {@link Projector#replace}
  466. * to create the DOM.
  467. * The `renderMaquetteFunction` callbacks will be called immediately to create the DOM. Afterwards, these functions
  468. * will be called again to update the DOM on the next animation-frame after:
  469. *
  470. * - The {@link Projector#scheduleRender} function was called
  471. * - An event handler (like `onclick`) on a rendered {@link VNode} was called.
  472. *
  473. * The projector stops when {@link Projector#stop} is called or when an error is thrown during rendering.
  474. * It is possible to use `window.onerror` to handle these errors.
  475. * Instances of {@link Projector} can be created using {@link module:maquette.createProjector}.
  476. * @interface Projector
  477. */
  478. /**
  479. * @callback enterAnimationCallback
  480. * @param {Element} element - Element that was just added to the DOM.
  481. * @param {Object} properties - The properties object that was supplied to the {@link module:maquette.h} method
  482. */
  483. /**
  484. * @callback exitAnimationCallback
  485. * @param {Element} element - Element that ought to be removed from to the DOM.
  486. * @param {function(Element)} removeElement - Function that removes the element from the DOM.
  487. * This argument is supplied purely for convenience.
  488. * You may use this function to remove the element when the animation is done.
  489. * @param {Object} properties - The properties object that was supplied to the {@link module:maquette.h} method that rendered this {@link VNode} the previous time.
  490. */
  491. /**
  492. * @callback updateAnimationCallback
  493. * @param {Element} element - Element that was modified in the DOM.
  494. * @param {Object} properties - The last properties object that was supplied to the {@link module:maquette.h} method
  495. * @param {Object} previousProperties - The previous properties object that was supplied to the {@link module:maquette.h} method
  496. */
  497. /**
  498. * @callback afterCreateCallback
  499. * @param {Element} element - The element that was added to the DOM.
  500. * @param {Object} projectionOptions - The projection options that were used see {@link module:maquette.createProjector}.
  501. * @param {string} vnodeSelector - The selector passed to the {@link module:maquette.h} function.
  502. * @param {Object} properties - The properties passed to the {@link module:maquette.h} function.
  503. * @param {VNode[]} children - The children that were created.
  504. * @param {Object} properties - The last properties object that was supplied to the {@link module:maquette.h} method
  505. * @param {Object} previousProperties - The previous properties object that was supplied to the {@link module:maquette.h} method
  506. */
  507. /**
  508. * @callback afterUpdateCallback
  509. * @param {Element} element - The element that may have been updated in the DOM.
  510. * @param {Object} projectionOptions - The projection options that were used see {@link module:maquette.createProjector}.
  511. * @param {string} vnodeSelector - The selector passed to the {@link module:maquette.h} function.
  512. * @param {Object} properties - The properties passed to the {@link module:maquette.h} function.
  513. * @param {VNode[]} children - The children for this node.
  514. */
  515. /**
  516. * Contains simple low-level utility functions to manipulate the real DOM. The singleton instance is available under {@link module:maquette.dom}.
  517. * @interface MaquetteDom
  518. */
  519. /**
  520. * The main object in maquette is the maquette object.
  521. * It is either bound to `window.maquette` or it can be obtained using {@link http://browserify.org/|browserify} or {@link http://requirejs.org/|requirejs}.
  522. * @exports maquette
  523. */
  524. var maquette = {
  525. /**
  526. * The `h` method is used to create a virtual DOM node.
  527. * This function is largely inspired by the mercuryjs and mithril frameworks.
  528. * The `h` stands for (virtual) hyperscript.
  529. *
  530. * @param {string} selector - Contains the tagName, id and fixed css classnames in CSS selector format.
  531. * It is formatted as follows: `tagname.cssclass1.cssclass2#id`.
  532. * @param {Object} [properties] - An object literal containing properties that will be placed on the DOM node.
  533. * @param {function} properties.<b>*</b> - Properties with functions values like `onclick:handleClick` are registered as event handlers
  534. * @param {String} properties.<b>*</b> - Properties with string values, like `href:"/"` are used as attributes
  535. * @param {object} properties.<b>*</b> - All non-string values are put on the DOM node as properties
  536. * @param {Object} properties.key - Used to uniquely identify a DOM node among siblings.
  537. * A key is required when there are more children with the same selector and these children are added or removed dynamically.
  538. * @param {Object} properties.classes - An object literal like `{important:true}` which allows css classes, like `important` to be added and removed dynamically.
  539. * @param {Object} properties.styles - An object literal like `{height:"100px"}` which allows styles to be changed dynamically. All values must be strings.
  540. * @param {(string|enterAnimationCallback)} properties.enterAnimation - The animation to perform when this node is added to an already existing parent.
  541. * {@link http://maquettejs.org/docs/animations.html|More about animations}.
  542. * When this value is a string, you must pass a `projectionOptions.transitions` object when creating the projector {@link module:maquette.createProjector}.
  543. * @param {(string|exitAnimationCallback)} properties.exitAnimation - The animation to perform when this node is removed while its parent remains.
  544. * When this value is a string, you must pass a `projectionOptions.transitions` object when creating the projector {@link module:maquette.createProjector}.
  545. * {@link http://maquettejs.org/docs/animations.html|More about animations}.
  546. * @param {updateAnimationCallback} properties.updateAnimation - The animation to perform when the properties of this node change.
  547. * This also includes attributes, styles, css classes. This callback is also invoked when node contains only text and that text changes.
  548. * {@link http://maquettejs.org/docs/animations.html|More about animations}.
  549. * @param {afterCreateCallback} properties.afterCreate - Callback that is executed after this node is added to the DOM. Childnodes and properties have already been applied.
  550. * @param {afterUpdateCallback} properties.afterUpdate - Callback that is executed every time this node may have been updated. Childnodes and properties have already been updated.
  551. * @param {Object[]} [children] - An array of virtual DOM nodes to add as child nodes.
  552. * This array may contain nested arrays, `null` or `undefined` values.
  553. * Nested arrays are flattened, `null` and `undefined` will be skipped.
  554. *
  555. * @returns {VNode} A VNode object, used to render a real DOM later. NOTE: There are {@link http://maquettejs.org/docs/rules.html|three basic rules} you should be aware of when updating the virtual DOM.
  556. */
  557. h: function (selector, properties, childrenArgs) {
  558. if (typeof selector !== "string") {
  559. throw new Error();
  560. }
  561. var childIndex = 1;
  562. if (properties && !properties.hasOwnProperty("vnodeSelector") && !Array.isArray(properties) && typeof properties === "object") {
  563. childIndex = 2;
  564. } else {
  565. // Optional properties argument was omitted
  566. properties = undefined;
  567. }
  568. var text = undefined;
  569. var children = undefined;
  570. var argsLength = arguments.length;
  571. // Recognize a common special case where there is only a single text node
  572. if(argsLength === childIndex + 1) {
  573. var onlyChild = arguments[childIndex];
  574. if (typeof onlyChild === "string") {
  575. text = onlyChild;
  576. } else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === "string") {
  577. text = onlyChild[0];
  578. }
  579. }
  580. if (text === undefined) {
  581. children = [];
  582. for (;childIndex<arguments.length;childIndex++) {
  583. var child = arguments[childIndex];
  584. if(child === null || child === undefined) {
  585. continue;
  586. } else if(Array.isArray(child)) {
  587. appendChildren(selector, child, children);
  588. } else if(child.hasOwnProperty("vnodeSelector")) {
  589. children.push(child);
  590. } else {
  591. children.push(toTextVNode(child));
  592. }
  593. }
  594. }
  595. return {
  596. /**
  597. * The CSS selector containing tagname, css classnames and id. An empty string is used to denote a text node.
  598. * @memberof VNode#
  599. */
  600. vnodeSelector: selector,
  601. /**
  602. * Object containing attributes, properties, event handlers and more @see module:maquette.h
  603. * @memberof VNode#
  604. */
  605. properties: properties,
  606. /**
  607. * Array of VNodes to be used as children. This array is already flattened.
  608. * @memberof VNode#
  609. */
  610. children: children,
  611. /**
  612. * Used in a special case when a VNode only has one childnode which is a textnode. Only used in combination with children === undefined.
  613. * @memberof VNode#
  614. */
  615. text: text,
  616. /**
  617. * Used by maquette to store the domNode that was produced from this {@link VNode}.
  618. * @memberof VNode#
  619. */
  620. domNode: null
  621. };
  622. },
  623. /**
  624. * @type MaquetteDom
  625. */
  626. dom: {
  627. /**
  628. * Creates a real DOM tree from a {@link VNode}. The {@link Projection} object returned will contain the resulting DOM Node under the {@link Projection#domNode} property.
  629. * This is a low-level method. Users wil typically use a {@link Projector} instead.
  630. * @memberof MaquetteDom#
  631. * @param {VNode} vnode - The root of the virtual DOM tree that was created using the {@link module:maquette.h} function. NOTE: {@link VNode} objects may only be rendered once.
  632. * @param {Object} projectionOptions - Options to be used to create and update the projection, see {@link module:maquette.createProjector}.
  633. * @returns {Projection} The {@link Projection} which contains the DOM Node that was created.
  634. */
  635. create: function (vnode, projectionOptions) {
  636. projectionOptions = applyDefaultProjectionOptions(projectionOptions);
  637. createDom(vnode, document.createElement("div"), undefined, projectionOptions);
  638. return createProjection(vnode, projectionOptions);
  639. },
  640. /**
  641. * Appends a new childnode to the DOM which is generated from a {@link VNode}.
  642. * This is a low-level method. Users wil typically use a {@link Projector} instead.
  643. * @memberof MaquetteDom#
  644. * @param {Element} parentNode - The parent node for the new childNode.
  645. * @param {VNode} vnode - The root of the virtual DOM tree that was created using the {@link module:maquette.h} function. NOTE: {@link VNode} objects may only be rendered once.
  646. * @param {Object} projectionOptions - Options to be used to create and update the projection, see {@link module:maquette.createProjector}.
  647. * @returns {Projection} The {@link Projection} that was created.
  648. */
  649. append: function (parentNode, vnode, projectionOptions) {
  650. projectionOptions = applyDefaultProjectionOptions(projectionOptions);
  651. createDom(vnode, parentNode, undefined, projectionOptions);
  652. return createProjection(vnode, projectionOptions);
  653. },
  654. /**
  655. * Inserts a new DOM node which is generated from a {@link VNode}.
  656. * This is a low-level method. Users wil typically use a {@link Projector} instead.
  657. * @memberof MaquetteDom#
  658. * @param {Element} beforeNode - The node that the DOM Node is inserted before.
  659. * @param {VNode} vnode - The root of the virtual DOM tree that was created using the {@link module:maquette.h} function. NOTE: {@link VNode} objects may only be rendered once.
  660. * @param {Object} projectionOptions - Options to be used to create and update the projection, see {@link module:maquette.createProjector}.
  661. * @returns {Projection} The {@link Projection} that was created.
  662. */
  663. insertBefore: function(beforeNode, vnode, projectionOptions) {
  664. projectionOptions = applyDefaultProjectionOptions(projectionOptions);
  665. createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
  666. return createProjection(vnode, projectionOptions);
  667. },
  668. /**
  669. * Merges a new DOM node which is generated from a {@link VNode} with an existing DOM Node.
  670. * This means that the virtual DOM and real DOM have one overlapping element.
  671. * Therefore the selector for the root {@link VNode} will be ignored, but its properties and children will be applied to the Element provided
  672. * This is a low-level method. Users wil typically use a {@link Projector} instead.
  673. * @memberof MaquetteDom#
  674. * @param {Element} domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
  675. * @param {VNode} vnode - The root of the virtual DOM tree that was created using the {@link module:maquette.h} function. NOTE: {@link VNode} objects may only be rendered once.
  676. * @param {Object} projectionOptions - Options to be used to create and update the projection, see {@link module:maquette.createProjector}.
  677. * @returns {Projection} The {@link Projection} that was created.
  678. */
  679. merge: function (element, vnode, options) {
  680. options = applyDefaultProjectionOptions(options);
  681. vnode.domNode = element;
  682. initPropertiesAndChildren(element, vnode, options);
  683. return createProjection(vnode, options);
  684. }
  685. },
  686. /**
  687. * Creates a {@link CalculationCache} object, useful for caching {@link VNode} trees.
  688. * In practice, caching of {@link VNode} trees is not needed, because achieving 60 frames per second is almost never a problem.
  689. * @returns {CalculationCache}
  690. */
  691. createCache: function () {
  692. var cachedInputs = undefined;
  693. var cachedOutcome = undefined;
  694. var result = {
  695. /**
  696. * Manually invalidates the cached outcome.
  697. * @memberof CalculationCache#
  698. */
  699. invalidate: function () {
  700. cachedOutcome = undefined;
  701. cachedInputs = undefined;
  702. },
  703. /**
  704. * If the inputs array matches the inputs array from the previous invocation, this method returns the result of the previous invocation.
  705. * Otherwise, the calculation function is invoked and its result is cached and returned.
  706. * Objects in the inputs array are compared using ===.
  707. * @param {Object[]} inputs - Array of objects that are to be compared using === with the inputs from the previous invocation.
  708. * These objects are assumed to be immutable primitive values.
  709. * @param {function} calculation - Function that takes zero arguments and returns an object (A {@link VNode} assumably) that can be cached.
  710. * @memberof CalculationCache#
  711. */
  712. result: function (inputs, calculation) {
  713. if(cachedInputs) {
  714. for(var i = 0; i < inputs.length; i++) {
  715. if(cachedInputs[i] !== inputs[i]) {
  716. cachedOutcome = undefined;
  717. }
  718. }
  719. }
  720. if(!cachedOutcome) {
  721. cachedOutcome = calculation();
  722. cachedInputs = inputs;
  723. }
  724. return cachedOutcome;
  725. }
  726. };
  727. return result;
  728. },
  729. /**
  730. * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
  731. * @param {function} getSourceKey - `function(source)` that must return a key to identify each source object. The result must eather be a string or a number.
  732. * @param {function} createResult - `function(source, index)` that must create a new result object from a given source. This function is identical argument of `Array.map`.
  733. * @param {function} updateResult - `function(source, target, index)` that updates a result to an updated source.
  734. * @returns {Mapping}
  735. */
  736. createMapping: function(getSourceKey, createResult, updateResult /*, deleteTarget*/) {
  737. var keys = [];
  738. var results = [];
  739. return {
  740. /**
  741. * The array of results. These results will be synchronized with the latest array of sources that were provided using {@link Mapping#map}.
  742. * @type {Object[]}
  743. * @memberof Mapping#
  744. */
  745. results: results,
  746. /**
  747. * Maps a new array of sources and updates {@link Mapping#results}.
  748. * @param {Object[]} newSources - The new array of sources.
  749. * @memberof Mapping#
  750. */
  751. map: function(newSources) {
  752. var newKeys = newSources.map(getSourceKey);
  753. var oldTargets = results.slice();
  754. var oldIndex = 0;
  755. for (var i=0;i<newSources.length;i++) {
  756. var source = newSources[i];
  757. var sourceKey = newKeys[i];
  758. if (sourceKey === keys[oldIndex]) {
  759. results[i] = oldTargets[oldIndex];
  760. updateResult(source, oldTargets[oldIndex], i);
  761. oldIndex++;
  762. } else {
  763. var found = false;
  764. for (var j = 1; j < keys.length; j++) {
  765. var searchIndex = (oldIndex + j) % keys.length;
  766. if (keys[searchIndex] === sourceKey) {
  767. results[i] = oldTargets[searchIndex];
  768. updateResult(newSources[i], oldTargets[searchIndex], i);
  769. oldIndex = searchIndex + 1;
  770. found = true;
  771. break;
  772. }
  773. }
  774. if (!found) {
  775. results[i] = createResult(source, i);
  776. }
  777. }
  778. }
  779. results.length = newSources.length;
  780. keys = newKeys;
  781. }
  782. };
  783. },
  784. /**
  785. * Creates a {@link Projector} instance using the provided projectionOptions.
  786. * @param {Object} [projectionOptions] - Options that influence how the DOM is rendered and updated.
  787. * @param {Object} projectionOptions.transitions - A transition strategy to invoke when
  788. * enterAnimation and exitAnimation properties are provided as strings.
  789. * The module `cssTransitions` in the provided `css-transitions.js` file provides such a strategy.
  790. * A transition strategy is not needed when enterAnimation and exitAnimation properties are provided as functions.
  791. * @returns {Projector}
  792. */
  793. createProjector: function (projectionOptions) {
  794. projectionOptions = applyDefaultProjectionOptions(projectionOptions);
  795. projectionOptions.eventHandlerInterceptor = function (propertyName, functionPropertyArgument) {
  796. return function () {
  797. // intercept function calls (event handlers) to do a render afterwards.
  798. projector.scheduleRender();
  799. return functionPropertyArgument.apply(this, arguments);
  800. };
  801. };
  802. var renderCompleted = true;
  803. var scheduled;
  804. var stopped = false;
  805. var projections = [];
  806. var renderFunctions = []; // matches the projections array
  807. var doRender = function () {
  808. scheduled = undefined;
  809. if (!renderCompleted) {
  810. return; // The last render threw an error, it should be logged in the browser console.
  811. }
  812. var s = Date.now()
  813. renderCompleted = false;
  814. for(var i = 0; i < projections.length; i++) {
  815. var updatedVnode = renderFunctions[i]();
  816. projections[i].update(updatedVnode);
  817. }
  818. renderCompleted = true;
  819. if (Date.now()-s > 5)
  820. console.log("Render:", Date.now()-s)
  821. };
  822. var projector = {
  823. /**
  824. * Instructs the projector to re-render to the DOM at the next animation-frame using the registered `renderMaquette` functions.
  825. * This method is automatically called for you when event-handlers that are registered in the {@link VNode}s are invoked.
  826. * You need to call this method for instance when timeouts expire or AJAX responses arrive.
  827. * @memberof Projector#
  828. */
  829. scheduleRender: function () {
  830. if(!scheduled && !stopped) {
  831. scheduled = requestAnimationFrame(doRender);
  832. }
  833. },
  834. /**
  835. * Stops the projector. This means that the registered `renderMaquette` functions will not be called anymore.
  836. * Note that calling {@link Projector#stop} is not mandatory. A projector is a passive object that will get garbage collected as usual if it is no longer in scope.
  837. * @memberof Projector#
  838. */
  839. stop: function () {
  840. if(scheduled) {
  841. cancelAnimationFrame(scheduled);
  842. scheduled = undefined;
  843. }
  844. stopped = true;
  845. },
  846. /**
  847. * Resumes the projector. Use this method to resume rendering after stop was called or an error occurred during rendering.
  848. * @memberof Projector#
  849. */
  850. resume: function() {
  851. stopped = false;
  852. renderCompleted = true;
  853. projector.scheduleRender();
  854. },
  855. /**
  856. * Scans the document for `<script>` tags with `type="text/hyperscript"`.
  857. * The content of these scripts are registered as `renderMaquette` functions.
  858. * The result of evaluating these functions will be inserted into the DOM after the script.
  859. * These scripts can make use of variables that come from the `parameters` parameter.
  860. * @param {Element} rootNode - Element to start scanning at, example: `document.body`.
  861. * @param {Object} parameters - Variables to expose to the scripts. format: `{var1:value1, var2: value2}`
  862. * @memberof Projector#
  863. */
  864. evaluateHyperscript: function (rootNode, parameters) {
  865. var nodes = rootNode.querySelectorAll("script[type='text/hyperscript']");
  866. var functionParameters = ["maquette", "h", "enhancer"];
  867. var parameterValues = [maquette, maquette.h, projector];
  868. Object.keys(parameters).forEach(function (parameterName) {
  869. functionParameters.push(parameterName);
  870. parameterValues.push(parameters[parameterName]);
  871. });
  872. Array.prototype.forEach.call(nodes, function (node) {
  873. var func = new Function(functionParameters, "return " + node.textContent.trim());
  874. var renderFunction = function () {
  875. return func.apply(undefined, parameterValues);
  876. };
  877. projector.insertBefore(node, renderFunction);
  878. });
  879. },
  880. /**
  881. * Appends a new childnode to the DOM using the result from the provided `renderMaquetteFunction`.
  882. * The `renderMaquetteFunction` will be invoked again to update the DOM when needed.
  883. * @param {Element} parentNode - The parent node for the new childNode.
  884. * @param {function} renderMaquetteFunction - Function with zero arguments that returns a {@link VNode} tree.
  885. * @memberof Projector#
  886. */
  887. append: function (parentNode, renderMaquetteFunction) {
  888. projections.push(maquette.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
  889. renderFunctions.push(renderMaquetteFunction);
  890. },
  891. /**
  892. * Inserts a new DOM node using the result from the provided `renderMaquetteFunction`.
  893. * The `renderMaquetteFunction` will be invoked again to update the DOM when needed.
  894. * @param {Element} beforeNode - The node that the DOM Node is inserted before.
  895. * @param {function} renderMaquetteFunction - Function with zero arguments that returns a {@link VNode} tree.
  896. * @memberof Projector#
  897. */
  898. insertBefore: function (beforeNode, renderMaquetteFunction) {
  899. projections.push(maquette.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
  900. renderFunctions.push(renderMaquetteFunction);
  901. },
  902. /**
  903. * Merges a new DOM node using the result from the provided `renderMaquetteFunction` with an existing DOM Node.
  904. * This means that the virtual DOM and real DOM have one overlapping element.
  905. * Therefore the selector for the root {@link VNode} will be ignored, but its properties and children will be applied to the Element provided
  906. * The `renderMaquetteFunction` will be invoked again to update the DOM when needed.
  907. * @param {Element} domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
  908. * @param {function} renderMaquetteFunction - Function with zero arguments that returns a {@link VNode} tree.
  909. * @memberof Projector#
  910. */
  911. merge: function (domNode, renderMaquetteFunction) {
  912. projections.push(maquette.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
  913. renderFunctions.push(renderMaquetteFunction);
  914. },
  915. /**
  916. * Replaces an existing DOM node with the result from the provided `renderMaquetteFunction`.
  917. * The `renderMaquetteFunction` will be invoked again to update the DOM when needed.
  918. * @param {Element} domNode - The DOM node to replace.
  919. * @param {function} renderMaquetteFunction - Function with zero arguments that returns a {@link VNode} tree.
  920. * @memberof Projector#
  921. */
  922. replace: function (domNode, renderMaquetteFunction) {
  923. var vnode = renderMaquetteFunction();
  924. createDom(vnode, domNode.parentNode, domNode, projectionOptions);
  925. domNode.parentNode.removeChild(domNode);
  926. projections.push(createProjection(vnode, projectionOptions));
  927. renderFunctions.push(renderMaquetteFunction);
  928. }
  929. };
  930. return projector;
  931. }
  932. };
  933. if(typeof module !== "undefined" && module.exports) {
  934. // Node and other CommonJS-like environments that support module.exports
  935. module.exports = maquette;
  936. } else if(typeof define === "function" && define.amd) {
  937. // AMD / RequireJS
  938. define(function () {
  939. return maquette;
  940. });
  941. } else {
  942. // Browser
  943. window.maquette = maquette;
  944. }
  945. })(this);