~evanj/cms

7a89d26fa58493882745f6482e4bed87069cd534 — Evan M Jones 1 year, 8 months ago 98f52d2
WIP(Reference+ReferenceList): Getting the ball rolling on reference
types.
A TODO => TODO +4 -0
@@ 0,0 1,4 @@
Handle update of ReferenceList type.
Break cache when a Reference or ReferenceList item has been deleted.
Cache listicles.
Allow updating of space and contenttype.

M internal/m/valuetype/valuetype.go => internal/m/valuetype/valuetype.go +1 -1
@@ 10,9 10,9 @@ const (
	File          ValueTypeEnum = "File"
	Date          ValueTypeEnum = "Date"
	Reference     ValueTypeEnum = "Reference"
	ReferenceList ValueTypeEnum = "ReferenceList"

	// Possible fields for the future.
	// ReferenceList ValueTypeEnum = "ReferenceList"
	// FileList      ValueTypeEnum = "FileList"
)


M internal/s/db/content.go => internal/s/db/content.go +306 -35
@@ 1,7 1,10 @@
package db

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"

	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"


@@ 10,6 13,10 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
)

const (
	defaultDepth = 3 // For fetching reference types.
)

type Content struct {
	// In DB.
	ContentID           string


@@ 19,10 26,21 @@ type Content struct {
}

type ContentValue struct {
	FieldID    string
	FieldType  string // StringSmall
	FieldName  string // Title of a blog post
	FieldValue string // "My First Blog Post!"
	FieldID            string
	FieldType          string            // StringSmall
	FieldName          string            // Title of a blog post
	FieldValue         string            // "My First Blog Post!"
	FieldReference     content.Content   `json:",omitempty"`
	FieldReferenceList []content.Content `json:",omitempty"`
}

type contentValueJSON struct {
	FieldID            string
	FieldType          string     // StringSmall
	FieldName          string     // Title of a blog post
	FieldValue         string     // "My First Blog Post!"
	FieldReference     *Content   `json:",omitempty"`
	FieldReferenceList []*Content `json:",omitempty"`
}

var (


@@ 86,6 104,11 @@ var (
		VALUES (?);
	`

	queryValueNewReference = `
		INSERT INTO cms_value_reference (VALUE) 
		VALUES (?);
	`

	queryValueUpdateStringSmall = `
		UPDATE cms_value_string_small
		SET value = ?


@@ 104,6 127,12 @@ var (
		WHERE cms_value_date.ID IN ( SELECT VALUE_ID FROM cms_value WHERE cms_value.ID = ? );
	`

	queryValueUpdateReference = `
		UPDATE cms_value_reference
		SET value = ?
		WHERE cms_value_reference.ID IN ( SELECT VALUE_ID FROM cms_value WHERE cms_value.ID = ? );
	`

	queryValueGetStringSmallByID = `
		SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_string_small.VALUE
		FROM cms_value


@@ 140,6 169,37 @@ var (
		WHERE cms_value.ID = ?;
	`

	queryValueGetReferenceByID = `
		SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_reference.VALUE
		FROM cms_value
		JOIN cms_contenttype_to_valuetype
		ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype
		ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		JOIN cms_value_reference
		ON cms_value_reference.ID = cms_value.VALUE_ID
		WHERE cms_value.ID = ?;
	`

	queryValueGetReferenceListByID = `
		SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_reference_list_values.CONTENT_ID as VALUE
		FROM cms_value
		JOIN cms_contenttype_to_valuetype
		ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
		JOIN cms_valuetype
		ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
		JOIN cms_value_reference_list
		ON cms_value_reference_list.ID = cms_value.VALUE_ID
		JOIN cms_value_reference_list_values
		ON cms_value_reference_list.ID = cms_value_reference_list_values.VALUE_ID
		WHERE cms_value.ID = ?;
	`

	queryValueGetReferenceListValuesByID = `
		SELECT CONTENT_ID FROM cms_value_reference_list_values 
		WHERE VALUE_ID = ?
	`

	queryValueListByContent = `
		SELECT ID, TYPE, NAME, VALUE FROM (



@@ 189,74 249,155 @@ var (
				OR cms_valuetype.VALUE = 'InputMarkdown'
			)

			UNION

			SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_reference.VALUE, cms_contenttype_to_valuetype.ID as ORDER_ID
			FROM cms_value
			JOIN cms_contenttype_to_valuetype
			ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
			JOIN cms_valuetype
			ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
			JOIN cms_value_reference
			ON cms_value_reference.ID = cms_value.VALUE_ID
			WHERE CONTENT_ID = ?
			AND (
				cms_valuetype.VALUE = 'Reference'
			)

			UNION

			SELECT cms_value.ID as ID, cms_valuetype.VALUE as TYPE, cms_contenttype_to_valuetype.NAME, cms_value_reference_list.ID as VALUE, cms_contenttype_to_valuetype.ID as ORDER_ID
			FROM cms_value
			JOIN cms_contenttype_to_valuetype
			ON cms_contenttype_to_valuetype.ID = cms_value.CONTENTTYPE_TO_VALUETYPE_ID
			JOIN cms_valuetype
			ON cms_valuetype.ID = cms_contenttype_to_valuetype.VALUETYPE_ID
			JOIN cms_value_reference_list
			ON cms_value_reference_list.ID = cms_value.VALUE_ID
			WHERE CONTENT_ID = ?
			AND (
				cms_valuetype.VALUE = 'ReferenceList'
			)

		) AS A

		ORDER BY ORDER_ID ASC;
	`
)

func (db *DB) valueReferenceListNew(s space.Space, ct contenttype.ContentType, c *Content, t *sql.Tx, fieldName string, IDs []string, depth int) error {
	if len(IDs) < 1 {
		return fmt.Errorf("reference list type has no values")
	}

	res, err := t.Exec("INSERT INTO cms_value_reference_list () VALUES ();")
	if err != nil {
		return fmt.Errorf("failed to attach field value of content")
	}

	refListID, err := res.LastInsertId()
	if err != nil {
		return fmt.Errorf("failed to read new field value of content")
	}

	res, err = t.Exec(queryValueNew, c.ID(), ct.ID(), fieldName, refListID)
	if err != nil {
		return fmt.Errorf("failed to attach field value of content")
	}

	_, err = res.LastInsertId()
	if err != nil {
		return fmt.Errorf("failed to read new field value of content")
	}

	var value ContentValue
	if err := t.QueryRow(queryValueGetReferenceListByID, refListID).Scan(&value.FieldID, &value.FieldType, &value.FieldName); err != nil {
		return err
	}

	for _, cid := range IDs {
		_, err := t.Exec("INSERT INTO cms_value_reference_list_values (VALUE_ID, CONTENT_ID) VALUES (?, ?);", refListID, cid)
		if err != nil {
			return err
		}
		db.contentValueAttachRefList(t, &value, depth)
	}

	c.ContentValues = append(c.ContentValues, value)
	return nil
}

type ContentNewParam struct {
	Type  string
	Name  string
	Value string
}

func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params []ContentNewParam) (content.Content, error) {
	res, err := db.Exec(queryContentNew, ct.ID())
func (db *DB) contentNew(space space.Space, ct contenttype.ContentType, params []ContentNewParam, depth int) (content.Content, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	res, err := t.Exec(queryContentNew, ct.ID())
	if err != nil {
		db.log.Println("db.Exec", err)
		return nil, fmt.Errorf("failed to create new content attached to contenttype of '%s'", ct.Name())
	}

	contentID, err := res.LastInsertId()
	if err != nil {
		db.log.Println("res.LastInsertId", err)
		return nil, fmt.Errorf("failed to read new content attached to contenttype of '%s'", ct.Name())
	}

	var content Content
	if err := db.QueryRow(queryContentGetByID, contentID).Scan(&content.ContentID, &content.ContentParentTypeID); err != nil {
		db.log.Println("db.QueryRow", err)
	if err := t.QueryRow(queryContentGetByID, contentID).Scan(&content.ContentID, &content.ContentParentTypeID); err != nil {
		return nil, fmt.Errorf("failed to find content created")
	}

	for _, item := range params {
		// Special data type. Life is hard.
		if item.Type == valuetype.ReferenceList {
			if err := db.valueReferenceListNew(space, ct, &content, t, item.Name, strings.Split(item.Value, "-"), depth); err != nil {
				return nil, err
			}
			continue
		}

		queryValueNewType, queryValueGetTypeByID, _, err := db.valueQuerySetByType(item.Type)
		if err != nil {
			return nil, err
		}

		res, err := db.Exec(queryValueNewType, item.Value)
		res, err := t.Exec(queryValueNewType, item.Value)
		if err != nil {
			// TODO cleanup orphan content.
			db.log.Println("db.Exec", err)
			return nil, fmt.Errorf("failed to attach field value of content")
		}

		valueID, err := res.LastInsertId()
		if err != nil {
			db.log.Println("res.LastInsertId", err)
			return nil, fmt.Errorf("failed to read new field value of content")
		}

		res, err = db.Exec(queryValueNew, contentID, ct.ID(), item.Name, valueID)
		res, err = t.Exec(queryValueNew, contentID, ct.ID(), item.Name, valueID)
		if err != nil {
			// TODO cleanup orphan content.
			db.log.Println("db.Exec", err)
			return nil, fmt.Errorf("failed to attach field value of content")
		}

		valueID, err = res.LastInsertId()
		if err != nil {
			db.log.Println("res.LastInsertId", err)
			return nil, fmt.Errorf("failed to read new field value of content")
		}

		var value ContentValue
		if err := db.QueryRow(queryValueGetTypeByID, valueID).Scan(&value.FieldID, &value.FieldType, &value.FieldName, &value.FieldValue); err != nil {
			db.log.Println("db.QueryRow", err)
		if err := t.QueryRow(queryValueGetTypeByID, valueID).Scan(&value.FieldID, &value.FieldType, &value.FieldName, &value.FieldValue); err != nil {
			return nil, fmt.Errorf("failed to find value created")
		}

		if err := db.contentValueAttachRef(&value, depth); err != nil {
			return nil, err
		}

		content.ContentValues = append(content.ContentValues, value)
	}



@@ 264,7 405,10 @@ func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params [
		return nil, fmt.Errorf("failed to create all values")
	}

	return &content, nil
	return &content, t.Commit()
}
func (db *DB) ContentNew(space space.Space, ct contenttype.ContentType, params []ContentNewParam) (content.Content, error) {
	return db.contentNew(space, ct, params, defaultDepth)
}

type ContentUpdateParam struct {


@@ 280,7 424,6 @@ func (db *DB) ContentUpdate(space space.Space, ct contenttype.ContentType, conte
			return nil, err
		}
		if _, err := db.Exec(queryValueUpdate, item.Value, item.ID); err != nil {
			db.log.Println("db.Exec", err)
			return nil, fmt.Errorf("failed to create update content value '%s'", item.Value)
		}
	}


@@ 309,9 452,9 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp
	var ret []content.Content
	rows, err := db.Query(queryContentListByContentType, ct.ID(), perPage, perPage*page)
	if err != nil {
		db.log.Println(err)
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var content Content


@@ 319,17 462,19 @@ func (db *DB) ContentPerContentType(space space.Space, ct contenttype.ContentTyp
			return nil, err
		}

		rows, err := db.Query(queryValueListByContent, content.ContentID, content.ContentID, content.ContentID)
		rows, err := db.Query(queryValueListByContent, content.ContentID, content.ContentID, content.ContentID, content.ContentID, content.ContentID)
		if err != nil {
			db.log.Println(err)
			return nil, err
		}
		defer rows.Close()

		for rows.Next() {
			var value ContentValue
			if err := rows.Scan(&value.FieldID, &value.FieldType, &value.FieldName, &value.FieldValue); err != nil {
				return nil, err
			}
			// TODO: Should we really fetch reference type values in lists? Probably a
			// horrible idea.
			content.ContentValues = append(content.ContentValues, value)
		}



@@ 345,9 490,9 @@ func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, query
	var ret []content.Content
	rows, err := db.Query(queryContentListByNameAndContentType, ct.ID(), s, perPage, perPage*page)
	if err != nil {
		db.log.Println(err)
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		var content Content


@@ 356,7 501,8 @@ func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, query
		if err := rows.Scan(&content.ContentID, &content.ContentParentTypeID, &fake.FieldValue); err != nil {
			return nil, err
		}

		// TODO: Should we really attach references in search? Probably a horrible
		// idea.
		content.ContentValues = append(content.ContentValues, fake)

		ret = append(ret, &content)


@@ 365,28 511,55 @@ func (db *DB) ContentSearch(space space.Space, ct contenttype.ContentType, query
	return ret, nil
}

func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {
func (db *DB) contentGet(space space.Space, ct contenttype.ContentType, contentID string, depth int) (content.Content, error) {
	t, err := db.Begin()
	if err != nil {
		return nil, err
	}
	defer t.Rollback()

	var content Content
	if err := db.QueryRow(queryContentGetByID, contentID).Scan(&content.ContentID, &content.ContentParentTypeID); err != nil {
		db.log.Println("db.QueryRow", err)
		return nil, fmt.Errorf("failed to find space")
	if err := t.QueryRow(queryContentGetByID, contentID).Scan(&content.ContentID, &content.ContentParentTypeID); err != nil {
		return nil, fmt.Errorf("failed to find content")
	}

	rows, err := db.Query(queryValueListByContent, content.ID(), content.ID(), content.ID())
	// TODO: For some reason t.Query(...) is causing errors here.
	// See: https://github.com/go-sql-driver/mysql/issues/314
	rows, err := db.Query(queryValueListByContent, content.ID(), content.ID(), content.ID(), content.ID(), content.ID())
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find values(s)")
		return nil, fmt.Errorf("failed to find value(s)")
	}
	defer rows.Close()

	for rows.Next() {
		var value ContentValue
		if err := rows.Scan(&value.FieldID, &value.FieldType, &value.FieldName, &value.FieldValue); err != nil {
			return nil, fmt.Errorf("failed to scan values(s)")
		}

		if err := db.contentValueAttachRef(&value, depth); err != nil {
			return nil, err
		}

		if err := db.contentValueAttachRefList(t, &value, depth); err != nil {
			return nil, err
		}

		content.ContentValues = append(content.ContentValues, value)
	}

	return &content, nil
	return &content, t.Commit()
}

func (db *DB) ContentGet(space space.Space, ct contenttype.ContentType, contentID string) (content.Content, error) {
	if space == nil {
		return nil, fmt.Errorf("must provide parent space")
	}
	if ct == nil {
		return nil, fmt.Errorf("must provide parent contenttype")
	}

	return db.contentGet(space, ct, contentID, defaultDepth)
}

func (db *DB) valueQuerySetByType(typ valuetype.ValueTypeEnum) (insert, get, update string, err error) {


@@ 407,11 580,59 @@ func (db *DB) valueQuerySetByType(typ valuetype.ValueTypeEnum) (insert, get, upd
	case valuetype.Date:
		return queryValueNewDate, queryValueGetDateByID, queryValueUpdateDate, nil

	case valuetype.Reference:
		return queryValueNewReference, queryValueGetReferenceByID, queryValueUpdateReference, nil

	}

	return "", "", "", fmt.Errorf("%s is not a valid valuetype", typ)
}

func (db *DB) contentValueAttachRef(c *ContentValue, depth int) error {
	depth--
	if c.Type() != valuetype.Reference || depth < 1 {
		return nil
	}

	ref, err := db.contentGet(nil, nil, c.Value(), depth)
	if err != nil {
		return err
	}

	c.FieldReference = ref
	return nil
}

func (db *DB) contentValueAttachRefList(t *sql.Tx, c *ContentValue, depth int) error {
	depth--
	if c.Type() != valuetype.ReferenceList || depth < 1 {
		return nil
	}

	rows, err := t.Query(queryValueGetReferenceListByID, c.ID())
	if err != nil {
		return err
	}
	defer rows.Close()

	for rows.Next() {
		// NOTE: value.FieldValue here will actually be a content ID.
		var value ContentValue
		if err := rows.Scan(&value.FieldID, &value.FieldType, &value.FieldName, &value.FieldValue); err != nil {
			return err
		}

		ref, err := db.contentGet(nil, nil, value.FieldValue, depth)
		if err != nil {
			return err
		}

		c.FieldReferenceList = append(c.FieldReferenceList, ref)
	}

	return nil
}

func (c *Content) ID() string {
	return c.ContentID
}


@@ 424,6 645,8 @@ func (c *Content) Values() []value.Value {
			item.FieldType,
			item.FieldName,
			item.FieldValue,
			item.FieldReference,
			item.FieldReferenceList,
		})
	}
	return ret


@@ 461,3 684,51 @@ func (c *ContentValue) Name() string {
func (c *ContentValue) Value() string {
	return c.FieldValue
}

func (c *ContentValue) MarshalJSON() (ret []byte, err error) {
	var v contentValueJSON

	v.FieldID = c.FieldID
	v.FieldType = c.FieldType
	v.FieldName = c.FieldName
	v.FieldValue = c.FieldValue

	if c.FieldReference != nil {
		var ok bool
		v.FieldReference, ok = c.FieldReference.(*Content)
		if !ok {
			return ret, fmt.Errorf("corrupted reference value")
		}
	}

	if len(c.FieldReferenceList) > 0 {
		for _, prev := range c.FieldReferenceList {
			next, ok := prev.(*Content)
			if !ok {
				return ret, fmt.Errorf("corrupted reference list value")
			}
			v.FieldReferenceList = append(v.FieldReferenceList, next)
		}
	}

	return json.Marshal(v)
}

func (c *ContentValue) UnmarshalJSON(b []byte) error {
	var v contentValueJSON

	if err := json.Unmarshal(b, &v); err != nil {
		return err
	}

	c.FieldID = v.FieldID
	c.FieldType = v.FieldType
	c.FieldName = v.FieldName
	c.FieldValue = v.FieldValue
	c.FieldReference = v.FieldReference
	for _, item := range v.FieldReferenceList {
		c.FieldReferenceList = append(c.FieldReferenceList, item)
	}

	return nil
}

M internal/s/db/contenttype.go => internal/s/db/contenttype.go +1 -1
@@ 44,7 44,7 @@ func (db *DB) ContentTypeNew(space space.Space, name string, params []ContentTyp

	res, err := t.Exec(queryCreateContentType, name, space.ID())
	if err != nil {
		return nil, fmt.Errorf("failed to create contenttype")
		return nil, err
	}

	id, err := res.LastInsertId()

M internal/s/db/db.go => internal/s/db/db.go +21 -2
@@ 152,15 152,33 @@ func (db *DB) CreateTables() error {
		);
	`)

	// value Date
	// value Reference
	_, _ = db.Exec(`
		CREATE TABLE cms_value_reference (
			ID INTEGER PRIMARY KEY AUTO_INCREMENT,
			VALUE DATE NOT NULL,
			VALUE INTEGER NOT NULL,
			FOREIGN KEY(VALUE) REFERENCES cms_content(ID)
		);
	`)

	// augment to ReferenceList
	_, _ = db.Exec(`
		CREATE TABLE cms_value_reference_list_values (
			ID INTEGER PRIMARY KEY AUTO_INCREMENT,
			VALUE_ID INTEGER NOT NULL,
			CONTENT_ID INTEGER NOT NULL,
			FOREIGN KEY(VALUE_ID) REFERENCES cms_value_reference_list(ID),
			FOREIGN KEY(CONTENT_ID) REFERENCES cms_content(ID)
		);
	`)

	// value ReferenceList
	_, _ = db.Exec(`
		CREATE TABLE cms_value_reference_list (
			ID INTEGER PRIMARY KEY AUTO_INCREMENT
		);
	`)

	// Only valuetypes cms supports.
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.StringSmall)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.StringBig)


@@ 169,6 187,7 @@ func (db *DB) CreateTables() error {
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.File)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Date)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.Reference)
	_, _ = db.Exec(`INSERT INTO cms_valuetype (VALUE) values (?);`, valuetype.ReferenceList)

	return nil
}

