מוטטורים

‫mutator הוא mixin שמוסיף סריאליזציה נוספת (מצב נוסף שנשמר ונרשם) לבלוק. לדוגמה, בלוקים מובנים כמו controls_if ו-list_create_with צריכים סריאליזציה נוספת כדי לשמור את מספר הקלטות שלהם. יכול להיות שיוסף גם ממשק משתמש כדי שהמשתמש יוכל לשנות את הצורה של הבלוק.

שלושה שינויים של בלוק ליצירת רשימה: ללא קלט, עם שלושה קלטים ועם חמישה קלטים.

שתי מוטציות של בלוק if/do: ‏ if-do ו-if-do-else-if-do-else.

שימו לב: שינוי הצורה של הבלוק לא בהכרח אומר שאתם צריכים סריאליזציה נוספת. לדוגמה, הצורה של הבלוק math_number_property משתנה, אבל זה קורה על סמך שדה של תפריט נפתח, שהערך שלו כבר עובר סריאליזציה. לכן, אפשר להשתמש רק במאמת שדות, ואין צורך בפונקציית שינוי.

בלוק `math_number_property` עם התפריט הנפתח שלו שמוגדר לערך `even`. יש לו קלט של ערך יחיד. בלוק `math_number_property`
עם התפריט הנפתח שלו שמוגדר ל-`divisible by`. יש לו שני קלטי ערכים.

מידע נוסף על מקרים שבהם צריך להשתמש בפונקציית שינוי ומקרים שבהם לא צריך מופיע בדף בנושא סריאליזציה.

בנוסף, אם מספקים כמה שיטות אופציונליות, מוטטורים מספקים ממשק משתמש מובנה שמאפשר למשתמשים לשנות את הצורה של הבלוקים.

קטעי הוק (hooks) של סריאליזציה

ל-Mutators יש שני זוגות של ווים (hooks) לסריאליזציה שהם פועלים איתם. זוג אחד של ווים פועל עם מערכת הסריאליזציה החדשה של JSON, והזוג השני פועל עם מערכת הסריאליזציה הישנה של XML. צריך לספק לפחות אחד מהזוגות האלה.

saveExtraState ו-loadExtraState

saveExtraState ו-loadExtraState הם ווים של סריאליזציה שפועלים עם מערכת הסריאליזציה החדשה של JSON. הפונקציה saveExtraState מחזירה ערך שניתן לסדר אותו ב-JSON ומייצג את המצב הנוסף של הבלוק, והפונקציה loadExtraState מקבלת את אותו ערך שניתן לסדר אותו ב-JSON ומחיל אותו על הבלוק.

// These are the serialization hooks for the lists_create_with block.
saveExtraState: function() {
  return {
    'itemCount': this.itemCount_,
  };
},

loadExtraState: function(state) {
  this.itemCount_ = state['itemCount'];
  // This is a helper function which adds or removes inputs from the block.
  this.updateShape_();
},

קובץ ה-JSON שיתקבל ייראה כך:

{
  "type": "lists_create_with",
  "extraState": {
    "itemCount": 3 // or whatever the count is
  }
}

אין מדינה

אם הבלוק נמצא במצב ברירת המחדל שלו כשהוא עובר סריאליזציה, השיטה saveExtraState יכולה להחזיר null כדי לציין זאת. אם ה-method‏ saveExtraState מחזירה null, המאפיין extraState לא מתווסף ל-JSON. כך גודל קובץ השמירה יישאר קטן.

סדרות מלאות ונתוני גיבוי

הפונקציה saveExtraState מקבלת גם פרמטר אופציונלי doFullSerialization. הוא משמש בלוקים שמפנים למצב שעבר סריאליזציה על ידי סריאליזטור אחר (כמו מודלים של נתונים בסיסיים). הפרמטר מציין שהמצב שאליו יש הפניה לא יהיה זמין כשהבלוק יעבור דה-סריאליזציה, ולכן הבלוק צריך לבצע סריאליזציה של כל מצב הגיבוי בעצמו. לדוגמה, הערך הוא true כשמבצעים סריאליזציה של בלוק בודד, או כשמעתיקים ומדביקים בלוק.

שני תרחישים נפוצים לדוגמה:

  • כשבלוק בודד נטען בסביבת עבודה שבה לא קיים מודל נתונים בסיסי, יש לו מספיק מידע במצב שלו כדי ליצור מודל נתונים חדש.
  • כשמעתיקים ומדביקים בלוק, תמיד נוצר מודל נתונים חדש במקום הפניה למודל קיים.