M internal/s/tmpl/css/main.css => internal/s/tmpl/css/main.css +16 -0
@@ 17,6 17,10 @@ dialog {
  max-width: 100%%;
}

dialog p { 
  margin-bottom: 16px;
}

dialog menu { 
  width: 100%%;
  padding: 0;


@@ 27,6 31,18 @@ dialog menu > div {
  padding: 16px;
}

dialog menu > div > div { 
  display: flex;
}

dialog .left {
  margin-right: 8px;
}

dialog .right {
  margin-left: 8px;
}

dialog menu span,
dialog menu input { 
  width: 100%%;

M internal/s/tmpl/html/content.html => internal/s/tmpl/html/content.html +17 -0
@@ 52,6 52,22 @@
              <input value="{{ .Value }}" required type=date name="{{ .Type }}-{{ .ID }}" placeholder="{{ .Name }}" />
            {{ end }}

            {{ if eq .Type "Reference" }}
            <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .ID}}" />
              <input class='input-ref' type=button value=Open />
              <dialog>
                <menu>
                  <div>
                    <center>
                      <p>Search for content to use as reference.</p>
                    </center>
                    <input autofocus class='input-contenttype' type=text placeholder='Search by content type' />
                    <input disabled class='input-content' type=text placeholder='Search by content name' />
                  </div>
                </menu>
              </dialog>
            {{ end }}

          {{ end }}

          <input type=submit value=Update />


@@ 73,6 89,7 @@
    {{ template "html/_footer.html" }}
  </main>
  <script src="//unpkg.com/tinymce@5.2.0/tinymce.min.js"></script>
  <script src='//unpkg.com/autocomplete.js@0.37.1/dist/autocomplete.min.js'></script>
  <script>{{ template "js/content.js" $ }}</script>
</body>


M internal/s/tmpl/html/contenttype.html => internal/s/tmpl/html/contenttype.html +21 -1
@@ 68,6 68,26 @@
              </dialog>
            {{ end }}

            {{ if eq .Type "ReferenceList" }}
              <input class='output-ref' required type=hidden name="{{ .Type }}-{{ .Name }}" />
              <input class='input-ref' type=button value=Open />
              <dialog>
                <menu>
                  <div>
                    <center>
                      <p>Search for content to use as reference.</p>
                    </center>
                    <input autofocus class='input-contenttype' type=text placeholder='Search by content type' />
                    <input disabled class='input-content' type=text placeholder='Search by content name' />
                    <div>
                      <input class=left type=button value=Clear />
                      <input class=right type=button value=Done />
                    </div>
                  </div>
                </menu>
              </dialog>
            {{ end }}

          {{ end }}

          <input type=submit value=Create />


@@ 103,7 123,7 @@
    {{ template "html/_footer.html" }}
  </main>
  <script src='//unpkg.com/tinymce@5.2.0/tinymce.min.js'></script>
  <script src='https://unpkg.com/autocomplete.js@0.37.1/dist/autocomplete.min.js'></script>
  <script src='//unpkg.com/autocomplete.js@0.37.1/dist/autocomplete.min.js'></script>
  <script>{{ template "js/content.js" $ }}</script>