חלק מהבלוקים שמשתמשים בזה הם הבלוקים @blockly/block-shareable-procedures. בדרך כלל, הם מבצעים סריאליזציה של הפניה למודל נתונים בסיסי שבו מאוחסן המצב שלהם. אבל אם הפרמטר doFullSerialization הוא true, כל המצב שלהם עובר סריאליזציה. הבלוקים של הפרוצדורות שאפשר לשתף משתמשים בזה כדי לוודא שכשהם מועתקים ומודבקים הם יוצרים מודל נתונים חדש, במקום להפנות למודל קיים.

‫mutationToDom ו-domToMutation

mutationToDom ו-domToMutation הם ווים של סריאליזציה שפועלים עם מערכת הסריאליזציה הישנה של XML. כדאי להשתמש ב-hooks האלה רק אם אין ברירה (למשל, אם אתם עובדים על בסיס קוד ישן שלא הועבר עדיין), אחרת כדאי להשתמש ב-saveExtraState וב-loadExtraState.

mutationToDom מחזירה צומת XML שמייצג את המצב הנוסף של הבלוק, ו-domToMutation מקבלת את אותו צומת XML ומחילת את המצב על הבלוק.

// These are the old XML serialization hooks for the lists_create_with block.
mutationToDom: function() {
  // You *must* create a <mutation></mutation> element.
  // This element can have children.
  var container = Blockly.utils.xml.createElement('mutation');
  container.setAttribute('items', this.itemCount_);
  return container;
},

domToMutation: function(xmlElement) {
  this.itemCount_ = parseInt(xmlElement.getAttribute('items'), 10);
  // This is a helper function which adds or removes inputs from the block.
  this.updateShape_();
},

קובץ ה-XML שיתקבל ייראה כך:

<block type="lists_create_with">
  <mutation items="3"></mutation>
</block>

אם הפונקציה mutationToDom מחזירה ערך null, לא יתווסף רכיב נוסף ל-XML.

UI Hooks

אם מספקים פונקציות מסוימות כחלק מהפונקציה לשינוי, Blockly יוסיף ממשק משתמש ברירת מחדל לשינוי לבלוק.

בלוק if-do עם בועת המוטטור שלו פתוחה. כך המשתמשים יכולים להוסיף משפטי else-if ו-else לבלוק if-do.

אם רוצים להוסיף סדרות נוספות, לא חייבים להשתמש בממשק המשתמש הזה. אפשר להשתמש בממשק משתמש מותאם אישית, כמו זה שמוצע בתוסף blocks-plus-minus, או לא להשתמש בממשק משתמש בכלל.

compose and decompose

ממשק המשתמש שמוגדר כברירת מחדל מסתמך על הפונקציות compose ו-decompose.

decompose "מפוצץ" את הבלוק לבלוקים קטנים יותר שאפשר להזיז, להוסיף ולמחוק. הפונקציה הזו צריכה להחזיר 'בלוק עליון', שהוא הבלוק הראשי בסביבת העבודה של הכלי לשינוי מאפיינים שאליו מתחברים בלוקים משניים.

compose מפרש את התצורה של בלוקי המשנה ומשתמש בהם כדי לשנות את הבלוק הראשי. הפונקציה הזו צריכה לקבל כפרמטר את ה-'top block' שהוחזר על ידי decompose.

שימו לב שהפונקציות האלה משולבות בבלוק שעובר שינוי, ולכן אפשר להשתמש ב-this כדי להפנות לבלוק הזה.

// These are the decompose and compose functions for the lists_create_with block.
decompose: function(workspace) {
  // This is a special sub-block that only gets created in the mutator UI.
  // It acts as our "top block"
  var topBlock = workspace.newBlock('lists_create_with_container');
  topBlock.initSvg();

  // Then we add one sub-block for each item in the list.
  var connection = topBlock.getInput('STACK').connection;
  for (var i = 0; i < this.itemCount_; i++) {
    var itemBlock = workspace.newBlock('lists_create_with_item');
    itemBlock.initSvg();
    connection.connect(itemBlock.previousConnection);
    connection = itemBlock.nextConnection;
  }

  // And finally we have to return the top-block.
  return topBlock;
},