</body>


M internal/s/tmpl/html/space.html => internal/s/tmpl/html/space.html +1 -0
@@ 34,6 34,7 @@
              <option disabled value="File">File</option>
              <option disabled value="Date">Date</option>
              <option disabled value="Reference">Reference</option>
              <option disabled value="ReferenceList">ReferenceList</option>
            </select>
            <input disabled type=button value='Remove Field' />
          </div>

M internal/s/tmpl/js/content.js => internal/s/tmpl/js/content.js +50 -4
@@ 50,7 50,13 @@
  var tobtns = document.querySelectorAll(".output-ref")
  for (i = 0; i < refs.length; i++) { 
    (function(btn, menu, dialog, output) { 
      var chosenContentTypeID
      var isList = output.getAttribute("name").indexOf("ReferenceList") != -1
      var clearBtn = dialog.querySelector(".left")
      var doneBtn = dialog.querySelector(".right")

      var chosenContentTypeID // used by both
      var chosenContentIDs = [] // only used be reflist
      var chosenContentNames = [] // only used be reflist

      // OPEN
      btn.addEventListener('click', function(e) { 


@@ 63,6 69,11 @@
      dialog.addEventListener('click', function(e) { 
        e.stopPropagation()
        e.preventDefault()
        if (isList) { 
          // Don't let reflist input close by off click, user must choose to
          // clear input to close, or be done to close.
          return 
        }
        dialog.close()
      })



@@ 72,6 83,34 @@
        e.preventDefault()
      })

      if (isList) {
        // CLEAR
        clearBtn.addEventListener('click', clearBtnHandle)
        function clearBtnHandle(e) { 
          e.stopPropagation()
          e.preventDefault()
          output.value = ''
          btn.value = 'Open'
          chosenContentIDs = []
          chosenContentNames = []
          dialog.close()
        }

        // DONE
        doneBtn.addEventListener('click', function(e) { 
          if (chosenContentIDs.length < 1) {
            return clearBtnHandle(e)
          }
          e.stopPropagation()
          e.preventDefault()
          output.value = chosenContentIDs.join('-')
          btn.value = chosenContentNames.join(', ')
          chosenContentIDs = []
          chosenContentNames = []
          dialog.close()
        })
      }

      // INPUTS EVENTS AND RESULTS
      var inputs = dialog.querySelectorAll('input')
      var contenttype = inputs[0]


@@ 145,9 184,16 @@

      window.autocomplete(content, opts, [contentOpts]).on('autocomplete:selected', onContentSelected)
      function onContentSelected(e, item, dataset, ctx) {
        output.value = item.ContentID
        btn.value = item.FieldValue
        dialog.close()
        if (isList) {
          chosenContentIDs.push(item.ContentID)
          chosenContentNames.push(item.FieldValue)
          btn.value = chosenContentNames.join(', ')
        }
        else {
          output.value = item.ContentID
          btn.value = item.FieldValue
          dialog.close()
        }
      }

    })(refbtns[i], menus[i], refs[i], tobtns[i])

M internal/s/tmpl/js/space.js => internal/s/tmpl/js/space.js +1 -0
@@ 19,6 19,7 @@
          <option value="File">File</option>
          <option value="Date">Date</option>
          <option value="Reference">Reference</option>
          <option value="ReferenceList">ReferenceList</option>
        </select>
        <input type=button id='remove-fieldbtn_${i}' value='Remove Field' />
      </div>

M internal/s/tmpl/tmpls_embed.go => internal/s/tmpl/tmpls_embed.go +106 -5
@@ 26,6 26,10 @@ dialog {
  max-width: 100%;
}

dialog p { 
  margin-bottom: 16px;
}

dialog menu { 
  width: 100%;
  padding: 0;


@@ 36,6 40,18 @@ dialog menu > div {
  padding: 16px;
}

dialog menu > div > div { 
  display: flex;
}

dialog .left {
  margin-right: 8px;
}

dialog .right {
  margin-left: 8px;
}

dialog menu span,
dialog menu input { 
  width: 100%;


@@ 564,6 580,22 @@ blockquote footer {
              <input value="{{ .Value }}" required type=date name="{{ .Type }}-{{ .ID }}" placeholder="{{ .Name }}" />
            {{ end }}

            {{ if eq .Type "Reference" }}
            <input class='output-ref' required type=hidden value="{{ .Value }}" name="{{ .Type }}-{{ .ID}}" />
              <input class='input-ref' type=button value=Open />
              <dialog>
                <menu>
                  <div>
                    <center>
                      <p>Search for content to use as reference.</p>
                    </center>
                    <input autofocus class='input-contenttype' type=text placeholder='Search by content type' />
                    <input disabled class='input-content' type=text placeholder='Search by content name' />
                  </div>
                </menu>
              </dialog>
            {{ end }}

          {{ end }}

          <input type=submit value=Update />


@@ 585,6 617,7 @@ blockquote footer {
    {{ template "html/_footer.html" }}
  </main>
  <script src="//unpkg.com/tinymce@5.2.0/tinymce.min.js"></script>
  <script src='//unpkg.com/autocomplete.js@0.37.1/dist/autocomplete.min.js'></script>
  <script>{{ template "js/content.js" $ }}</script>
</body>



@@ 661,6 694,26 @@ blockquote footer {
              </dialog>
            {{ end }}

            {{ if eq .Type "ReferenceList" }}
              <input class='output-ref' required type=hidden name="{{ .Type }}-{{ .Name }}" />
              <input class='input-ref' type=button value=Open />
              <dialog>
                <menu>
                  <div>
                    <center>
                      <p>Search for content to use as reference.</p>
                    </center>
                    <input autofocus class='input-contenttype' type=text placeholder='Search by content type' />
                    <input disabled class='input-content' type=text placeholder='Search by content name' />
                    <div>
                      <input class=left type=button value=Clear />
                      <input class=right type=button value=Done />
                    </div>
                  </div>
                </menu>
              </dialog>
            {{ end }}

          {{ end }}

          <input type=submit value=Create />


@@ 696,7 749,7 @@ blockquote footer {
    {{ template "html/_footer.html" }}
  </main>
  <script src='//unpkg.com/tinymce@5.2.0/tinymce.min.js'></script>
  <script src='https://unpkg.com/autocomplete.js@0.37.1/dist/autocomplete.min.js'></script>
  <script src='//unpkg.com/autocomplete.js@0.37.1/dist/autocomplete.min.js'></script>
  <script>{{ template "js/content.js" $ }}</script>
</body>



@@ 819,6 872,7 @@ blockquote footer {
              <option disabled value="File">File</option>
              <option disabled value="Date">Date</option>
              <option disabled value="Reference">Reference</option>
              <option disabled value="ReferenceList">ReferenceList</option>
            </select>
            <input disabled type=button value='Remove Field' />
          </div>


@@ 908,7 962,13 @@ blockquote footer {
  var tobtns = document.querySelectorAll(".output-ref")
  for (i = 0; i < refs.length; i++) { 
    (function(btn, menu, dialog, output) { 
      var chosenContentTypeID
      var isList = output.getAttribute("name").indexOf("ReferenceList") != -1
      var clearBtn = dialog.querySelector(".left")
      var doneBtn = dialog.querySelector(".right")

      var chosenContentTypeID // used by both
      var chosenContentIDs = [] // only used be reflist
      var chosenContentNames = [] // only used be reflist

      // OPEN
      btn.addEventListener('click', function(e) { 


@@ 921,6 981,11 @@ blockquote footer {
      dialog.addEventListener('click', function(e) { 
        e.stopPropagation()
        e.preventDefault()
        if (isList) { 
          // Don't let reflist input close by off click, user must choose to
          // clear input to close, or be done to close.
          return 
        }
        dialog.close()
      })



@@ 930,6 995,34 @@ blockquote footer {
        e.preventDefault()
      })

      if (isList) {
        // CLEAR
        clearBtn.addEventListener('click', clearBtnHandle)
        function clearBtnHandle(e) { 
          e.stopPropagation()
          e.preventDefault()
          output.value = ''
          btn.value = 'Open'
          chosenContentIDs = []
          chosenContentNames = []
          dialog.close()
        }

        // DONE
        doneBtn.addEventListener('click', function(e) { 
          if (chosenContentIDs.length < 1) {
            return clearBtnHandle(e)
          }
          e.stopPropagation()
          e.preventDefault()
          output.value = chosenContentIDs.join('-')
          btn.value = chosenContentNames.join(', ')
          chosenContentIDs = []
          chosenContentNames = []
          dialog.close()
        })
      }

      // INPUTS EVENTS AND RESULTS
      var inputs = dialog.querySelectorAll('input')
      var contenttype = inputs[0]


@@ 1003,9 1096,16 @@ blockquote footer {

      window.autocomplete(content, opts, [contentOpts]).on('autocomplete:selected', onContentSelected)
      function onContentSelected(e, item, dataset, ctx) {
        output.value = item.ContentID
        btn.value = item.FieldValue
        dialog.close()
        if (isList) {
          chosenContentIDs.push(item.ContentID)
          chosenContentNames.push(item.FieldValue)
          btn.value = chosenContentNames.join(', ')
        }
        else {
          output.value = item.ContentID
          btn.value = item.FieldValue
          dialog.close()
        }
      }

    })(refbtns[i], menus[i], refs[i], tobtns[i])


@@ 1035,6 1135,7 @@ blockquote footer {
          <option value="File">File</option>
          <option value="Date">Date</option>
          <option value="Reference">Reference</option>
          <option value="ReferenceList">ReferenceList</option>
        </select>
        <input type=button id='remove-fieldbtn_${i}' value='Remove Field' />
      </div>