// The container block is the top-block returned by decompose.
compose: function(topBlock) {
  // First we get the first sub-block (which represents an input on our main block).
  var itemBlock = topBlock.getInputTargetBlock('STACK');

  // Then we collect up all of the connections of on our main block that are
  // referenced by our sub-blocks.
  // This relates to the saveConnections hook (explained below).
  var connections = [];
  while (itemBlock && !itemBlock.isInsertionMarker()) {  // Ignore insertion markers!
    connections.push(itemBlock.valueConnection_);
    itemBlock = itemBlock.nextConnection &&
        itemBlock.nextConnection.targetBlock();
  }

  // Then we disconnect any children where the sub-block associated with that
  // child has been deleted/removed from the stack.
  for (var i = 0; i < this.itemCount_; i++) {
    var connection = this.getInput('ADD' + i).connection.targetConnection;
    if (connection && connections.indexOf(connection) == -1) {
      connection.disconnect();
    }
  }

  // Then we update the shape of our block (removing or adding iputs as necessary).
  // `this` refers to the main block.
  this.itemCount_ = connections.length;
  this.updateShape_();

  // And finally we reconnect any child blocks.
  for (var i = 0; i < this.itemCount_; i++) {
    connections[i].reconnect(this, 'ADD' + i);
  }
},

saveConnections

אפשר גם להגדיר פונקציה saveConnections שתפעל עם ממשק המשתמש שמוגדר כברירת מחדל. הפונקציה הזו מאפשרת לשייך צאצאים של הבלוק הראשי (שקיים בסביבת העבודה הראשית) לבלוקים משניים שקיימים בסביבת העבודה של המוטטור. אחר כך תוכלו להשתמש בנתונים האלה כדי לוודא שפונקציית composeמתחברת מחדש כמו שצריך לצאצאים של הבלוק הראשי כשמארגנים מחדש את הבלוקים המשניים.

הפונקציה saveConnections צריכה לקבל כפרמטר את 'הבלוק העליון' שמוחזר על ידי הפונקציה decompose. אם הפונקציה saveConnections מוגדרת, Blockly קוראת לה לפני שהיא קוראת לפונקציה compose.

saveConnections: function(topBlock) {
  // First we get the first sub-block (which represents an input on our main block).
  var itemBlock = topBlock.getInputTargetBlock('STACK');

  // Then we go through and assign references to connections on our main block
  // (input.connection.targetConnection) to properties on our sub blocks
  // (itemBlock.valueConnection_).
  var i = 0;
  while (itemBlock) {
    // `this` refers to the main block (which is being "mutated").
    var input = this.getInput('ADD' + i);
    // This is the important line of this function!
    itemBlock.valueConnection_ = input && input.connection.targetConnection;
    i++;
    itemBlock = itemBlock.nextConnection &&
        itemBlock.nextConnection.targetBlock();
  }
},

מבצע רישום

מוטטורים הם סוג מיוחד של mixin, ולכן צריך לרשום אותם לפני שמשתמשים בהם בהגדרת ה-JSON של סוג הבלוק.

// Function signature.
Blockly.Extensions.registerMutator(name, mixinObj, opt_helperFn, opt_blockList);

// Example call.
Blockly.Extensions.registerMutator(
    'controls_if_mutator',
    { /* mutator methods */ },
    undefined,
    ['controls_if_elseif', 'controls_if_else']);
  • name: מחרוזת לשיוך ל-mutator, כדי שאפשר יהיה להשתמש בה ב-JSON.
  • mixinObj: אובייקט שמכיל את שיטות המוטציה השונות. לדוגמה: saveExtraState ו-loadExtraState.
  • opt_helperFn: פונקציית עזר אופציונלית שתפעל בבלוק אחרי שמשלבים את ה-mixin.
  • opt_blockList: מערך אופציונלי של סוגי בלוקים (כמחרוזות) שיתווספו לתפריט הנשלף בממשק המשתמש של כלי העריכה שמוגדר כברירת מחדל, אם מוגדרות גם שיטות ממשק המשתמש.

שימו לב: בניגוד לתוספים, לכל סוג בלוק יכול להיות רק מוטטור אחד.

{
  //...
  "mutator": "controls_if_mutator"
}

פונקציית עזר

בנוסף ל-mixin, יכול להיות שפונקציית mutator תרשום פונקציית עזר. הפונקציה הזו מופעלת בכל בלוק מהסוג שצוין אחרי שהוא נוצר והתווית mixinObj נוספת. אפשר להשתמש בו כדי להוסיף טריגרים או אפקטים נוספים למוטציה.

לדוגמה, אפשר להוסיף לרשימה בלוק עזר שמגדיר את המספר ההתחלתי של הפריטים:

var helper = function() {
  this.itemCount_ = 5;
  this.updateShape();
}